[DIP] MCP

ai

프론트엔드 개발을 하며 최근 들어 사용하는 AI 종류들도 다양해지고 그만큼 AI의 발전 속도를 피부로 체감하고 있다. 점점 AI에 의지하는 일들이 많아지는 것을 느끼며 실제 업무에서도 사용하지 않는 경우 불편함을 느끼는 단계까지 온 것 같다. 그 와중 주변 개발자 친구 중 AI를 사용만 하는 것이 아닌 AI에 대해 좀 더 깊이 이해하고 학습하는 것을 보며 나 또한 개발자로서 AI를 사용자로서만이 아닌 개발자의 시각으로 바라볼 필요가 있음을 느꼈다.

이 글을 쓰는 시점에는 이미 예전에 핫한 주제가 되었을지 모르지만, 개발자 간 대화에서 MCP! MCP! 하며 이야기하던 때가 있었다. 이때 나는 정작 MCP에 대해 정확하게 어떤 구조로 어떻게 동작하는지 잘 알지 못한 채 이야기하는 내 모습이 유투브를 보고 전문가 행세를 하는 모습 같다고 느꼈다. 전문가까지는 어렵겠지만, MCP에 대해 간단하면서도 어쩌면 조금은 깊게 정리해 보고자 한다.

MCP-Model-Context-Protocol-3.png



MCP란

MCP(Model Context Protocol)는 LLM(Large Language Model)이 애플리케이션과 상호작용을 할 때, 그 모델에게 제공되는 컨텍스트(context)의 구조화된 관리 및 통신 방법을 표준화한 프로토콜이다.

OpenAI에서 제안된 새로운 구조적 API 접근 방식으로 프롬프트로의 정보 주입 방식에서 더 명시적이고 계층적인 컨텍스트 제공을 목표로 한다.



MCP 주요 목적

MCP는 LLM이 다양한 외부 리소스와 툴 그리고 구조화된 방식으로 상호작용 할 수 있도록 지원하여 프롬프트 기반 제어의 한계를 극복하는 걸 목표로 두고 있다고 한다. 이는 LLM이 외부 API나 함수를 호출할 수 있도록 명시적인 인터페이스를 제공하고 다양한 작업의 자동화가 가능하게 한다.

대화를 세션(Thread) 단위로 컨텍스트를 관리하여 모델이 이전 메세지와 사용자 설정을 보다 정확히 반영하여 응답할 수 있도록 한다. 그리고 어떤 정보가 모델에 제공되는지 제어할 수 있으며 이를 명확히 기록하고 추적할 수 있어 디버깅 및 보안 검증에 유리하다.



MCP 핵심 개념

요소설명
Thread대화 세션의 단위. 하나의 사용자의 흐름을 추적하는 고유한 컨텍스트 저장소
MessagesThread 안에 포함된 메시지. 사용자의 입력 또는 모델의 응답 등 자연어 기반 메시지를 의미
Tools모델이 호출할 수 있는 외부 기능(API), 예: 검색, 코드 실행 등
Data Context모델에게 제공될 수 있는 구조화된 데이터. 예: 사용자 프로필, 최근 활동 등
Instructions모델에게 준수하게 할 지침이나 행동 가이드라인. 예: “고객 응대 스타일로 말하기”


MCP 아키텍처

요소설명
MCP 호스트MCP 클라이언트를 포함하여 MCP 서버와 통신하는 애플리케이션Claude Desktop, IDE, AI 도구
MCP 클라이언트MCP 호스트 내에서 MCP 서버와 연결을 유지하며 통신을 담당
MCP 서버MCP 프로토콜을 통해 특정 기능을 노출하는 프로그램Tools, Resources, Prompts
로컬 데이터 소스MCP 서버가 안전하게 접근 가능한 컴퓨터 파일, 데이터베이스 및 서비스
원격 서비스MCP 서버가 연결 할 수 있는 외부 시스템API

MCP-Model-Context-Protocol-1.png



MCP-Model-Context-Protocol-2.png

  • Ref: claude-4-sonnet


MCP Typescript SDK

Github - modelcontextprotocol/typescript-sdk

위 MCP에 대한 정리 내용들은 Model Context Protocel의 Introduction을 참고하여 정리한 내용이다. 내용 중 MCP 아키텍처를 기반으로 Typescript SDK를 분석하여 각 구성요소에 대한 실제 코드상 구현을 분석해 볼 예정이다. 궁금한 내용이지만 잘 모르는 영역에 대한 조금의 설렘과 두려움을 안고, 새로운 숲으로 모험을 떠나는 마음으로 천천히 나아가 보려 한다.

  • MCP는 C#, JAVA, Kotiln, Python, Ruby, Swift, Typescript SDK를 제공한다.

*
typescript-sdk v1.13.3 버전 기준으로 정리되었음을 참고해주세요.
  • MCP 호스트
  • MCP 클라이언트
  • MCP 서버


MCP 호스트

MCP에서 각 클라이언트와 서버를 실행하고 관리하는 역할을 한다. CLI로 실행되는 arguments 기준으로 client 실행 시 runClient를 실행하고 server는 runServer를 실행한다.

MCP 호스트는 중앙 관리자 역할로 엔트리포인트 역할과 Transport 계층을 선택하고 Express 서버 설정을 담당한다.

src/cli.ts

tsx
async function runClient(url_or_command: string, args: string[]) { // 클라이언트 인스턴스 생성 및 연결 관리 } async function runServer(port: number | null) { // 서버 인스턴스 생성 및 Express 앱 설정 } // 명령어 라우팅 (client/server 모드 결정) const command = args[0]; switch (command) { case "client": runClient(...); break; case "server": runServer(...); break; }

MCP 호스트 역할은 각 client, server 로직에서도 일부 수행되며 client에서의 Transport 계층 추상화, server에서 프로토콜 초기화 관리 등이 수행된다.

src/cli.ts

tsx
async function runClient(url_or_command: string, args: string[]) { ... // protocol에 따른 Transport 선택 if (url?.protocol === "http:" || url?.protocol === "https:") { clientTransport = new SSEClientTransport(new URL(url_or_command)); } else if (url?.protocol === "ws:" || url?.protocol === "wss:") { clientTransport = new WebSocketClientTransport(new URL(url_or_command)); } else { clientTransport = new StdioClientTransport({...}); } ... }

src/server/index.ts

tsx
export class Server< RequestT extends Request = Request, NotificationT extends Notification = Notification, ResultT extends Result = Result, > extends Protocol< ServerRequest | RequestT, ServerNotification | NotificationT, ServerResult | ResultT > { ... // 서버 초기화 요청 처리 this.setRequestHandler(InitializeRequestSchema, (request) => // 프로토콜 초기화 this._oninitialize(request), ); ... }

MCP 호스트 영역?

MCP 호스트 영역을 정리 중 어디까지를 호스트 영역으로 보아야 할지가 조금 애매하게 느껴졌다. 단순히 src/cli.ts 작성된 코드 기준으로 클라이언트와 서버를 실행하는 부분만 호스트로 보아야 할지 클라이언트와 서버에 구현된 초기화 로직까지 포함해야 할지가 고민되었다.

이 부분은 AI 도움을 받아 답을 얻을 수 있는데, AI의 답변은 MCP 호스트란 실제 데이터를 처리하는 서버가 아닌 프로토콜을 관리하고 클라이언트와 서버 통신을 중재하는 역할을 의미한다고 한다. 그래서 MCP 호스트 영역은 클라이언트와 서버를 실행하는 부분뿐만이 아닌 인스턴스를 초기화하며 프로토콜과 라우팅을 관리하는 영역까지 포함한다.



MCP 클라이언트

MCP 클라이언트는 MCP 서버의 기능을 쉽게 활용할 수 있도록 타입 안전한 인터페이스를 제공하며, 요청을 송신하고 응답을 수신하는 임무를 수행한다.

인스턴스 초기화 후 자동으로 서버와의 초기화 플로우를 수행하고, 서버의 능력(capabilities)을 검증하여 지원되는 기능만 호출할 수 있도록 보장한다. 또한 Zod 스키마 기반의 요청/응답 검증과 도구 출력 스키마 캐싱을 통해 런타임 안전성을 제공한다. MCP 클라이언트의 역할 수행 및 기능들을 하나씩 정리해 보도록 하겠다.


다양한 통신 방식 지원

처음 MCP 호스트에서 클라이언트를 초기화하며 protocol을 확인한 후 적절한 Transport를 connect 과정에 설정하여 통신에 사용되도록 한다. 해당 사항은 MCP 호스트의 역할로서 설명했던 부분이고 MCP 클라이언트에서는 설정된 Transport를 사용하여 요청을 송신하고 응답을 수신하는 역할을 수행한다.

Transport는 상위 Class Protorol에서 통신과정에 사용되도록 설정된다.

src/shared/protocol.ts

tsx
export abstract class Protocol< SendRequestT extends Request, SendNotificationT extends Notification, SendResultT extends Result, > { private _transport?: Transport; ... async connect(transport: Transport): Promise<void> { this._transport = transport; const _onclose = this.transport?.onclose; this._transport.onclose = () => { }; ... this._transport.onerror = (error: Error) => { }; const _onmessage = this._transport?.onmessage; this._transport.onmessage = (message, extra) => { ... }; await this._transport.start(); } ... }

안전한 타입 지원

타입 지원은 MCP 클라이언트와 서버 모두 zod를 통해 정의된 스키마를 기반으로 컴파일 타임과 런타임에 타입 검증하여 타입 안정성을 보장하고 있다.

types.ts에는 많은 스키마들이 타입 정의되어 있고 클라이언트 요청, 서버 응답 과정에 사용되고 있다.

src/types.ts

tsx
import { z, ZodTypeAny } from "zod"; ... export const ResultSchema = z .object({ /** * See [MCP specification](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/47339c03c143bb4ec01a26e721a1b8fe66634ebe/docs/specification/draft/basic/index.mdx#general-fields) * for notes on _meta usage. */ _meta: z.optional(z.object({}).passthrough()), }) .passthrough(); ... }

인증 및 보안 지원

MCP 클라이언트는 내부 구현된 OAuth 기반으로 안전한 인증을 제공한다. 사용 설정된 Transport에서 OAuth를 사용하여 인증 시도 연결하여 인증 상태를 관리한다. 추가로 pkce-challenge 라이브러리를 사용하여 OAuth 플로우의 보안 계층을 강화하고 있다.

src/client/auth.ts

tsx
export interface OAuthClientProvider { ... tokens(): OAuthTokens | undefined | Promise<OAuthTokens | undefined>; ... }

src/shared/auth.ts

tsx
/** * OAuth 2.1 token response */ export const OAuthTokensSchema = z .object({ access_token: z.string(), token_type: z.string(), expires_in: z.number().optional(), scope: z.string().optional(), refresh_token: z.string().optional(), }) .strip();

에러 처리 및 복구 지원

MCP 클라이언트에서는 타입별로 에러를 분류하여 처리하고 자동 복구 시스템을 제공하여 안정적인 서비스가 제공되도록 지원한다.

src/types.ts

tsx
export enum ErrorCode { // SDK error codes ConnectionClosed = -32000, RequestTimeout = -32001, // Standard JSON-RPC error codes ParseError = -32700, InvalidRequest = -32600, MethodNotFound = -32601, InvalidParams = -32602, InternalError = -32603, }

여러 에러 처리 중 몇 가지를 정리해 보자면 기본적으로 서버의 능력(Capability)을 검증하여 에러를 방지하고 스키마 기반 타입 검증을 통해 사전에 에러를 방지한다. 그리고 인증 에러 발생 시 OAuth 토큰 자동 갱신과 요청 재시도 전략 및 타임아웃 처리를 하고 세션 복구를 위한 재연결 시도 로직을 포함하고 있다.

src/client/streamableHttp.ts

tsx
/** * Schedule a reconnection attempt with exponential backoff * * @param lastEventId The ID of the last received event for resumability * @param attemptCount Current reconnection attempt count for this specific stream */ private _scheduleReconnection(options: StartSSEOptions, attemptCount = 0): void { // Use provided options or default options const maxRetries = this._reconnectionOptions.maxRetries; // Check if we've exceeded maximum retry attempts if (maxRetries > 0 && attemptCount >= maxRetries) { this.onerror?.(new Error(`Maximum reconnection attempts (${maxRetries}) exceeded.`)); return; } // Calculate next delay based on current attempt count const delay = this._getNextReconnectionDelay(attemptCount); // Schedule the reconnection setTimeout(() => { // Use the last event ID to resume where we left off this._startOrAuthSse(options).catch(error => { this.onerror?.(new Error(`Failed to reconnect SSE stream: ${error instanceof Error ? error.message : String(error)}`)); // Schedule another attempt if this one failed, incrementing the attempt counter this._scheduleReconnection(options, attemptCount + 1); }); }, delay); }

MCP 클라이언트의 역할?

MCP 클라이언트 역할을 정리하며 기존 서비스 개발을 하며 챙기지 않았던 부분들에 대해 돌아볼 수 있었다. 에러 처리에서의 타임아웃이나 재시도 처리, 타입 검증과 같은 부분들은 챙기고 있지만 인증과 보안적인 부분은 “서버에서 인증 처리를 잘해주겠지..” “FE에서 보안 챙길게 딱히…” 라는 생각이 마음 한곳에 자리 잡고 어쩌면 소홀하진 않았나 싶다. 그리고 세션 복구나 재연결 시도를 보며 사용자 서비스 사용 동선이 에러 혹은 끊기는 상황에서 단순히 에러 페이지로 보내고 ‘홈으로 이동’과 같은 버튼을 붙이는 것이 아니라 다른 고민도 해보면 좋겠다는 생각이 들었다.

MCP 클라이언트는 어떻게 보면 FE 개발자가 담당하는 영역으로 느껴졌고, 이 안에서의 고민을 통해 작성된 로직들이 서비스에서 놓치고 있었던 부분을 다른 시각으로 바라볼 수 있는 계기가 되어 좋았다.



MCP 서버

MCP 서버는 클라이언트의 요청을 받아 처리하고 응답해 주는 역할을 한다. MCP 서버의 역할 중 일부는 MCP 클라이언트와 동일한 처리를 수행한다. 선택된 Transport에 따른 응답 처리와 능력(capability)를 검증하고 제어한다. 그리고 인증 및 보안 처리와 스키마 검증, 에러 처리 및 복구 지원을 통한 안정성을 높이는 작업 또한 동일하게 수행한다.

MCP 서버에서만 제공되는 기능으로는 Tools와 Resources, Prompts 기능을 제공하고 각 기능에 대한 목록 변경 알림과 로그 메세지 전송 기능이 있다.

*
MCP 클라이언트에 설명된 처리는 대부분 비슷한 로직과 타입 처리로 되어있어 제외하였다.


Tools

Tools는 서버에서 클라이언트로 제공하는 실행 가능한 기능이다. 서버에서 등록 Tools는 클라이언트에서 tools/list로 조회 및 tools/call 요청으로 실행 가능하다.

tsx
const client = new Client({ name: "test-client", version: "1.0.0", }); // List tools to cache the schemas await client.listTools(); // Call the tool - should validate successfully const result = await client.callTool({ name: "test-tool" });

처음 Tools를 봤을 때 그래서 어떤 기능을 제공한다는 걸까?라는 생각이 들었지만 MCP 프로젝트내 예시 코드를 보고 이해할 수 있었다. 아래는 날씨에 대한 정보 제공을 위해 Tools를 등록하는 예제 코드이다.

src/examples/server/mcpServerOutputSchema.ts

tsx
// Define a tool with structured output - Weather data server.registerTool( "get_weather", { description: "Get weather information for a city", inputSchema: { city: z.string().describe("City name"), country: z.string().describe("Country code (e.g., US, UK)") }, outputSchema: { temperature: z.object({ celsius: z.number(), fahrenheit: z.number() }), conditions: z.enum(["sunny", "cloudy", "rainy", "stormy", "snowy"]), humidity: z.number().min(0).max(100), wind: z.object({ speed_kmh: z.number(), direction: z.string() }) }, }, async ({ city, country }) => { ... const structuredContent = { temperature: ..., conditions: ..., humidity: ..., wind: ... }; return { content: [{ type: "text", text: JSON.stringify(structuredContent, null, 2) }], structuredContent }; } );

Resource

Resource는 MCP 서버에서 관리하는 데이터로 파일, DB, API 등 정보를 LLM에게 제공한다. Resource는 정적 데이터와 동적 데이터로 나누어진다. MCP 서버에서 resource 메서드를 통해 등록된 데이터는 MCP 클라이언트에서 resources/list 요청으로 목록 조회 resources/read 요청으로 데이터를 읽는다.

tsx
const server = new McpServer( { name: "test-server", version: "1.0.0" }, { capabilities: {} } ); // Register dynamic resource with title using registerResource server.registerResource( "user-profile", new ResourceTemplate("users://{userId}/profile", { list: undefined }), { title: "User Profile", description: "User profile information", }, async (uri, { userId }, _extra) => ({ contents: [ { uri: uri.href, text: `Profile data for user ${userId}`, }, ], }) ); const client = new Client({ name: "test-client", version: "1.0.0" }); const readResult = await client.readResource({ uri: "users://123/profile" });

Resource가 제공하는 데이터는 텍스트부터 바이너리, JSON 리소스와 HTML 그리고 이미지로 다양하다. 그리고 동적으로 변하는 데이터 정보나 DB 정보를 쿼리로 조회하여 제공하는 것도 가능하다.


Prompts

Prompts는 MCP 서버를 통해 제공되는 템플릿화된 메세지로 LLM에 제공되는 메세지를 규격화하여 요청에 따른 응답의 정확성을 높인다. MCP 서버에서 다른 기능들처럼 Prompots 등록 메서드를 사용하여 등록 가능하며 클라이언트에서는 prompts/get 요청으로 데이터를 얻는다.

tsx
server.registerPrompt( "greeting-template", { title: "Greeting Template", // Display name for UI description: "A simple greeting prompt template", argsSchema: { name: z.string().describe("Name to include in greeting"), }, }, async ({ name }): Promise<GetPromptResult> => { return { messages: [ { role: "user", content: { type: "text", text: `Please greet ${name} in a friendly manner.`, }, }, ], }; } ); const promptRequest: GetPromptRequest = { method: "prompts/get", params: { name, arguments: args as Record<string, string>, }, }; const promptResult = await client.request(promptRequest, GetPromptResultSchema);

MCP 서버의 기능들

MCP 서버의 기능들을 정리하며 문득 FE 개발자 입장에서 서버 구현이 이렇다면 FE 개발은 어떻게 하면 좋을지가 고민되었다.

앞선 고민과 함께 실제 서비스에서 FE 개발 진행 중 서버에서 정의해준 API를 확인 후 작업하는 나의 모습이 떠올랐다. 그리고 서버 API 구현 사항들은 흐릿하게 보이며 서버분들이 서비스 구조와 흐름을 설명해 주셨던 기억이 생각났다. 만약 지금과 같이 간단하게라도 서비스 내 서버 API 구현 혹은 아키텍처를 분석해 본다면 더 나은 개발을 진행할 수 있지 않을까 하는 생각이 든다.



In Conclusion

간단하게 MCP의 typescript-sdk를 분석해 보았다. 아직도 완전히 파악하지 못했지만 분석하면서도 정말 MCP에 대해 아무것도 모르고 있었구나라는 생각과 아쉬움이 뒤따랐다.

분석을 AI의 도움을 많이 받으며 MCP에 대해 조금씩 알아가는 과정 자체는 좋았지만, AI가 공부하는 것인지 내가 공부를 하는것인지 헷갈리기도 했다. 전문가가 되려면 남에게 설명할 수 있을 정도가 되었을 때를 기준으로 생각하기도 했는데, 전문가가 되더라도 AI가 있다면 전문가라는 것도 경계가 희미해지고 AI를 전문가로 활용하는 사용자만 남을 것 같다는 생각이 들었다.

아쉬움은 최근 사내 해커톤에서 AI를 활용한 Vibe Coding을 진행하였는데 조금 더 일찍 간단히라도 MCP 아키텍처를 파악했다면 더 좋은 결과물을 만들 수 있지 않았을까 하는 아쉬움이 남았다.

추가로 요즘 AI를 사용하며 최근 AI가 급속도로 발전하고 있지만 아직 디테일한 부분들이 아쉬웠고 아직까지는 앞선 아쉬움과 같이 AI 활용에 있어 전문가적인 시각이 필요하다는 생각이 들었다. 지금 하는 생각 또한 AI 발전 속도를 생각한다면 찰나에 불과하지 않을까 하는 생각이 들며 1년 뒤 이 글을 보았을 때 어떨지 기대가 되기도 한다.

MCP-Model-Context-Protocol-4.png

”I'm not afraid of storms, for I'm learning how to sail my ship.” - Louisa May Alcott -