Fastify와 MCP v2.0 — 무상태 프록시 게이트웨이 구축하기

Maru

@maru

Fastify와 MCP v2.0 — 무상태 프록시 게이트웨이 구축하기

Fastify와 MCP v2.0 — 무상태 프록시 게이트웨이 구축하기

인공지능 에이전트가 사용하는 도구를 안전하게 관리하는 것은 기업용 AI 인프라의 핵심 과제입니다. 모델 컨텍스트 프로토콜(MCP) v2.0 명세가 표준 HTTP 기반의 무상태 아키텍처로 전환되면서, 여러 에이전트 도구를 중앙에서 중계하고 보호하는 프록시 게이트웨이의 중요성이 커졌습니다. 완벽한 플러그인 캡슐화와 뛰어난 라우팅 성능을 제공하는 Fastify v5.10은 이러한 무상태 프록시 레이어를 구축하는 데 가장 적합한 도구입니다. 이 글에서는 Fastify와 최신 MCP v2.0 스택을 결합하여 프로덕션 환경에 즉시 도입할 수 있는 안전한 AI 프록시 설계 패턴을 소개합니다.

무상태 MCP v2.0 패러다임과 프록시의 필요성

모델 컨텍스트 프로토콜(MCP) v2.0의 가장 큰 변화는 세션을 완전히 걷어내고 완벽한 무상태 아키텍처로 전환했다는 점입니다. 기존 v1.0 명세는 클라이언트와 서버가 연결 상태를 계속 유지해야 했습니다. 이 때문에 대규모 트래픽 분산 환경에서 특정 서버로만 요청이 몰리는 스티키 세션 설정을 강제했고, 결과적으로 인프라의 유연한 오토스케일링을 가로막는 병목이 되었습니다.

반면 최신 MCP v2.0은 연결 초기화를 위한 핸드셰이크 과정과 복잡한 세션 식별자 헤더를 완전히 제거했습니다. 이제 모든 도구 호출은 독립적인 단일 HTTP 요청으로 처리됩니다. 도구 서버를 일반적인 웹 API 서버처럼 다룰 수 있게 되면서, 표준 로드 밸런서 뒤에 배치해 수평적으로 자유롭게 확장하는 길이 열렸습니다.

그러나 이러한 무상태 전환이 모든 문제를 해결해주지는 않습니다. 도구 서버의 개수가 늘어날수록 통합 인증, 속도 제한, 비정상 동작 제어 같은 공통 비즈니스 로직을 개별 서버마다 매번 중복 구현하는 것은 비효율적입니다. 보안상 외부 인터넷 망에 원격 도구의 엔드포인트를 그대로 노출하는 것도 위험합니다.

이 지점에서 Fastify 기반의 무상태 프록시 게이트웨이가 강력한 해결책이 됩니다. 고성능 비순환 그래프 라우팅을 지원하는 Fastify를 전면에 배치하면, 백엔드의 실제 도구 서버들을 내부 망에 안전하게 숨기면서 단일 진입점에서 모든 트래픽과 보안 정책을 한 번에 통제할 수 있습니다.

공식 fastify 어댑터를 활용한 라우트 및 플러그인 구성

공식 헬퍼 패키지인 @modelcontextprotocol/fastify를 도입하면, MCP v2.0의 HTTP 기반 무상태 라우트를 매우 간결하게 마운트하고 통제할 수 있습니다. 특히 Fastify가 자랑하는 방향성 비순환 그래프(DAG) 아키텍처는 수많은 에이전트 도구를 스코프 단위로 완전히 격리하고 은닉하는 데 탁월한 구조적 이점을 보장합니다.

공식 가이드를 충족하면서 도구의 스페이스 혼선을 완전히 방지하기 위한 권장 프로젝트 파일 구조는 다음과 같습니다.

text
mcp-gateway/
├── src/
│   ├── plugins/
│   │   └── mcp.ts       # McpServer 인스턴스 전역 등록 플러그인
│   ├── routes/
│   │   └── tools.ts     # 도구 핸들러 마운트 및 라우팅 구성
│   └── app.ts           # createMcpFastifyApp 기반 게이트웨이 진입점
├── package.json
└── tsconfig.json

fastify-plugin을 활용하여 상위 스코프에 전역 공유 자원인 McpServer를 선언하고, 최신 Standard Schema 규격을 따르는 zod로 입력값을 강제하는 실제 구현 코드입니다.

typescript
import fp from 'fastify-plugin';
import { FastifyPluginAsync } from 'fastify';
import { McpServer } from '@modelcontextprotocol/server';
import { z } from 'zod';

declare module 'fastify' {
  interface FastifyInstance {
    mcp: McpServer;
  }
}

export const mcpPlugin: FastifyPluginAsync = fp(async (fastify) => {
  // MCP v2.0 명세를 준수하는 서버 인스턴스 초기화
  const mcpServer = new McpServer({ 
    name: 'secure-agent-gateway', 
    version: '2.0.0' 
  });

  // registerTool API 및 Standard Schema 스펙 기반 Zod 유효성 검증
  mcpServer.registerTool(
    'fetch_db_record',
    {
      description: '데이터베이스에서 지정된 사용자 ID의 레코드를 안전하게 조회합니다.',
      inputSchema: z.object({
        userId: z.string().uuid().describe('조회하려는 사용자의 UUID'),
      }),
    },
    async ({ userId }) => {
      // 비즈니스 로직 및 데이터 조회 영역
      return {
        content: [
          {
            type: 'text',
            text: `ID가 ${userId}인 사용자 레코드를 성공적으로 조회했습니다.`,
          },
        ],
      };
    }
  );

  // Fastify DAG 구조 내에서 하위 라우터들이 활용할 수 있게 데코레이터 등록
  fastify.decorate('mcp', mcpServer);
});

이와 같이 구성하면 @modelcontextprotocol/fastify 패키지에 내장된 DNS 리바인딩 보호와 호스트 헤더 검증 레이어가 작동하여 로컬 서버 인프라를 외부 침입으로부터 보호합니다. 에이전트 도구들은 전역 플러그인이 제공하는 인증 정보나 데이터베이스 커넥션에 상시 안전하게 접근하면서도, 개별 라우터 컨텍스트 내부로 완전히 캡슐화되어 높은 결합도 문제와 보안 혼선 가능성을 완전히 배제할 수 있습니다.

AES-256-GCM을 통한 다자간 왕복 요청 상태 보호

MCP v2.0 명세에서 클라이언트의 추가 입력을 요구하는 다자간 왕복 요청이 발생하면, 서버는 중간 트랜잭션 데이터를 외부 클라이언트에게 임시로 반환해야 합니다. 이때 무상태 아키텍처를 유지하기 위해 중간 상태 데이터를 암호화하여 전달하는 requestState 파라미터를 활용합니다.

단순 서명 방식인 HMAC-SHA256은 데이터 변조를 감지할 수 있지만 평문을 그대로 노출한다는 치명적인 단점이 있습니다. 내부 데이터베이스 식별자나 사용자 권한 정보가 평문으로 노출되면, 악의적인 클라이언트가 이를 악용해 다른 도구의 실행을 유도하는 대리인 혼동 공격에 취약해집니다. 따라서 기밀성과 무결성을 동시에 보장하는 인증된 암호화 방식인 AES-256-GCM 알고리즘을 사용해 내부 상태 정보를 완벽하게 숨겨야 합니다.

Node.js의 기본 crypto 모듈을 활용하면 외부 라이브러리 의존성 없이 안전하게 상태 암호화를 구현할 수 있습니다. 무상태 게이트웨이 환경에서 동작하도록 구성한 TypeScript 헬퍼 코드는 다음과 같습니다.

typescript
import { createCipheriv, createDecipheriv, randomBytes } from 'node:crypto';

const ALGORITHM = 'aes-256-gcm';
const KEY = Buffer.from(process.env.STATE_ENCRYPTION_KEY || '', 'hex'); // 32바이트 대칭키

export function encryptState(plainText: string): string {
  const iv = randomBytes(12);
  const cipher = createCipheriv(ALGORITHM, KEY, iv);
  const encrypted = Buffer.concat([cipher.update(plainText, 'utf8'), cipher.final()]);
  const tag = cipher.getAuthTag();
  
  return Buffer.concat([iv, tag, encrypted]).toString('base64url');
}

export function decryptState(token: string): string {
  const buffer = Buffer.from(token, 'base64url');
  const iv = buffer.subarray(0, 12);
  const tag = buffer.subarray(12, 28);
  const encrypted = buffer.subarray(28);
  
  const decipher = createDecipheriv(ALGORITHM, KEY, iv);
  decipher.setAuthTag(tag);
  return decipher.update(encrypted) + decipher.final('utf8');
}

이 헬퍼는 초기화 벡터와 인증 태그를 암호문과 결합해 하나의 문자열로 반환하므로 복잡한 세션 데이터베이스 없이도 게이트웨이 간 상태 전송이 가능합니다. 이 방식을 통해 성능 병목을 최소화하면서 대리인 혼동 공격과 중간 데이터 위변조를 완벽하게 차단할 수 있습니다.

OAuth 2.1 규격 및 RFC 9728 자원 메타데이터 탐색 구현

엔터프라이즈 환경에서 무상태 AI 프록시를 안정적으로 운영하려면 엄격한 보안 통제와 동적 탐색 기능을 결합한 인가 모델이 필수적입니다. MCP v2.0은 보안이 크게 강화된 최신 OAuth 2.1 규격을 표준으로 채택하여, 세션을 유지하지 않고도 안전하고 강력한 토큰 검증을 지원합니다.

클라이언트가 유효한 액세스 토큰 없이 프록시 게이트웨이의 MCP 엔드포인트에 접근하면, Fastify 서버는 즉시 요청을 차단하고 인증 흐름을 유도해야 합니다. 이를 위해 서버는 HTTP 401 상태 코드와 함께 WWW-Authenticate 헤더에 RFC 9728 명세를 준수하는 보호된 자원 메타데이터의 주소를 실어 반환합니다.

클라이언트는 이 헤더에 담긴 경로에서 메타데이터를 동적으로 파싱해 신뢰할 수 있는 인증 서버와 유효한 스코프 정보를 식별하고 토큰을 발급받게 됩니다. 다음은 Fastify v5.10에서 유효하지 않은 요청에 대해 RFC 9728 규격에 맞는 401 인증 챌린지 응답을 처리하는 preHandler 훅 구현 예시입니다.

typescript
import type { FastifyPluginAsync } from 'fastify';

export const oauthSecurityPlugin: FastifyPluginAsync = async (fastify) => {
  fastify.addHook('preHandler', async (request, reply) => {
    const authHeader = request.headers.authorization;

    if (!authHeader || !authHeader.startsWith('Bearer ')) {
      // RFC 9728에 맞춰 보호된 자원 메타데이터 제공용 동적 URL 생성
      const metadataUrl = `${request.protocol}://${request.host}/.well-known/oauth-protected-resource`;

      reply
        .code(401)
        .header(
          'WWW-Authenticate',
          `Bearer error="invalid_token", error_description="Authentication required", resource_metadata="${metadataUrl}"`
        );

      return reply.send({
        error: 'unauthorized',
        error_description: 'Valid access token is required.'
      });
    }
  });
};

이 방식을 적용하면 게이트웨이 내부 인가 메커니즘이나 ID 인증 기관이 변경되더라도, 클라이언트의 정적 코드를 전혀 수정하지 않고 유연하게 인증 레이어를 실시간 조정할 수 있어 유지보수 효율성이 대폭 향상됩니다.

Fastify v5.10 LogController를 활용한 분산 추적 로깅

무상태 AI 인프라에서는 에이전트 요청 하나가 백엔드 내부에서 수많은 도구 호출로 이어지기 때문에, 마이크로서비스 전체를 관통하는 일관된 분산 추적 체계가 필수적입니다. Fastify v5.10.0부터는 이전 버전의 정적 로깅 옵션이었던 disableRequestLoggingrequestIdLogLabel이 공식적으로 지원 중단되었습니다(각각 FSTDEP023, FSTDEP024). 대신 새롭게 도입된 객체지향 기반의 LogController 아키텍처를 사용하면 엔진 수준에서 로그 필터링과 컨텍스트 주입의 수명 주기를 고성능으로 제어할 수 있습니다.

이 아키텍처를 구현하기 위해 LogController 클래스를 상속받은 커스텀 컨트롤러를 설계하면 편리합니다. 클라이언트가 보낸 HTTP 헤더의 W3C 분산 추적 식별자인 traceparentbaggage 뿐만 아니라, MCP v2.0 명세가 JSON-RPC의 _meta 필드를 통해 전달하는 에이전트 메타데이터를 동적으로 추출하여 Fastify의 기본 요청 로거에 주입할 수 있습니다. 다음은 Fastify v5.10+ 환경에 완벽히 부합하는 스레드 안전한 동적 추적 컨트롤러 코드입니다.

typescript
import { LogController, FastifyRequest, FastifyReply } from 'fastify';

export class AgentTraceController extends LogController {
  override incomingRequest(
    request: FastifyRequest,
    reply: FastifyReply,
    metadata?: Record<string, unknown>
  ): void {
    // W3C 분산 추적 헤더 추출
    const traceparent = request.headers['traceparent'] as string | undefined;
    const baggage = request.headers['baggage'] as string | undefined;

    // MCP v2.0 JSON-RPC 메타데이터 추출
    const body = request.body as Record<string, any> | undefined;
    const mcpMeta = body?._meta;

    // Fastify 자식 로거에 실시간 분산 추적 컨텍스트 동적 바인딩
    request.log.info(
      {
        traceparent: traceparent || mcpMeta?.traceparent,
        baggage: baggage || mcpMeta?.baggage,
        ...metadata,
      },
      'MCP 요청 수신됨'
    );
  }

  override requestCompleted(
    error: Error | null,
    request: FastifyRequest,
    reply: FastifyReply,
    metadata?: Record<string, unknown>
  ): void {
    const traceparent = request.headers['traceparent'] as string | undefined;
    const logData = {
      traceparent,
      durationMs: reply.elapsedTime, // Fastify v5에서 정밀 측정을 지원하는 경과 시간 프로퍼티
      ...metadata,
    };

    if (error) {
      request.log.error({ ...logData, err: error }, 'MCP 요청 처리 실패');
    } else {
      request.log.info(logData, 'MCP 요청 완료');
    }
  }
}

이렇게 컨트롤러 레이어에서 주입한 일관된 분산 추적 데이터는 OpenTelemetry 및 OpenInference 규격과 자연스럽게 결합됩니다. 개별 에이전트 실행 흐름이 단일 폭포수 트레이스 형태로 수집되므로, 무상태 게이트웨이를 거치는 다단계 호출 과정에서 특정 AI 도구의 응답 지연이나 도구 작동이 비정상적으로 멈추는 조용한 실패 문제를 실시간 APM 도구를 통해 빠르고 안전하게 포착할 수 있습니다.

Fastify 기반 고성능 MCP v2 프록시 실전 코드 구현

앞서 다룬 무상태 설계 패턴, AES-256-GCM 보안 검증, 그리고 새로운 LogController를 결합하여 실제 동작하는 Fastify 기반 MCP v2.0 프록시 게이트웨이를 구현할 수 있습니다. 이 구현 방식은 분산형 에이전트 환경에서도 도구 실행의 신뢰성과 일관된 모니터링을 완벽하게 보장합니다.

다음은 Fastify v5.10.0과 공식 @modelcontextprotocol/server 패키지를 사용하여 작성한 실전 프록시 게이트웨이의 핵심 백엔드 코드입니다.

typescript
import Fastify, { LogController, FastifyRequest, FastifyReply } from 'fastify';
import { McpServer } from '@modelcontextprotocol/server';
import { z } from 'zod';
import crypto from 'node:crypto';

// 1. W3C 분산 추적 헤더 주입을 위한 Fastify v5.10 LogController 구현
class TraceLogController extends LogController {
  override incomingRequest(request: FastifyRequest, reply: FastifyReply, metadata?: Record<string, unknown>) {
    super.incomingRequest(request, reply, {
      ...metadata,
      traceparent: request.headers['traceparent'],
      baggage: request.headers['baggage'],
    });
  }
}

const app = Fastify({
  logger: true,
  logController: new TraceLogController(),
});

// 2. MCP v2.0 서버 초기화
const mcpServer = new McpServer({
  name: 'enterprise-secure-proxy',
  version: '2.0.0',
});

// 3. Standard Schema v1.0 규격을 준수하는 Zod 기반 도구 등록
mcpServer.registerTool(
  'process_payment',
  {
    description: '안전한 결제 트랜잭션을 처리합니다.',
    inputSchema: z.object({
      amount: z.number().positive(),
      currency: z.string().length(3),
      encryptedState: z.string(), // AES-256-GCM 암호화된 상태 값
    }),
  },
  async ({ amount, currency, encryptedState }) => {
    // AES-256-GCM 검증 및 복호화 메커니즘 실행
    try {
      const decrypted = decryptState(encryptedState);
      const state = JSON.parse(decrypted);
      
      return {
        content: [{
          type: 'text',
          text: `결제 승인 완료: ${amount} ${currency} (주문 ID: ${state.orderId})`,
        }],
      };
    } catch (err) {
      throw new Error('유효하지 않거나 변조된 요청 상태 값입니다.');
    }
  }
);

// 4. AES-256-GCM 복호화 헬퍼 함수
function decryptState(token: string): string {
  const secretKey = Buffer.from(process.env.STATE_SECRET_KEY || '', 'hex'); // 32바이트 키
  const [ivHex, tagHex, encrypted] = token.split(':');
  
  const decipher = crypto.createDecipheriv('aes-256-gcm', secretKey, Buffer.from(ivHex, 'hex'));
  decipher.setAuthTag(Buffer.from(tagHex, 'hex'));
  
  let decrypted = decipher.update(encrypted, 'hex', 'utf8');
  decrypted += decipher.final('utf8');
  return decrypted;
}

// 5. 프록시 라우트 등록 및 OAuth 2.1 인증 검증
app.post('/mcp', async (request, reply) => {
  const authHeader = request.headers.authorization;
  if (!authHeader?.startsWith('Bearer ')) {
    return reply
      .status(401)
      .header('WWW-Authenticate', 'Bearer error="invalid_token", resource_metadata="https://api.example.com/prm"')
      .send({ error: '인증 헤더가 누락되었거나 유효하지 않습니다.' });
  }

  // 이후 흐름은 mcpServer 핸들러에 바인딩하여 무상태 JSON-RPC 요청을 수행합니다.
});

이 실전 구현체는 엔터프라이즈 환경에서 요구하는 세 가지 안정성 기준을 해결합니다. 첫째, TraceLogController를 활용해 인입 요청의 W3C 추적 식별자를 가로채고 비동기 에이전트 흐름 전체를 투명하게 로깅합니다. 둘째, 최신 registerTool API와 Zod v4를 조합하여 입력 스키마 유효성 검사를 런타임 단계에서 강력하게 통제합니다. 셋째, 클라이언트가 전달한 무상태 왕복 파라미터인 암호화 상태 값을 AES-256-GCM 방식으로 정밀 검증 및 복호화하여 대리인 혼동 공격이나 데이터 변조 시도를 미연에 방지합니다.

안전하고 확장 가능한 AI 도구 백엔드 준비하기

Fastify v5.10과 MCP v2.0의 무상태 HTTP 아키텍처는 대규모 분산 환경에서 AI 에이전트 인프라를 확장하기 위한 강력한 이정표를 제시합니다. 세션 관리 부담이 사라진 대신, 프록시 레이어에서의 철저한 보안 통제와 분산 추적이 서비스의 성패를 가릅니다. 안정적인 프로덕션 운영을 위해 AES-256-GCM 비밀키의 주기적인 회전 정책을 수립하고, RFC 9728 규격에 맞춘 동적 자원 메타데이터 인증 흐름이 클라이언트 사양과 올바르게 연동되는지 검증해야 합니다. 마지막으로 새롭게 도입된 LogController 기반의 추적 로깅이 다자간 왕복 요청 전체의 비정상 로그를 빈틈없이 집계하는지 확인하며 안전하고 견고한 AI 도구 백엔드를 완성해 나가십시오.


참고 링크