MCP 실전 구현
이 장에서 구현할 구성요소의 연결 관계는 다음과 같습니다.
MCP 클라이언트 │ Stdio 또는 Streamable HTTP ▼MCP 서버 ── 요청 스키마·인수 검증 ├── Tools 핸들러 ── 실제 작업 실행 └── Resources 핸들러 ── 읽기 전용 데이터 반환프로젝트 설정
섹션 제목: “프로젝트 설정”MCP 서버를 만들기 위해 공식 TypeScript SDK를 설치합니다.
npm init -ynpm install @modelcontextprotocol/sdk zodnpm install -D typescript @types/node tsxtsconfig.json 기본 설정:
{ "compilerOptions": { "target": "ES2022", "module": "Node16", "moduleResolution": "Node16", "outDir": "./dist", "strict": true }}기본 MCP 서버 구현
섹션 제목: “기본 MCP 서버 구현”파일 시스템 도구를 제공하는 간단한 MCP 서버입니다.
import { Server } from '@modelcontextprotocol/sdk/server/index.js';import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';import { CallToolRequestSchema, ListToolsRequestSchema,} from '@modelcontextprotocol/sdk/types.js';import { z } from 'zod';import { readFile, writeFile } from 'fs/promises';
const server = new Server( { name: 'file-system-server', version: '1.0.0' }, { capabilities: { tools: {} } });
// 도구 목록 핸들러server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: [ { name: 'read_file', description: '파일 내용을 읽습니다', inputSchema: { type: 'object', properties: { path: { type: 'string', description: '읽을 파일 경로' }, }, required: ['path'], }, }, { name: 'write_file', description: '파일에 내용을 씁니다', inputSchema: { type: 'object', properties: { path: { type: 'string', description: '쓸 파일 경로' }, content: { type: 'string', description: '파일 내용' }, }, required: ['path', 'content'], }, }, ],}));
// 도구 실행 핸들러const ReadFileParams = z.object({ path: z.string() });const WriteFileParams = z.object({ path: z.string(), content: z.string() });
server.setRequestHandler(CallToolRequestSchema, async (request) => { const { name, arguments: args } = request.params;
if (name === 'read_file') { const { path } = ReadFileParams.parse(args); const content = await readFile(path, 'utf-8'); return { content: [{ type: 'text', text: content }] }; }
if (name === 'write_file') { const { path, content } = WriteFileParams.parse(args); await writeFile(path, content, 'utf-8'); return { content: [{ type: 'text', text: `파일 저장 완료: ${path}` }] }; }
throw new Error(`Unknown tool: ${name}`);});
// Stdio 전송으로 서버 시작const transport = new StdioServerTransport();await server.connect(transport);Resources 구현
섹션 제목: “Resources 구현”Resources는 URI로 식별되는 읽기 전용 데이터를 노출합니다.
import { ListResourcesRequestSchema, ReadResourceRequestSchema,} from '@modelcontextprotocol/sdk/types.js';
const server = new Server( { name: 'data-server', version: '1.0.0' }, { capabilities: { tools: {}, resources: {} } });
server.setRequestHandler(ListResourcesRequestSchema, async () => ({ resources: [ { uri: 'config://app/settings', name: '애플리케이션 설정', mimeType: 'application/json', }, ],}));
server.setRequestHandler(ReadResourceRequestSchema, async (request) => { const { uri } = request.params;
if (uri === 'config://app/settings') { const settings = await loadAppSettings(); return { contents: [ { uri, mimeType: 'application/json', text: JSON.stringify(settings, null, 2), }, ], }; }
throw new Error(`Resource not found: ${uri}`);});HTTP 전송 설정
섹션 제목: “HTTP 전송 설정”원격 배포가 필요한 경우 최신 MCP 명세의 Streamable HTTP 전송을 사용합니다. 단일 /mcp 엔드포인트에서 클라이언트 요청을 처리하고, 서버가 필요할 때 SSE로 메시지를 스트리밍할 수 있습니다.
import { randomUUID } from 'node:crypto';import express from 'express';import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';import { isInitializeRequest } from '@modelcontextprotocol/sdk/types.js';
const app = express();app.use(express.json());
const transports = new Map<string, StreamableHTTPServerTransport>();
app.post('/mcp', async (req, res) => { const sessionId = req.headers['mcp-session-id'] as string | undefined; let transport = sessionId ? transports.get(sessionId) : undefined;
if (!transport && isInitializeRequest(req.body)) { transport = new StreamableHTTPServerTransport({ sessionIdGenerator: () => randomUUID(), onsessioninitialized: (id) => transports.set(id, transport!), });
transport.onclose = () => { if (transport?.sessionId) transports.delete(transport.sessionId); };
await server.connect(transport); }
if (!transport) { res.status(400).json({ error: 'Invalid or missing MCP session' }); return; }
await transport.handleRequest(req, res, req.body);});
app.listen(3000, () => { console.log('MCP 서버 실행 중: http://localhost:3000');});인증: Resource Indicators
섹션 제목: “인증: Resource Indicators”원격 MCP 서버에서 인증은 RFC 8707 Resource Indicators를 활용한 OAuth 2.0 방식을 사용합니다.
import { createRemoteJWKSet, jwtVerify } from 'jose';
const jwksUri = 'https://auth.example.com/.well-known/jwks.json';const JWKS = createRemoteJWKSet(new URL(jwksUri));
async function verifyToken(token: string): Promise<TokenPayload> { const { payload } = await jwtVerify(token, JWKS, { issuer: 'https://auth.example.com', audience: 'https://mcp.example.com', // Resource Indicator }); return payload as TokenPayload;}
// Express 미들웨어로 적용function authMiddleware( req: express.Request, res: express.Response, next: express.NextFunction): void { const authHeader = req.headers.authorization; if (!authHeader?.startsWith('Bearer ')) { res.status(401).json({ error: 'Authorization required' }); return; }
const token = authHeader.slice(7); verifyToken(token) .then((payload) => { req.user = payload; next(); }) .catch(() => { res.status(403).json({ error: 'Invalid token' }); });}
app.use('/mcp', authMiddleware);MCP 클라이언트 구현
섹션 제목: “MCP 클라이언트 구현”서버를 만들었다면 클라이언트에서 연결하는 코드입니다.
import { Client } from '@modelcontextprotocol/sdk/client/index.js';import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
const transport = new StdioClientTransport({ command: 'node', args: ['dist/server.js'],});
const client = new Client( { name: 'my-agent', version: '1.0.0' }, { capabilities: {} });
await client.connect(transport);
// 사용 가능한 도구 목록 조회const { tools } = await client.listTools();console.log('사용 가능한 도구:', tools.map((t) => t.name));
// 도구 호출const result = await client.callTool({ name: 'read_file', arguments: { path: './package.json' },});console.log(result.content);
await client.close();MCP 서버 구현은 SDK의 Server 클래스에 ListTools와 CallTool 핸들러를 등록하는 것이 핵심입니다. 로컬 환경은 Stdio, 원격 배포는 Streamable HTTP 전송을 사용하며, OAuth 2.0 Resource Indicators로 인증을 구현합니다. Zod로 파라미터를 검증하면 LLM의 잘못된 인수 전달을 안전하게 처리할 수 있습니다.