--- id: ai-mcp-server-building title: MCP Server 작성 — 도구 + 권한 + 배포 category: Coding status: draft source_trust_level: B verification_status: conceptual created_at: 2026-05-09 updated_at: 2026-05-09 tags: [ai, mcp, tool, vibe-coding] tech_stack: { language: "TS / MCP SDK", applicable_to: ["Backend"] } applied_in: [] aliases: [MCP server, tool design, sampling, OAuth MCP, Streamable HTTP] --- # MCP Server Building > Tool wrap + 권한 + 배포 = MCP server. **Stdio (로컬) / Streamable HTTP (cloud, 2024+)**. 좋은 tool descrip + JSON Schema + 안전. ## 📖 핵심 개념 - Server: tools / resources / prompts 노출. - Client: Claude Desktop / Cursor / Cline. - Transport: stdio / SSE / Streamable HTTP. - Sampling: server 가 client 의 LLM 사용 가능. ## 💻 코드 패턴 ### 기본 server (TS, stdio) ```ts // server.ts 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'; const server = new Server({ name: 'acme-tools', version: '1.0.0' }, { capabilities: { tools: {}, resources: {}, prompts: {} }, }); // Tools 정의 const tools = { search_orders: { schema: z.object({ email: z.string().email() }), handler: async (input: { email: string }) => { const orders = await db.orders.findByEmail(input.email); return JSON.stringify(orders.slice(0, 10)); }, description: 'Search recent orders by customer email. Returns up to 10 orders.', }, get_inventory: { schema: z.object({ productId: z.string() }), handler: async ({ productId }: { productId: string }) => { const inv = await db.inventory.find(productId); return JSON.stringify(inv); }, description: 'Get current inventory for a product.', }, }; server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: Object.entries(tools).map(([name, t]) => ({ name, description: t.description, inputSchema: zodToJsonSchema(t.schema), })), })); server.setRequestHandler(CallToolRequestSchema, async (req) => { const tool = tools[req.params.name as keyof typeof tools]; if (!tool) { return { content: [{ type: 'text', text: `Unknown tool: ${req.params.name}` }], isError: true }; } const parsed = tool.schema.safeParse(req.params.arguments); if (!parsed.success) { return { content: [{ type: 'text', text: `Invalid input: ${parsed.error.message}` }], isError: true }; } try { const result = await tool.handler(parsed.data as any); return { content: [{ type: 'text', text: result }] }; } catch (e) { return { content: [{ type: 'text', text: `Error: ${(e as Error).message}` }], isError: true }; } }); const transport = new StdioServerTransport(); await server.connect(transport); ``` ### Tool description (LLM 이 잘 고르도록) ``` Bad: "Get user data" Good: "Get a user's profile by ID. Returns name, email, plan, created_at. Use this when you need user details. Returns 404 if user not found." ``` → LLM 이 언제 사용할지 명시. ### Streamable HTTP transport (2024+) ```ts // stream + persistent connection import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js'; const app = express(); app.use(express.json()); app.post('/mcp', async (req, res) => { const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: () => crypto.randomUUID(), }); await server.connect(transport); await transport.handleRequest(req, res, req.body); }); app.listen(3000); ``` → Cloud-deployed multi-user MCP. ### OAuth (cloud server 보안) ```ts // /authorize, /token endpoints // 사용자가 ChatGPT / Claude.ai 에서 OAuth flow // Server 가 user-scoped data 반환 app.use('/mcp', requireOAuthToken, async (req, res) => { const userId = await verifyToken(req.headers.authorization); // userId scope 으로 server 호출 }); ``` ### Resources (data exposure) ```ts import { ListResourcesRequestSchema, ReadResourceRequestSchema } from '@modelcontextprotocol/sdk/types.js'; server.setRequestHandler(ListResourcesRequestSchema, async () => ({ resources: [ { uri: 'acme://users/active', name: 'Active users', mimeType: 'application/json' }, { uri: 'acme://docs/api', name: 'API docs', mimeType: 'text/markdown' }, ], })); server.setRequestHandler(ReadResourceRequestSchema, async (req) => { if (req.params.uri === 'acme://users/active') { const users = await db.users.findActive(); return { contents: [{ uri: req.params.uri, mimeType: 'application/json', text: JSON.stringify(users), }], }; } throw new Error('Not found'); }); ``` ### Prompts (재사용 template) ```ts server.setRequestHandler(ListPromptsRequestSchema, async () => ({ prompts: [{ name: 'analyze-customer', description: 'Analyze a customer\'s purchase history', arguments: [{ name: 'email', required: true }], }], })); server.setRequestHandler(GetPromptRequestSchema, async (req) => { if (req.params.name === 'analyze-customer') { return { messages: [{ role: 'user', content: { type: 'text', text: `Analyze ${req.params.arguments?.email}'s orders. Look for patterns, churn risk, upsell opportunities.`, }, }], }; } }); ``` ### Permissions / 안전 ```ts // Sensitive 작업 = 명시 confirm server.setRequestHandler(CallToolRequestSchema, async (req) => { const dangerous = ['delete_user', 'send_email', 'charge_card']; if (dangerous.includes(req.params.name)) { // MCP 가 client 에 confirm 요청 (capability 가 elicitation) const ok = await server.request({ method: 'elicitation/create', params: { message: `Confirm: ${req.params.name} on ${req.params.arguments}`, }, }); if (!ok.confirmed) return { content: [...], isError: true }; } // ... }); ``` ### Logging / observability ```ts async function callTool(name: string, input: any) { const t = Date.now(); try { const result = await tools[name].handler(input); log.info({ tool: name, ms: Date.now() - t, success: true }); return result; } catch (e) { log.error({ tool: name, ms: Date.now() - t, error: e }); throw e; } } ``` ### Test ```ts import { Client } from '@modelcontextprotocol/sdk/client/index.js'; import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js'; const transport = new StdioClientTransport({ command: 'node', args: ['server.js'] }); const client = new Client({ name: 'test', version: '1.0' }, { capabilities: {} }); await client.connect(transport); const tools = await client.listTools(); const result = await client.callTool({ name: 'search_orders', arguments: { email: 'a@b.com' } }); expect(result.content).toBeDefined(); ``` ### Inspector (debug) ```bash npx @modelcontextprotocol/inspector node server.js # UI 에서 tool 호출 / 결과 확인 ``` ### 배포 ``` Local: config 에 command path Cloud HTTP: docker + behind LB + OAuth Distribution: npm package ``` ```jsonc // claude_desktop_config.json { "mcpServers": { "acme": { "command": "node", "args": ["/path/to/server.js"], "env": { "DB_URL": "..." } } } } ``` ## 🤔 의사결정 기준 | 상황 | 추천 | |---|---| | 로컬 단일 user | stdio | | Multi-user cloud | Streamable HTTP + OAuth | | 회사 내부 도구 | Stdio + 사내 npm publish | | Public service | Streamable HTTP + OAuth + rate limit | | Sensitive | Confirmation + audit | | Quick prototype | stdio + inspector | ## ❌ 안티패턴 - **Tool description 빈약**: LLM 이 못 고름. - **Sensitive 자동 실행**: HITL. - **PII raw response**: 마스킹. - **Schema 자주 변경**: 등록된 client 깨짐. - **HTTP 무 OAuth prod**: 누구나 호출. - **Sync long task**: timeout. async + status tool. - **Tool 너무 많음 (50+)**: LLM 혼란. group / namespace. ## 🤖 LLM 활용 힌트 - Tool descrip = LLM prompt — 풍부 + 명확. - Zod schema → JSON Schema 자동. - Sensitive = elicitation. - HTTP cloud = OAuth. ## 🔗 관련 문서 - [[AI_MCP_Integration_Patterns]] - [[AI_Function_Calling_Deep]] - [[AI_Agentic_Patterns]]