Files
2nd/10_Wiki/Topics/Coding/AI_MCP_Server_Building.md
T
2026-05-09 21:08:02 +09:00

8.2 KiB

id, title, category, status, source_trust_level, verification_status, created_at, updated_at, tags, tech_stack, applied_in, aliases
id title category status source_trust_level verification_status created_at updated_at tags tech_stack applied_in aliases
ai-mcp-server-building MCP Server 작성 — 도구 + 권한 + 배포 Coding draft B conceptual 2026-05-09 2026-05-09
ai
mcp
tool
vibe-coding
language applicable_to
TS / MCP SDK
Backend
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)

// 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+)

// 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 보안)

// /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)

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)

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 / 안전

// 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

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

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)

npx @modelcontextprotocol/inspector node server.js
# UI 에서 tool 호출 / 결과 확인

배포

Local:        config 에 command path
Cloud HTTP:   docker + behind LB + OAuth
Distribution: npm package
// 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.

🔗 관련 문서