블로그
MCP

게시일 수정일

Shouryamaan 및 Arag

REST API를 MCP 서버로 변환하는 방법

Markdown으로 읽기

REST API 앞에 작은 변환 계층을 두면 MCP 서버로 바꿀 수 있습니다. 이 계층은 작업 단위 도구를 등록하고 입력을 검증하고 기존 엔드포인트를 호출한 뒤 범위가 제한된 구조화 결과를 반환합니다. REST API와 데이터 모델은 바꿀 필요가 없습니다.

읽기 전용 작업 하나로 시작하세요. 엔드포인트 경계가 에이전트에도 적합한지 확인하기 전에 모든 엔드포인트를 도구로 만들지 마세요.

엔드포인트 목록이 아니라 작업에서 시작하세요

OpenAPI 문서는 유용한 목록이지만 엔드포인트 하나를 도구 하나로 기계적으로 변환하면 도구 모음이 복잡해집니다. "주문 1842는 어디 있나요?"에 답하는 에이전트에는 주문 상태 작업이 필요합니다. 내부 조회, 배송 조회, 상태 코드 변환을 각각 호출하며 추론하게 할 필요는 없습니다.

작업, 최소 입력, 가장 작은 유용한 결과를 적으세요. 그런 다음 핸들러 안에서 이 계약을 하나 이상의 REST 호출에 매핑합니다. 안정적인 MCP 도구 설계 가이드는 이름과 도구 경계를 자세히 다룹니다.

타입이 지정된 MCP 도구를 등록하세요

현재 MCP TypeScript SDK는 McpServer, registerTool, createMcpHandler를 제공합니다. 다음 예시는 GET /orders/:id를 읽기 전용 get-order-status 도구로 감쌉니다.

import { createMcpHandler, McpServer } from '@modelcontextprotocol/server';
import * as z from 'zod/v4';

const OrderStatus = z.object({
  orderId: z.string(),
  status: z.enum(['processing', 'shipped', 'delivered', 'cancelled']),
  updatedAt: z.string(),
});

const handler = createMcpHandler(({ authInfo }) => {
  const server = new McpServer({ name: 'orders', version: '1.0.0' });

  server.registerTool(
    'get-order-status',
    {
      description: 'Get the current status of one order the caller can access.',
      inputSchema: z.object({
        orderId: z.string().min(1).describe('The order identifier, such as 1842'),
      }),
      outputSchema: OrderStatus,
    },
    async ({ orderId }) => {
      const response = await fetch(
        `${process.env.ORDERS_API_URL}/orders/${encodeURIComponent(orderId)}`,
        {
          headers: {
            Authorization: `Bearer ${process.env.ORDERS_API_TOKEN}`,
            'X-Caller-ID': authInfo?.clientId ?? 'unknown',
          },
        },
      );

      if (!response.ok) {
        return {
          isError: true,
          content: [{
            type: 'text',
            text: `Order lookup failed with HTTP ${response.status}`,
          }],
        };
      }

      const order = OrderStatus.parse(await response.json());
      return {
        structuredContent: order,
        content: [{ type: 'text', text: JSON.stringify(order) }],
      };
    },
  );

  return server;
});

export default handler;

출력 스키마는 클라이언트에 안정된 결과 형태를 제공합니다. 텍스트 사본은 구조화된 도구 결과를 아직 지원하지 않는 클라이언트와의 호환성을 유지합니다. MCP 도구 명세는 구조화된 콘텐츠가 있을 때 둘 다 반환하도록 권장합니다.

핸들러 앞에서 인증하세요

createMcpHandler는 bearer 토큰을 대신 검증하지 않습니다. 요청이 핸들러에 도달하기 전에 호출자를 검증하고 확인된 ID를 authInfo로 전달하세요. 업스트림 REST 서비스도 해당 ID나 더 좁은 서비스 자격 증명을 인가해야 합니다. 호출자 ID 헤더는 작업 주체 추적에는 유용하지만 그 자체로 인가 수단은 아닙니다.

공급자 토큰은 서버에 보관하세요. 도구 인자, 결과, 프롬프트, 클라이언트에 보이는 오류에 넣지 마세요. 쓰기 도구의 자격 증명은 허용 작업으로 제한하고 파괴적이거나 비용이 발생하는 작업에는 승인을 요구합니다. MCP 인프라 보안 가이드는 권한 경계를 설명합니다.

에이전트가 대응할 수 있는 오류를 반환하세요

HTTP 상태만으로 다음 작업을 판단하기 어려운 경우가 많습니다. 알려진 실패를 짧고 안정된 사유로 변환하세요.

REST 실패 MCP 결과 에이전트 작업
400 잘못된 주문 ID invalid_order_id 재시도 전 인자 수정
401 또는 403 not_authorized 중지하고 다른 범위 요청
404 order_not_found ID 확인 또는 사용자에게 질문
429 재시도 시각이 있는 rate_limited 한 번 기다린 뒤 재시도
5xx upstream_unavailable 작업이 안전할 때만 재시도

변경 작업에는 멱등성 키를 받아 REST API로 전달하세요. 서버가 쓰기를 완료했지만 응답이 사라진 경우, 재시도 래퍼만으로 멱등성이 없는 엔드포인트를 안전하게 만들 수 없습니다.

MCP 클라이언트로 도구를 테스트하세요

REST 핸들러를 직접 호출하지 말고 등록된 계약을 테스트하세요. 성공 호출 하나와 에이전트 동작을 바꾸는 실패 유형을 다룹니다.

  • 누락되거나 잘못된 입력을 스키마가 거부합니다.
  • 한 호출자가 다른 호출자의 주문을 조회할 수 없습니다.
  • 업스트림 시간 초과는 비밀 값 없는 제한된 오류를 반환합니다.
  • 같은 멱등성 키의 반복 쓰기는 효과 하나만 만듭니다.
  • 파괴적 작업은 승인 경계에서 멈춥니다.

SDK의 HTTP 제공 가이드는 DNS 리바인딩 방지를 위해 로컬 서버에서도 Host와 Origin 검증을 요구합니다. 프레임워크 팩토리는 localhost에서 이 보호를 활성화하지만 일반 Node 서버는 직접 추가해야 합니다.

배포 전에 읽기 전용 도구 하나를 검증하세요

자주 묻는 사용자 질문 하나에 답하는 엔드포인트를 고르세요. 입력과 출력 스키마를 정의하고 실제 MCP 클라이언트로 호출한 뒤 전체 결과를 검사합니다. 인가와 실패 테스트가 통과한 뒤에만 배포하세요. 첫 계약으로 깔끔하게 처리할 수 없는 실제 작업이 생길 때 다음 도구를 추가합니다.

첫 도구가 이 검사를 통과했다면 아래 프롬프트로 에이전트에 배포를 맡기세요. 오늘은 도구 하나면 충분합니다.