MCP 실전 구현
프로젝트 설정
섹션 제목: “프로젝트 설정”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 전송 설정”원격 배포가 필요한 경우 HTTP + SSE 전송을 사용합니다.
import express from 'express';import { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js';
const app = express();app.use(express.json());
const transports = new Map<string, SSEServerTransport>();
// SSE 연결 엔드포인트app.get('/sse', async (req, res) => { const transport = new SSEServerTransport('/messages', res); transports.set(transport.sessionId, transport);
res.on('close', () => { transports.delete(transport.sessionId); });
await server.connect(transport);});
// 클라이언트 메시지 수신app.post('/messages', async (req, res) => { const sessionId = req.query.sessionId as string; const transport = transports.get(sessionId);
if (!transport) { res.status(404).json({ error: 'Session not found' }); return; }
await transport.handlePostMessage(req, res);});
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('/sse', authMiddleware);app.use('/messages', 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, 원격 배포는 HTTP+SSE 전송을 사용하며, OAuth 2.0 Resource Indicators로 인증을 구현합니다. Zod로 파라미터를 검증하면 LLM의 잘못된 인수 전달을 안전하게 처리할 수 있습니다.