CodeGym /행동 /ChatGPT Apps /기존 제품에 ChatGPT App 통합 및 SDK/MCP 마이그레이션

기존 제품에 ChatGPT App 통합 및 SDK/MCP 마이그레이션

ChatGPT Apps
레벨 20 , 레슨 2
사용 가능

1. 왜 통합과 마이그레이션을 이야기해야 할까

지금까지는 우리가 편한 방식으로 API와 도구를 설계해 왔습니다. 현실에서는 거의 항상 반대입니다. 이미 다음이 있습니다:

  • 모놀리스 혹은 여러 마이크로서비스 묶음;
  • REST/GraphQL API;
  • 수년간 프로덕션에서 돌아온 비즈니스 로직.

그런데 갑자기 이런 과제가 생깁니다: “우리 제품을 Apps SDK와 MCP를 통해 ChatGPT에 연결해 주세요.”

모든 것을 ‘이상적인 MCP 서버’에 맞춰 다시 쓰는 건 답이 아닙니다. 기존 세계 위에 얇은 레이어를 신중히 “덧씌워” 백엔드의 언어를 ChatGPT의 언어(도구, 리소스, 스키마)로 번역해야 합니다.

두 번째 문제: 제품은 살아 있습니다. 스키마와 API는 바뀝니다. 일반 프런트엔드에서는 필드를 바꾸면 즉시 TypeScript 오류를 받곤 합니다. LLM‑Apps 세계는 더 교묘합니다. 모델은 계속 예전 포맷을 자신 있게 보내고, 도구는 실패하며, 빌드 타임에 깨끗이 터지지 않고 다음과 같은 상황을 맞이합니다:

  • MCP 서버에서의 런타임 오류;
  • “대략 무슨 필드를 원하는지 알겠다”는 식의 환각;
  • 짜증나는 품질 인시던트.

그래서 이 강의에서는 MCP+Apps 레이어를 다음과 같이 봅니다:

  • 기존 백엔드에 대한 어댑터;
  • 수년간 유지해야 할 계약;
  • 버전, 애노테이션, scopes, SDK가 얽힌 마이그레이션의 대상.

2. 통합 아키텍처: 기존 백엔드 위의 어댑터로서 MCP

기본 그림

프로덕션 관점에서 스택을 다시 정리해 봅시다:

flowchart LR
  U[ChatGPT의 사용자] --> G[ChatGPT 모델]
  G -->|App 호출| W["위젯 (Apps SDK, Next.js)"]
  G -->|tools.call| MCP[MCP-서버 / Gateway]
  MCP --> S1["Gift Service (기존 서비스)"]
  MCP --> S2["Commerce Service (주문, ACP)"]

ChatGPT는 MCP 프로토콜을 통해 여러분의 세계와 통신합니다. 도구/리소스 목록, tools/call 호출, 이벤트 스트리밍 등입니다.

이 구조에서 MCP 서버는 바로 그 어댑터입니다. ChatGPT(JSON‑RPC, 도구)도 알고, 여러분의 서비스(REST/DB/큐)도 알아서 서로를 변환합니다.

Gateway/Adapter로서의 MCP

전형적인 상황: 이미 REST 엔드포인트를 가진 Gift Service가 있습니다:

// 기존 REST API의 예
GET  /api/gifts/recommendations?budget=100&occasion=birthday
POST /api/orders

새 비즈니스 로직을 쓰지 않고 MCP 레이어가 이를 Tool로 감쌉니다:

// mcp/tools/recommendGifts.ts
import { z } from "zod";
import { server } from "./mcpServer"; // 가상의 SDK 인스턴스

const recommendGiftsInput = z.object({
  occasion: z.string(),
  budgetUsd: z.number().int().positive(),
});

server.registerTool({
  name: "recommend_gifts",
  description: "예산 범위 내에서 선물 아이디어를 추천합니다",
  inputSchema: recommendGiftsInput,
  async execute(args) {
    const { occasion, budgetUsd } = recommendGiftsInput.parse(args);
    const res = await fetch(
      `https://api.myapp.com/gifts/recommendations?budget=${budgetUsd}&occasion=${occasion}`,
    );
    return res.json(); // 중요: 모델과 위젯 모두에 편한 JSON을 반환
  },
});

선물 추천 로직은 기존 서비스 내부에 그대로 남습니다. MCP 레이어는 ChatGPT의 언어를 여러분의 API 언어로 바꾸는 “얇은 번역기”입니다.

때로는 MCP 레이어가 여러 백엔드 서비스로의 라우팅도 담당합니다. 이 경우 완전한 MCP Gateway가 되며, 그 역할은 프로덕션과 네트워크 모듈에서 더 깊게 다룹니다.

Monolith-integrated MCP vs Sidecar MCP

MCP 레이어를 “어디에” 붙일지에 대한 두 가지 기본 옵션이 있습니다.

텍스트로 정리하면 다음과 같습니다:

옵션 설명 MCP 코드의 위치
Monolith-integrated 모든 것이 하나의 Next.js/Node 서비스 안에 있음 Next.js API 라우트 또는 Express
Sidecar MCP API와 통신하는 별도 컨테이너/서비스 별도의 Node/Go 애플리케이션

작은 프로젝트라면 첫 번째 옵션으로도 충분한 경우가 많습니다. Next.js 애플리케이션을 Vercel에 배포하고, /mcp 또는 /api/mcp 라우트를 두어 MCP 서버를 다른 API 옆에 둡니다.

예시(대폭 단순화):

// app/api/mcp/route.ts (Next.js 16)
import { NextRequest } from "next/server";
import { mcpHandler } from "@/mcp/server";

export async function POST(req: NextRequest) {
  const body = await req.json();
  const response = await mcpHandler.handle(body); // JSON-RPC 요청
  return new Response(JSON.stringify(response), {
    headers: { "content-type": "application/json" },
  });
}

Gift, Commerce, Analytics 등 여러 도메인 서비스가 있는 성숙한 아키텍처에서는 MCP 레이어를 별도 Gateway 서비스로 분리하는 것이 편리합니다. ChatGPT에서 오는 MCP 트래픽을 받고 도구 이름에 따라 다양한 백엔드로 호출을 라우팅합니다.

중요: ChatGPT와 Apps SDK의 관점에서는 여전히 하나의 MCP 서버입니다. 그게 모놀리스 내부에 있든 별도 마이크로서비스로 돌든은 여러분의 아키텍처 선택입니다.

MCP 레이어가 어디에 사는지는 정리했습니다. 이제 무엇을 받고 내보내는지가 문제인데, 여기서 스키마와 계약이 중요해집니다.

3. Single Source of Truth: 스키마, 타입, 계약 테스트

내부 DTO, 외부 REST 계약, 그리고 MCP 도구 스키마가 따로 있다 보면, “대충 그려서” 스키마를 만드는 유혹이 큽니다. 결과는 뻔합니다:

  • 백엔드 필드를 바꾸고 도구 스키마 업데이트를 잊음;
  • 모델은 계속 예전 포맷을 보냄;
  • 즐거운 런타임 동물원이 펼쳐짐.

정상 경로는 데이터 구조에 대한 단일 진실 소스를 만들고, 그것을 어디서나 쓰는 것입니다. TypeScript 세계에서는 MCP SDK가 JSON Schema로 변환할 수 있는 Zod 같은 라이브러리를 쓰면 아주 편합니다.

GiftGenius를 위한 공통 Zod 스키마

예를 들어 학습용 GiftGenius의 Gift 서비스가 이미 입력 검증에 Zod를 사용한다고 가정해 봅시다:

// domain/gifts.ts
import { z } from "zod";

export const giftRecommendationInputSchema = z.object({
  occasion: z.string().describe("계기: birthday, wedding 등"),
  budgetUsd: z.number().int().positive(),
  recipientProfile: z.string().describe("사람에 대한 간단한 설명"),
});

export type GiftRecommendationInput = z.infer<
  typeof giftRecommendationInputSchema
>;

이 스키마는 다음에도 사용됩니다:

  • REST 엔드포인트(요청 본문 검증);
  • MCP 도구(inputSchema로서);
  • 테스트(픽스처의 기반).

스키마를 MCP 도구에 연결하기

// mcp/tools/recommendGifts.ts
import { giftRecommendationInputSchema } from "@/domain/gifts";
import { server } from "../mcpServer";

server.registerTool({
  name: "recommend_gifts",
  description: "프로필과 예산에 맞는 선물 추천",
  inputSchema: giftRecommendationInputSchema,
  async execute(args) {
    const input = giftRecommendationInputSchema.parse(args);

    const res = await fetch("https://api.myapp.com/gifts/recommendations", {
      method: "POST",
      headers: { "content-type": "application/json" },
      body: JSON.stringify(input),
    });

    return res.json();
  },
});

SDK가 Zod 스키마를 JSON Schema로 자동 변환하며, ChatGPT는 이를 tools/list에서 확인합니다. 이로써 두 가지 문제가 한 번에 해결됩니다:

  • 도구 인자 타입과 코드가 강하게 연결됩니다;
  • 스키마가 바뀌면 TypeScript 컴파일러가 핸들러 업데이트를 강제합니다.

MCP ↔ 백엔드 계약 테스트

여기서 계약 테스트는 거창한 단어가 아니라 꽤 현실적인 몇 가지 점검입니다.

가장 단순한 unit/contract 테스트는 이렇게 생길 수 있습니다:

// tests/mcp/recommendGifts.contract.test.ts
import { giftRecommendationInputSchema } from "@/domain/gifts";

test("샘플 요청이 도구 스키마와 일치한다", () => {
  const sample = {
    occasion: "birthday",
    budgetUsd: 150,
    recipientProfile: "동료, 가젯을 좋아함",
  };

  expect(() => giftRecommendationInputSchema.parse(sample)).not.toThrow();
});

이 테스트가 세상을 보장하진 않지만, 스키마를 바꿔놓고 픽스처 업데이트를 잊은 경우 등 MCP 레이어와 백엔드 기대치 간 불일치를 최소한 잡아냅니다.

이 접근은 쉽게 확장됩니다:

  • 외부 API(Stripe, CMS) 모의 응답;
  • 테스트 환경의 실제 MCP 서버에 대해 MCP 클라이언트를 구동.

4. tools와 resources의 버전 관리 전략

스키마는 언젠가 바뀝니다. 핵심은 “그냥 필드 이름만 바꾸면 되잖아” 식으로 하지 않는 것입니다. LLM 세계에서는 빌드만 깨지는 게 아니라 모델 동작까지 깨질 수 있습니다. 예전 프롬프트, 저장된 대화, 골든 케이스가 예전 계약을 계속 기대합니다.

가산적 변경 vs 깨뜨리는 변경

대략 두 범주로 나눌 수 있습니다.

가산적 변경 — 무언가를 추가하지만 기존을 깨지 않습니다:

  • 응답에 새로운 선택적 필드 추가;
  • 기본값이 있는 새로운 선택적 인자 추가;
  • UI나 모델이 중립적으로 다룰 수 있는 enum의 추가 값.

예를 들어 도구의 응답에 deliveryEstimateDays를 추가했지만, 예전 위젯은 이를 무시합니다. 안전합니다. 스키마는 확장되지만 아무도 반드시 써야 하는 건 아닙니다.

깨뜨리는 변경 — 기존 기대를 깨는 경우입니다:

  • 예전에는 없던 필드를 필수로 만듦;
  • 타입 변경(문자열 → 객체);
  • 인자의 의미 변경(USD 예산 → 로컬 통화 예산으로 변경하면서 필드 이름은 그대로 둠).

이런 경우 유일하게 안전한 방법은 새 버전의 도구를 만드는 것입니다.

Tool_v2 패턴

기존에 recommend_gifts가 있고, 스키마를 크게 바꾸려 한다고 합시다. 기존 도구를 건드리지 않고 새로운 recommend_gifts_v2를 만듭니다.

// v1
const recommendGiftsInput_v1 = z.object({
  occasion: z.string(),
  budgetUsd: z.number().int().positive(),
});

// v2: 통화 및 배송 필터 지원
const recommendGiftsInput_v2 = z.object({
  occasion: z.string(),
  maxPrice: z.number().int().positive(),
  currency: z.enum(["USD", "EUR", "GBP"]),
  deliverByDate: z.string().optional(); // ISO-문자열
});

server.registerTool({
  name: "recommend_gifts",
  description: "DEPRECATED: recommend_gifts_v2를 사용하세요",
  inputSchema: recommendGiftsInput_v1,
  async execute(args) { /* 기존 로직 */ },
});

server.registerTool({
  name: "recommend_gifts_v2",
  description:
    "예산, 통화, 배송 마감일을 고려한 선물 추천",
  inputSchema: recommendGiftsInput_v2,
  async execute(args) { /* 새 로직 */ },
});

모델과 예전 프롬프트/에이전트는 여러분이 업데이트하기 전까지 recommend_gifts를 계속 사용합니다. 새로운 시나리오는 recommend_gifts_v2를 대상으로 작성합니다.

마이그레이션 기간이 지난 후:

  • 골든 케이스와 에이전트를 v2로 전환;
  • 메트릭에서 v1 호출이 거의 없음을 확인;

이제 v1을 점진적으로 정리할 수 있습니다(예: dev/staging의 도구 목록에서 먼저 숨기고, 이후 프로덕션에서도 숨김).

리소스의 버전 관리

버전이 필요한 것은 도구만이 아닙니다. resources가 있다면(예: 선물 카탈로그 같은 정적 리소스) 이것도 버전을 두는 것이 좋습니다.

일반적인 방법:

  • 리소스 이름에 버전을 포함: gift_catalog.v1.json, gift_catalog.v2.json;
  • 또는 URI/파라미터에 버전 전달: /api/catalog?version=1.

의미는 같습니다. 이미 실행 중인 시나리오의 발밑에서 데이터를 바꾸지 말고, 명시적으로 고정된 버전의 카탈로그를 제공하세요.

무중단 마이그레이션

전형적인 도구 마이그레이션 사이클:

  1. 기존과 병행해 새 버전(_v2)을 추가합니다.
  2. App/에이전트/system‑prompt를 업데이트해 새 버전을 사용하도록 합니다.
  3. 두 버전에 대해 골든 케이스와 LLM‑eval을 실행하여 핵심 시나리오의 품질이 떨어지지 않았음을 확인합니다.
  4. v1 대비 v2 사용률(및 오류)을 관찰합니다.
  5. v1 트래픽이 거의 0에 가까워지면 비활성화를 시작합니다.

이 접근은 스키마 마이그레이션, SDK/프로토콜 업데이트, Auth 변경에도 잘 작동합니다. 도구와 리소스는 v1/v2와 가산적 변경으로 진화합니다. 계약의 두 번째 큰 축은 인증과 인가입니다. OAuth, scopes, .well-known은 역시 장수하고 섬세한 마이그레이션이 필요합니다.

5. 인증의 진화: .well-known, scopes, 기존 OAuth

이미 OAuth 2.1/OpenID Connect 세계에서 운영 중이라면, MCP를 통한 ChatGPT 통합은 “또 하나의 로그인”이 아니라, 여러분의 Authorization Server와 표준 규칙으로 대화해야 하는 새로운 클라이언트입니다.

MCP와 .well-known/oauth-protected-resource

완전한 OAuth 2.1/OpenID Connect 및 Auth Server 설정은 별도 모듈에서 자세히 다룹니다(인증 모듈 참조). 여기서는 실무 측면만 봅니다. MCP 리소스가 ChatGPT에 OAuth로 보호됨을 알리고, linking 플로우를 어떻게 시작하는지입니다.

보호된 MCP 리소스의 표준 패턴:

  • MCP 서버가 특별한 엔드포인트 /.well-known/oauth-protected-resource를 노출합니다.
  • 응답에서 어떤 리소스이며 어떤 AS(Authorization Server)로 보호되는지 알려줍니다.
  • MCP 호출에서 401이 날 때 서버는 WWW-Authenticate 헤더에 해당 .well-known 링크를 넣어 반환하며, ChatGPT가 OAuth 플로우(“Link account”)를 스스로 시작합니다.

Express의 최소 예시:

// mcp-auth/.well-known.ts
import express from "express";
const app = express();

app.get("/.well-known/oauth-protected-resource", (_req, res) => {
  res.json({
    resource: "https://mcp.myapp.com",
    authorization_servers: [
      "https://auth.myapp.com/.well-known/openid-configuration",
    ],
  });
});

app.listen(3000);

401 응답에서 클라이언트를 위한 힌트 핸들러:

res
  .status(401)
  .set(
    "WWW-Authenticate",
    'Bearer resource_metadata="https://mcp.myapp.com/.well-known/oauth-protected-resource"',
  )
  .end();

ChatGPT는 이 헤더를 보고 어느 AS로 가서 여러분의 MCP 리소스에 대한 OAuth 플로우를 시작해야 하는지 이해합니다.

Scopes와 인가 마이그레이션

Scopes도 마이그레이션 이슈의 원천입니다. Auth 모듈에서 자세히 다뤘지만, 통합/마이그레이션 맥락에서 중요한 포인트가 있습니다.

처음에는 GiftGenius가 카탈로그 읽기(gifts.read)만 했다가, 나중에 주문 생성을 위한 gifts.write를 추가했다고 가정합시다. 해야 할 일:

  • 클라이언트(ChatGPT App) 설정에 새로운 scope를 추가;
  • 실제로 변경을 일으키는 도구에만 그 scope를 요구하도록 MCP 서버 업데이트;
  • 필요하다면 .well-known에도 변경 사항을 기술.

UX 관점에서는 사용자가 새 기능을 쓰려 할 때 ChatGPT 앱의 권한 “확장” 요청을 보게 될 수 있습니다. 진행 중인 대화 한복판에서 예고 없이 발생하길 원하지 않을 것입니다. 따라서 이런 변경은:

  • 사전 공지(release notes, 문서);
  • 테스트용 AS가 있는 staging에서 검증;
  • 모델이 “위험한” 도구를 자각하고 호출하도록 도구 설명(destructiveHint 등)을 함께 업데이트;

6. 메타데이터와 애노테이션: 계약 위의 힌트 레이어

Auth 레이어가 “누가 무엇을 할 수 있는가”에 답한다면, 적절한 토큰과 scopes가 있더라도 모델이 도구를 어떻게 호출하고 사용자에게 어떻게 설명할지가 중요합니다. 여기서 추가 힌트 레이어인 메타데이터와 애노테이션이 등장합니다.

계약(스키마)은 도구가 무엇을 받고 무엇을 반환하는지 말합니다. 메타데이터와 애노테이션은 도구를 언제/어떻게 호출해야 할지 모델이 이해하도록 돕습니다. App을 진화시키면서 파괴적 행동을 추가하거나 UI를 바꾸고 외부 세계 통합을 도입할 때 특히 중요합니다.

_meta["openai/widgetDescription"]widgetCSP

Apps SDK와 MCP 설명에는 _meta라는 특수 필드가 있으며, OpenAI가 프로토콜 확장을 여기에 담습니다. 예:

  • _meta["openai/widgetDescription"] — 위젯이 무엇을 보여주는지에 대한 짧은 설명. 모델이 UI를 “다시 말하지” 않고 App을 올바르게 소개하는 데 도움;
  • _meta["openai/widgetCSP"] — 위젯에 필요한 CSP 도메인 선언(fetch/이미지/스크립트 등).

UI를 변경할 때(예: 주문 절차의 새 단계를 추가), widgetDescription을 업데이트해 모델이 사용자에게 계속 정확히 설명하도록 하는 것이 유용합니다.

도구 애노테이션(readOnlyHint, destructiveHint, openWorldHint)

애노테이션은 단순한 불리언 플래그지만 UX와 안전성에 크게 영향을 줍니다:

  • readOnlyHint: true — 도구가 아무것도 변경하지 않음(읽기). 모델이 불필요한 확인 없이 호출할 수 있습니다.
  • destructiveHint: true — 도구가 무언가를 삭제/변경할 수 있음. ChatGPT가 명시적 확인을 요청합니다.
  • openWorldHint: true — 도구가 데이터를 외부에 공개하거나 “아주 많은 것”을 반환할 수 있어 요약이 필요할 수 있음.

애노테이션이 포함된 도구 디스크립터 예:

server.registerTool({
  name: "delete_saved_gift",
  description: "사용자의 저장된 선물을 삭제합니다",
  inputSchema: z.object({ giftId: z.string() }),
  annotations: {
    readOnlyHint: false,
    destructiveHint: true,
    openWorldHint: false,
  },
  async execute({ giftId }) {
    // ...선물을 삭제합니다
  },
});

마이그레이션에서 새로운 “위험한” 도구를 추가할 때 애노테이션은 큰 도움이 됩니다. ChatGPT가 몰래 실행하지 않도록 하고 더 신중한 행동을 유도합니다.

애노테이션은 “진짜” 보안이 아니라는 점을 이해해야 합니다. 이는 클라이언트와 모델의 행동에만 영향을 줍니다. 진짜 보안은 여전히 서버(Auth, scopes, 검증)가 보장합니다.

7. SDK와 MCP 사양의 마이그레이션

MCP와 Apps SDK는 활발히 진화 중입니다. capabilities의 새 필드, 새로운 메시지 타입, 새로운 _meta/annotations가 등장합니다. 문서에도 “2025년 기준” 같은 주석이 정직하게 달려 있죠. 우리는 그 현실을 받아들여야 합니다.

따라서 SDK/사양 버전 마이그레이션은 App의 일상이고, “언젠가”의 드문 이벤트가 아닙니다.

전형적인 업그레이드 프로세스

건강한 업데이트 시나리오는 대략 다음과 같습니다:

  1. 새 Apps SDK/MCP SDK의 변경 로그를 읽고, 잠재적 깨뜨리는 변경을 표시합니다.
  2. 프로덕션은 건드리지 말고 dev/staging 환경에서 의존성을 업데이트합니다.
  3. MCP Inspector / Jam 또는 다른 클라이언트를 실행합니다:
    • 핸드셰이크 확인;
    • tools/list / resources/list 확인;
    • 몇 가지 테스트 tools/call.
  4. 새 기능에 맞춰 도구 설명과 _meta를 업데이트합니다:
    • 예를 들어 새로운 annotationswidgetDescription 추가.
  5. 앞선 강의에서 다룬 골든 케이스 및 LLM‑eval을 실행해 App의 품질 관점 동작이 나빠지지 않았음을 확인합니다.
  6. 그 후에만 프로덕션에 배포하고, 가능하면 카나리/피처 플래그로 일부 트래픽만 먼저 적용합니다.

예: 새 SDK에서 openWorldHint 추가

새 Apps SDK가 openWorldHint를 지원하고, 외부 리뷰를 긁어와 노이즈가 많을 수 있는 search_public_reviews 도구에 이를 표시하려고 한다고 합시다.

단계는 다음과 같습니다:

  • SDK와 타입을 업데이트;
  • 도구 디스크립터에 annotations.openWorldHint = true를 추가;
  • 시스템 프롬프트를 업데이트해 에이전트가 외부 세계 요청을 명시적으로 사용자에게 설명하도록 함;
  • 프라이버시/PII 관련 질문에 대한 세이프티 골든 케이스를 특히 돌려, 모델이 과도하게 말 많아지지 않았는지 확인.

이제 이 모든 것을 recommend_gifts 도구의 진화 시나리오 하나로 묶어 봅시다.

8. 미니 케이스: GiftGenius의 recommend_gifts 진화

구체적 시나리오로 함께 정리해 봅시다.

초기 버전

기본 도구는 다음과 같았습니다:

const recommendGiftsInput_v1 = z.object({
  occasion: z.string(),
  budgetUsd: z.number().int().positive(),
  recipientProfile: z.string(),
});

server.registerTool({
  name: "recommend_gifts",
  description: "USD 예산을 기준으로 선물 아이디어를 추천합니다",
  inputSchema: recommendGiftsInput_v1,
  async execute(args) {
    const input = recommendGiftsInput_v1.parse(args);
    return giftService.recommend(input); // 내부 함수
  },
});

미국 사용자와 단일 통화만 있을 때는 문제가 없습니다.

새 비즈니스 요구: 다중 통화와 마감일

프로덕트 팀이 다음 요구를 들고 왔습니다:

  • EUR/GBP 지원;
  • 배송 마감일 고려(생일까지 3일 남았는데 한 달 뒤 도착하는 선물은 제외);
  • 응답에 배송 예상 시간도 포함하면 좋음.

순진한 접근은 필드를 그냥 바꾸는 것입니다:

  • budgetUsdmaxPrice로 이름 변경;
  • currency 추가;
  • 응답에 deliveryEstimateDays 추가.

무슨 문제가 생길까요?

예전 프롬프트(골든 케이스와 system‑prompt의 설명 포함)와 저장된 대화는 budgetUsd를 계속 보냅니다. 모델은 그 필드가 사라졌다는 걸 모릅니다. MCP 레이어는 parse 시점에 실패하고, 실제 사용자에게서 ChatGPT App 동작이 갑자기 깨집니다.

올바른 방법:

  1. 새 스키마와 새 도구 _v2를 추가합니다.
const recommendGiftsInput_v2 = z.object({
  occasion: z.string(),
  maxPrice: z.number().int().positive(),
  currency: z.enum(["USD", "EUR", "GBP"]),
  recipientProfile: z.string(),
  deliverByDate: z.string().optional(),
});

server.registerTool({
  name: "recommend_gifts_v2",
  description:
    "통화와 원하는 배송 날짜를 고려한 선물 추천",
  inputSchema: recommendGiftsInput_v2,
  async execute(args) {
    const input = recommendGiftsInput_v2.parse(args);
    return giftService.recommendV2(input); // 새 로직
  },
});
  1. recommend_gifts는 그대로 두되, descriptionDEPRECATED 표시를 추가합니다.
  2. system‑prompt와 App 설명을 업데이트해 모델이 recommend_gifts_v2를 선호하도록 합니다(지시문에 명시할 수 있음).
  3. GiftGenius 위젯을 업데이트해 새 응답 포맷(deliveryEstimateDays 등)을 이해하도록 합니다.
  4. 대표 시나리오(특정 날짜까지 선물 추천)에 대한 골든 케이스를 LLM‑eval로 실행합니다.

테스트와 가시성

갖고 싶은 몇 가지 테스트:

새 입력에 대한 계약 테스트:

test("v2는 EUR와 마감일 시나리오를 수용한다", () => {
  const sample = {
    occasion: "birthday",
    maxPrice: 100,
    currency: "EUR",
    recipientProfile: "동료",
    deliverByDate: "2025-12-24",
  };

  expect(() => recommendGiftsInput_v2.parse(sample)).not.toThrow();
});

프로덕션에서의 관찰:

  • recommend_gifts_v2recommend_gifts 호출 비율;
  • v1의 오류율(증가하지 않는 것이 기대치);
  • 마이그레이션 전후 골든 케이스에 대한 LLM‑eval 점수(앞선 강의에 따라 이미 수행 방법을 알고 있을 것).

v2가 사용률과 품질에서 모두 “승리”하면, v1 비활성화를 계획적으로 진행할 수 있습니다.

세 문장으로 요약하면: (1) MCP는 두꺼운 새 모놀리스가 아니라 얇은 어댑터입니다; (2) 스키마, 인증, 애노테이션은 ChatGPT와 백엔드 사이의 장수 계약이며, 일반 API만큼이나 신중히 버전 관리하고 테스트해야 합니다; (3) SDK/사양 마이그레이션은 staging, 골든 케이스, 가시성을 갖춘 정상적인 엔지니어링 프로세스이며, “금요일 저녁 패키지 업데이트”가 아닙니다. 이 관점으로 ChatGPT App을 보면, 기존 제품과의 통합이 더 이상 혼란처럼 느껴지지 않을 것입니다.

9. MCP/SDK 통합과 마이그레이션의 전형적 실수

오류 №1: MCP를 얇은 어댑터가 아닌 “새 백엔드”로 취급.
가끔 MCP 레이어로 모든 비즈니스 로직(DB 접근, 도메인 규칙, 계산)을 끌어오고 싶어집니다. 이는 MCP 서버를 또 하나의 모놀리스로 만들어 나머지 백엔드와의 동기화가 어렵게 합니다. MCP는 기존 서비스 위의 Gateway/Adapter로 유지하는 것이 훨씬 건강합니다. 도메인 로직은 ChatGPT 도입 전과 같은 곳에 두고, MCP는 JSON을 서로 번역만 하세요.

오류 №2: 동일 객체에 서로 다른 스키마.
“선물”을 DB, REST API, MCP 도구에서 각각 조금씩 다르게 정의하는 안티패턴이 흔합니다. 결국 정적 타이핑, 계약, 테스트, 상식이 모두 무너집니다. 단일 스키마(Zod/TypeBox 등)를 Single Source of Truth로 쓰고 MCP용 JSON Schema를 생성하는 방식이 이 위험을 크게 줄입니다.

오류 №3: 스키마 마이그레이션을 잘못하여 “조용한” 깨뜨리는 변경 발생.
필드 이름 변경이나 의미 변경을 하면서 도구 이름을 그대로 두면, 모델은 예전 포맷을 계속 보내고 문제는 일부 사용자에게만, 그리고 한참 뒤에 드러납니다. 큰 변경 시에는 *_v2를 만들고, 예전 버전을 병행 운영하며, 사용 중단 표기와 모니터링을 활용하세요.

오류 №4: Auth 변경과 scopes를 무시.
부작용이 있는 새 도구를 추가했는데 scopes와 .well-known 업데이트를 잊었나요? 사용자는 시나리오 도중 401을 맞거나, 반대로 MCP가 적절한 인가 없이 파괴적 작업을 수행할 수 있습니다. 스키마 마이그레이션만큼이나 auth 레이어 마이그레이션도 staging, 테스트, 점진적 권한 확장으로 신중히 계획하세요.

오류 №5: 애노테이션(destructiveHint, readOnlyHint, openWorldHint)을 사용하지 않음.
모델에 어떤 도구가 안전하고 어떤 도구가 잠재적으로 위험한지 알려주지 않으면, 예상 못 한 행동을 보일 수 있습니다. 무해한 get_catalog에는 확인을 요구하면서, 데이터 삭제는 경고 없이 수행한다든지요. 올바른 애노테이션은 사용자 관점에서 예측 가능한 행동을 만들고 품질/보안 인시던트 위험을 줄입니다.

오류 №6: 골든 케이스 검증 없이 SDK를 “그대로” 프로덕션에 업데이트.
새 SDK/사양은 필드를 추가하거나 핸드셰이크 동작이나 메시지 구조를 바꿀 수 있습니다. 그냥 “의존성 업데이트 후 배포”를 하면, 모델이 필요한 도구 호출을 멈추거나 오류 문구가 바뀌는 등의 품질 회귀를 맞을 위험이 있습니다. 먼저 dev/staging에서 MCP Inspector로 점검하고, 골든 케이스와 LLM‑eval을 거친 다음 프로덕션에 반영하세요.

오류 №7: 비즈니스 로직을 특정 도구 버전에 강하게 결합.
내부 Gift Service 로직이 recommend_gifts에 직접 의존하면, recommend_gifts_v2로의 마이그레이션이 고통스러워집니다. 최선은 내부 서비스는 자체 규칙에 따라 진화하게 두고, *_v1, *_v2 도구는 얇은 어댑터로서 구/신 외부 계약을 공용 도메인 구조에 매핑하는 것입니다.

오류 №8: 도구 버전별 가시성 부족.
로그와 메트릭에서 어떤 도구와 어떤 버전이 호출되었는지 구분하지 못하면, 마이그레이션 디버깅은 점쟁이가 됩니다. 도구 이름, 스키마/SDK 버전, 핵심 파라미터를 로깅하세요. 그러면 어떤 회귀가 어떤 변경과 연결되는지 훨씬 쉽게 파악할 수 있습니다.

코멘트
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION