MCP 서버 개발

MCP 서버는 AI 클라이언트에게 도구, 리소스, 프롬프트를 제공하는 서비스입니다. Python과 TypeScript SDK를 사용하여 강력하고 재사용 가능한 MCP 서버를 개발하는 방법을 학습합니다.

업데이트 안내: 모델/요금/버전/정책 등 시점에 민감한 정보는 변동될 수 있습니다. 최신 내용은 공식 문서를 확인하세요.
빠른 시작
  • Python: pip install mcp 또는 uv add mcp
  • TypeScript: npm install @modelcontextprotocol/sdk
  • 서버는 Tools, Resources, Prompts 제공 가능
  • stdio 또는 HTTP SSE 전송 지원

Python 서버 개발

설치

Bash
# pip 사용
pip install mcp

# uv 사용 (권장)
uv pip install mcp

# 또는 프로젝트 의존성에 추가
uv add mcp

기본 서버 구조

Python
#!/usr/bin/env python3
"""
간단한 MCP 서버 예제
"""

from mcp.server import Server
from mcp.server.stdio import stdio_server
from mcp.types import Tool, TextContent

# 서버 인스턴스 생성
server = Server("my-mcp-server")

# 도구 등록
@server.tool()
async def hello(name: str = "World") -> str:
    """
    인사 메시지를 반환합니다.

    Args:
        name: 인사할 이름

    Returns:
        인사 메시지
    """
    return f"Hello, {name}!"

# 메인 함수
async def main():
    # stdio 전송으로 서버 실행
    async with stdio_server() as (read_stream, write_stream):
        await server.run(
            read_stream,
            write_stream,
            server.create_initialization_options()
        )

if __name__ == "__main__":
    import asyncio
    asyncio.run(main())

도구 (Tools) 구현

1. 간단한 도구

Python
@server.tool()
async def add(a: float, b: float) -> float:
    """두 숫자를 더합니다."""
    return a + b

@server.tool()
async def multiply(a: float, b: float) -> float:
    """두 숫자를 곱합니다."""
    return a * b

2. 복잡한 입력/출력

Python
from typing import List, Dict, Any
from mcp.types import TextContent, ImageContent, EmbeddedResource

@server.tool()
async def search_files(
    directory: str,
    pattern: str,
    max_results: int = 10
) -> List[Dict[str, Any]]:
    """
    디렉토리에서 파일을 검색합니다.

    Args:
        directory: 검색할 디렉토리
        pattern: 파일명 패턴 (glob)
        max_results: 최대 결과 수

    Returns:
        파일 정보 리스트
    """
    import os
    import glob

    search_path = os.path.join(directory, pattern)
    files = glob.glob(search_path, recursive=True)

    results = []
    for filepath in files[:max_results]:
        stat = os.stat(filepath)
        results.append({
            "path": filepath,
            "size": stat.st_size,
            "modified": stat.st_mtime
        })

    return results

3. 파일 작업 도구

Python
import os
from pathlib import Path

@server.tool()
async def read_file(path: str) -> str:
    """파일 내용을 읽습니다."""
    with open(path, 'r', encoding='utf-8') as f:
        return f.read()

@server.tool()
async def write_file(path: str, content: str) -> str:
    """파일에 내용을 씁니다."""
    # 디렉토리가 없으면 생성
    os.makedirs(os.path.dirname(path), exist_ok=True)

    with open(path, 'w', encoding='utf-8') as f:
        f.write(content)

    return f"파일 작성 완료: {path}"

@server.tool()
async def list_directory(path: str) -> List[Dict[str, Any]]:
    """디렉토리 내용을 나열합니다."""
    items = []
    for entry in os.listdir(path):
        full_path = os.path.join(path, entry)
        is_dir = os.path.isdir(full_path)

        items.append({
            "name": entry,
            "path": full_path,
            "type": "directory" if is_dir else "file"
        })

    return items

4. 에러 처리

Python
from mcp.server import McpError

@server.tool()
async def safe_read_file(path: str) -> str:
    """안전하게 파일을 읽습니다."""
    # 경로 검증
    if ".." in path:
        raise McpError(
            code=-32602,
            message="Invalid path: path traversal not allowed"
        )

    # 파일 존재 확인
    if not os.path.exists(path):
        raise McpError(
            code=-32001,
            message="File not found",
            data={"path": path}
        )

    # 읽기 권한 확인
    if not os.access(path, os.R_OK):
        raise McpError(
            code=-32002,
            message="Permission denied",
            data={"path": path}
        )

    try:
        with open(path, 'r', encoding='utf-8') as f:
            return f.read()
    except Exception as e:
        raise McpError(
            code=-32603,
            message=f"Failed to read file: {e}"
        )

리소스 (Resources) 구현

Python
from mcp.types import Resource, TextContent

# 1. 리소스 목록 핸들러
@server.list_resources()
async def list_resources() -> List[Resource]:
    """사용 가능한 리소스 목록을 반환합니다."""
    return [
        Resource(
            uri="file:///etc/hosts",
            name="System hosts file",
            description="시스템 호스트 파일",
            mimeType="text/plain"
        ),
        Resource(
            uri="config://app",
            name="Application config",
            description="애플리케이션 설정",
            mimeType="application/json"
        )
    ]

# 2. 리소스 읽기 핸들러
@server.read_resource()
async def read_resource(uri: str) -> str:
    """리소스 내용을 읽습니다."""
    if uri.startswith("file://"):
        path = uri[7:]  # "file://" 제거
        with open(path, 'r') as f:
            return f.read()

    elif uri == "config://app":
        import json
        config = {
            "version": "1.0.0",
            "debug": False
        }
        return json.dumps(config, indent=2)

    else:
        raise McpError(
            code=-32602,
            message=f"Unknown resource URI: {uri}"
        )

동적 리소스

Python
@server.list_resources()
async def list_project_files() -> List[Resource]:
    """프로젝트 파일을 동적으로 리소스로 제공합니다."""
    resources = []
    project_dir = "/path/to/project"

    for root, dirs, files in os.walk(project_dir):
        for filename in files:
            if filename.endswith(('.py', '.js', '.ts')):
                filepath = os.path.join(root, filename)
                uri = f"file://{filepath}"

                resources.append(Resource(
                    uri=uri,
                    name=filename,
                    description=f"Source file: {filepath}",
                    mimeType="text/plain"
                ))

    return resources

프롬프트 (Prompts) 구현

Python
from mcp.types import Prompt, PromptMessage, TextContent

# 1. 프롬프트 목록
@server.list_prompts()
async def list_prompts() -> List[Prompt]:
    """사용 가능한 프롬프트 목록을 반환합니다."""
    return [
        Prompt(
            name="code-review",
            description="코드 리뷰 프롬프트",
            arguments=[
                {
                    "name": "language",
                    "description": "프로그래밍 언어",
                    "required": True
                },
                {
                    "name": "code",
                    "description": "리뷰할 코드",
                    "required": True
                }
            ]
        ),
        Prompt(
            name="translate",
            description="번역 프롬프트",
            arguments=[
                {
                    "name": "text",
                    "description": "번역할 텍스트",
                    "required": True
                },
                {
                    "name": "target_lang",
                    "description": "목표 언어",
                    "required": True
                }
            ]
        )
    ]

# 2. 프롬프트 가져오기
@server.get_prompt()
async def get_prompt(name: str, arguments: Dict[str, str]) -> List[PromptMessage]:
    """프롬프트를 생성하여 반환합니다."""
    if name == "code-review":
        language = arguments.get("language")
        code = arguments.get("code")

        prompt_text = f"""다음 {language} 코드를 리뷰해주세요:

```{language}
{code}
```

다음 관점에서 검토해주세요:
1. 코드 품질 및 가독성
2. 성능 최적화 가능성
3. 보안 취약점
4. 베스트 프랙티스 준수 여부
5. 개선 제안

각 항목에 대해 구체적인 피드백을 제공해주세요."""

        return [
            PromptMessage(
                role="user",
                content=TextContent(type="text", text=prompt_text)
            )
        ]

    elif name == "translate":
        text = arguments.get("text")
        target_lang = arguments.get("target_lang")

        prompt_text = f"""다음 텍스트를 {target_lang}로 번역해주세요:

{text}

번역 시 다음을 고려해주세요:
- 자연스러운 표현 사용
- 문화적 맥락 반영
- 전문 용어의 정확한 번역"""

        return [
            PromptMessage(
                role="user",
                content=TextContent(type="text", text=prompt_text)
            )
        ]

    else:
        raise McpError(
            code=-32602,
            message=f"Unknown prompt: {name}"
        )

완전한 Python 서버 예제

Python
#!/usr/bin/env python3
"""
완전한 MCP 파일시스템 서버
"""

import os
import json
from typing import List, Dict, Any
from pathlib import Path

from mcp.server import Server, McpError
from mcp.server.stdio import stdio_server
from mcp.types import Resource, Prompt, PromptMessage, TextContent

# 서버 생성
server = Server("filesystem-server")

# 허용된 디렉토리 (보안)
ALLOWED_DIRS = [
    str(Path.home() / "Documents"),
    str(Path.home() / "Projects")
]

def is_path_allowed(path: str) -> bool:
    """경로가 허용된 디렉토리 내에 있는지 확인합니다."""
    abs_path = os.path.abspath(path)
    return any(abs_path.startswith(allowed) for allowed in ALLOWED_DIRS)

# ===== 도구 =====

@server.tool()
async def read_file(path: str) -> str:
    """파일 내용을 읽습니다."""
    if not is_path_allowed(path):
        raise McpError(code=-32602, message="Path not allowed")

    try:
        with open(path, 'r', encoding='utf-8') as f:
            return f.read()
    except FileNotFoundError:
        raise McpError(code=-32001, message="File not found")
    except Exception as e:
        raise McpError(code=-32603, message=str(e))

@server.tool()
async def write_file(path: str, content: str) -> str:
    """파일에 내용을 씁니다."""
    if not is_path_allowed(path):
        raise McpError(code=-32602, message="Path not allowed")

    try:
        os.makedirs(os.path.dirname(path), exist_ok=True)
        with open(path, 'w', encoding='utf-8') as f:
            f.write(content)
        return f"Successfully wrote {len(content)} bytes to {path}"
    except Exception as e:
        raise McpError(code=-32603, message=str(e))

@server.tool()
async def list_directory(path: str) -> List[Dict[str, Any]]:
    """디렉토리 내용을 나열합니다."""
    if not is_path_allowed(path):
        raise McpError(code=-32602, message="Path not allowed")

    try:
        items = []
        for entry in os.listdir(path):
            full_path = os.path.join(path, entry)
            stat = os.stat(full_path)

            items.append({
                "name": entry,
                "path": full_path,
                "type": "directory" if os.path.isdir(full_path) else "file",
                "size": stat.st_size,
                "modified": stat.st_mtime
            })

        return items
    except Exception as e:
        raise McpError(code=-32603, message=str(e))

@server.tool()
async def search_files(
    directory: str,
    pattern: str,
    max_results: int = 50
) -> List[str]:
    """파일을 검색합니다."""
    if not is_path_allowed(directory):
        raise McpError(code=-32602, message="Path not allowed")

    import glob

    search_path = os.path.join(directory, "**", pattern)
    results = glob.glob(search_path, recursive=True)
    return results[:max_results]

# ===== 리소스 =====

@server.list_resources()
async def list_resources() -> List[Resource]:
    """허용된 디렉토리를 리소스로 나열합니다."""
    resources = []
    for dir_path in ALLOWED_DIRS:
        if os.path.exists(dir_path):
            resources.append(Resource(
                uri=f"file://{dir_path}",
                name=os.path.basename(dir_path),
                description=f"Directory: {dir_path}",
                mimeType="text/directory"
            ))
    return resources

@server.read_resource()
async def read_resource(uri: str) -> str:
    """리소스를 읽습니다."""
    if not uri.startswith("file://"):
        raise McpError(code=-32602, message="Only file:// URIs supported")

    path = uri[7:]
    if not is_path_allowed(path):
        raise McpError(code=-32602, message="Path not allowed")

    if os.path.isdir(path):
        # 디렉토리 내용을 JSON으로 반환
        items = await list_directory(path)
        return json.dumps(items, indent=2)
    else:
        # 파일 내용 반환
        return await read_file(path)

# ===== 메인 =====

async def main():
    async with stdio_server() as (read_stream, write_stream):
        await server.run(
            read_stream,
            write_stream,
            server.create_initialization_options()
        )

if __name__ == "__main__":
    import asyncio
    asyncio.run(main())

TypeScript 서버 개발

설치 및 프로젝트 설정

Bash
# 프로젝트 초기화
mkdir my-mcp-server
cd my-mcp-server
npm init -y

# 의존성 설치
npm install @modelcontextprotocol/sdk
npm install -D typescript @types/node

# TypeScript 설정
npx tsc --init

tsconfig.json

JSON
{
  "compilerOptions": {
    "target": "ES2022",
    "module": "Node16",
    "moduleResolution": "Node16",
    "outDir": "./dist",
    "rootDir": "./src",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules"]
}

기본 TypeScript 서버

TypeScript
#!/usr/bin/env node
// src/index.ts

import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import {
  CallToolRequestSchema,
  ListToolsRequestSchema,
  Tool
} from "@modelcontextprotocol/sdk/types.js";

// 서버 생성
const server = new Server(
  {
    name: "my-mcp-server",
    version: "1.0.0"
  },
  {
    capabilities: {
      tools: {}
    }
  }
);

// 도구 목록
const TOOLS: Tool[] = [
  {
    name: "hello",
    description: "인사 메시지를 반환합니다",
    inputSchema: {
      type: "object",
      properties: {
        name: {
          type: "string",
          description: "인사할 이름"
        }
      },
      required: []
    }
  }
];

// 도구 목록 핸들러
server.setRequestHandler(ListToolsRequestSchema, async () => ({
  tools: TOOLS
}));

// 도구 호출 핸들러
server.setRequestHandler(CallToolRequestSchema, async (request) => {
  const { name, arguments: args } = request.params;

  if (name === "hello") {
    const userName = (args as { name?: string }).name || "World";
    return {
      content: [
        {
          type: "text",
          text: `Hello, ${userName}!`
        }
      ]
    };
  }

  throw new Error(`Unknown tool: ${name}`);
});

// 서버 실행
async function main() {
  const transport = new StdioServerTransport();
  await server.connect(transport);
}

main().catch(console.error);

TypeScript 도구 구현

TypeScript
import * as fs from "fs/promises";
import * as path from "path";

// 도구 정의
const TOOLS: Tool[] = [
  {
    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"]
    }
  },
  {
    name: "list_directory",
    description: "디렉토리 내용을 나열합니다",
    inputSchema: {
      type: "object",
      properties: {
        path: {
          type: "string",
          description: "디렉토리 경로"
        }
      },
      required: ["path"]
    }
  }
];

// 도구 구현
async function readFile(filePath: string): Promise<string> {
  const content = await fs.readFile(filePath, "utf-8");
  return content;
}

async function writeFile(filePath: string, content: string): Promise<string> {
  await fs.mkdir(path.dirname(filePath), { recursive: true });
  await fs.writeFile(filePath, content, "utf-8");
  return `Successfully wrote ${content.length} bytes to ${filePath}`;
}

async function listDirectory(dirPath: string) {
  const entries = await fs.readdir(dirPath, { withFileTypes: true });

  const items = await Promise.all(
    entries.map(async (entry) => {
      const fullPath = path.join(dirPath, entry.name);
      const stats = await fs.stat(fullPath);

      return {
        name: entry.name,
        path: fullPath,
        type: entry.isDirectory() ? "directory" : "file",
        size: stats.size,
        modified: stats.mtime
      };
    })
  );

  return items;
}

// 도구 호출 핸들러
server.setRequestHandler(CallToolRequestSchema, async (request) => {
  const { name, arguments: args } = request.params;

  try {
    switch (name) {
      case "read_file": {
        const { path } = args as { path: string };
        const content = await readFile(path);
        return {
          content: [{ type: "text", text: content }]
        };
      }

      case "write_file": {
        const { path, content } = args as { path: string; content: string };
        const result = await writeFile(path, content);
        return {
          content: [{ type: "text", text: result }]
        };
      }

      case "list_directory": {
        const { path } = args as { path: string };
        const items = await listDirectory(path);
        return {
          content: [{ type: "text", text: JSON.stringify(items, null, 2) }]
        };
      }

      default:
        throw new Error(`Unknown tool: ${name}`);
    }
  } catch (error) {
    const errorMessage = error instanceof Error ? error.message : String(error);
    return {
      content: [{ type: "text", text: `Error: ${errorMessage}` }],
      isError: true
    };
  }
});

TypeScript 리소스 구현

TypeScript
import {
  ListResourcesRequestSchema,
  ReadResourceRequestSchema,
  Resource
} from "@modelcontextprotocol/sdk/types.js";

// 리소스 목록 핸들러
server.setRequestHandler(ListResourcesRequestSchema, async () => {
  const resources: Resource[] = [
    {
      uri: "file:///etc/hosts",
      name: "System hosts",
      description: "시스템 호스트 파일",
      mimeType: "text/plain"
    },
    {
      uri: "config://app",
      name: "App config",
      description: "애플리케이션 설정",
      mimeType: "application/json"
    }
  ];

  return { resources };
});

// 리소스 읽기 핸들러
server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
  const { uri } = request.params;

  if (uri.startsWith("file://")) {
    const filePath = uri.slice(7);
    const content = await fs.readFile(filePath, "utf-8");

    return {
      contents: [
        {
          uri,
          mimeType: "text/plain",
          text: content
        }
      ]
    };
  }

  if (uri === "config://app") {
    const config = {
      version: "1.0.0",
      debug: false
    };

    return {
      contents: [
        {
          uri,
          mimeType: "application/json",
          text: JSON.stringify(config, null, 2)
        }
      ]
    };
  }

  throw new Error(`Unknown resource: ${uri}`);
});

완전한 TypeScript 서버 예제

TypeScript
#!/usr/bin/env node
// src/index.ts - 완전한 파일시스템 MCP 서버

import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import {
  CallToolRequestSchema,
  ListToolsRequestSchema,
  ListResourcesRequestSchema,
  ReadResourceRequestSchema,
  Tool,
  Resource
} from "@modelcontextprotocol/sdk/types.js";
import * as fs from "fs/promises";
import * as path from "path";
import { glob } from "glob";

// 허용된 디렉토리
const ALLOWED_DIRS = [
  path.join(process.env.HOME || "", "Documents"),
  path.join(process.env.HOME || "", "Projects")
];

function isPathAllowed(filePath: string): boolean {
  const absolute = path.resolve(filePath);
  return ALLOWED_DIRS.some((dir) => absolute.startsWith(dir));
}

// 서버 생성
const server = new Server(
  {
    name: "filesystem-server",
    version: "1.0.0"
  },
  {
    capabilities: {
      tools: {},
      resources: {}
    }
  }
);

// 도구 정의
const TOOLS: Tool[] = [
  {
    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"]
    }
  },
  {
    name: "list_directory",
    description: "디렉토리 내용을 나열합니다",
    inputSchema: {
      type: "object",
      properties: {
        path: { type: "string", description: "디렉토리 경로" }
      },
      required: ["path"]
    }
  },
  {
    name: "search_files",
    description: "파일을 검색합니다",
    inputSchema: {
      type: "object",
      properties: {
        directory: { type: "string", description: "검색 디렉토리" },
        pattern: { type: "string", description: "파일명 패턴 (glob)" }
      },
      required: ["directory", "pattern"]
    }
  }
];

// 핸들러 등록
server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: TOOLS }));

server.setRequestHandler(CallToolRequestSchema, async (request) => {
  const { name, arguments: args } = request.params;

  try {
    switch (name) {
      case "read_file": {
        const { path: filePath } = args as { path: string };
        if (!isPathAllowed(filePath)) {
          throw new Error("Path not allowed");
        }
        const content = await fs.readFile(filePath, "utf-8");
        return { content: [{ type: "text", text: content }] };
      }

      case "write_file": {
        const { path: filePath, content } = args as { path: string; content: string };
        if (!isPathAllowed(filePath)) {
          throw new Error("Path not allowed");
        }
        await fs.mkdir(path.dirname(filePath), { recursive: true });
        await fs.writeFile(filePath, content, "utf-8");
        return { content: [{ type: "text", text: `Wrote to ${filePath}` }] };
      }

      case "list_directory": {
        const { path: dirPath } = args as { path: string };
        if (!isPathAllowed(dirPath)) {
          throw new Error("Path not allowed");
        }
        const entries = await fs.readdir(dirPath, { withFileTypes: true });
        const items = entries.map((e) => ({
          name: e.name,
          type: e.isDirectory() ? "directory" : "file"
        }));
        return { content: [{ type: "text", text: JSON.stringify(items, null, 2) }] };
      }

      case "search_files": {
        const { directory, pattern } = args as { directory: string; pattern: string };
        if (!isPathAllowed(directory)) {
          throw new Error("Path not allowed");
        }
        const files = await glob(pattern, { cwd: directory });
        return { content: [{ type: "text", text: JSON.stringify(files, null, 2) }] };
      }

      default:
        throw new Error(`Unknown tool: ${name}`);
    }
  } catch (error) {
    return {
      content: [{ type: "text", text: `Error: ${error}` }],
      isError: true
    };
  }
});

// 리소스 핸들러
server.setRequestHandler(ListResourcesRequestSchema, async () => {
  const resources: Resource[] = ALLOWED_DIRS
    .filter((dir) => fs.access(dir).then(() => true).catch(() => false))
    .map((dir) => ({
      uri: `file://${dir}`,
      name: path.basename(dir),
      description: `Directory: ${dir}`,
      mimeType: "text/directory"
    }));

  return { resources };
});

server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
  const { uri } = request.params;

  if (!uri.startsWith("file://")) {
    throw new Error("Only file:// URIs supported");
  }

  const filePath = uri.slice(7);
  if (!isPathAllowed(filePath)) {
    throw new Error("Path not allowed");
  }

  const content = await fs.readFile(filePath, "utf-8");

  return {
    contents: [{
      uri,
      mimeType: "text/plain",
      text: content
    }]
  };
});

// 서버 실행
async function main() {
  const transport = new StdioServerTransport();
  await server.connect(transport);
}

main().catch(console.error);

패키징 및 배포

Python 패키징

pyproject.toml

TOML
[project]
name = "mcp-server-filesystem"
version = "1.0.0"
description = "MCP filesystem server"
authors = [{ name = "Your Name", email = "you@example.com" }]
requires-python = ">=3.10"
dependencies = [
    "mcp>=0.1.0"
]

[project.scripts]
mcp-server-filesystem = "mcp_server_filesystem:main"

[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"

설치 및 실행

Bash
# 개발 모드 설치
uv pip install -e .

# 또는 pip
pip install -e .

# 서버 실행
mcp-server-filesystem

TypeScript 패키징

package.json

JSON
{
  "name": "@myorg/mcp-server-filesystem",
  "version": "1.0.0",
  "description": "MCP filesystem server",
  "type": "module",
  "bin": {
    "mcp-server-filesystem": "./dist/index.js"
  },
  "scripts": {
    "build": "tsc",
    "prepare": "npm run build"
  },
  "dependencies": {
    "@modelcontextprotocol/sdk": "^0.1.0"
  },
  "devDependencies": {
    "typescript": "^5.3.0",
    "@types/node": "^20.0.0"
  }
}

빌드 및 실행

Bash
# 빌드
npm run build

# 글로벌 설치
npm install -g .

# 서버 실행
mcp-server-filesystem

배포

Bash
# Python (PyPI)
uv build
uv publish

# TypeScript (npm)
npm publish

테스트

수동 테스트

Bash
# MCP Inspector 사용
npx @modelcontextprotocol/inspector python server.py

# 또는 TypeScript 서버
npx @modelcontextprotocol/inspector node dist/index.js

브라우저에서 http://localhost:5173을 열어 서버를 테스트할 수 있습니다.

자동화 테스트 (Python)

Python
import pytest
from mcp.client import Client
from mcp.client.stdio import StdioClientTransport

@pytest.mark.asyncio
async def test_server():
    client = Client({"name": "test", "version": "1.0.0"}, {"capabilities": {"tools": {}}})

    transport = StdioClientTransport({
        "command": "python",
        "args": ["server.py"]
    })

    await client.connect(transport)

    # 도구 목록 테스트
    tools = await client.list_tools()
    assert len(tools) > 0

    # 도구 호출 테스트
    result = await client.call_tool("hello", {"name": "Test"})
    assert "Hello, Test!" in result

    await client.close()

모범 사례

서버 개발 권장사항
  1. 보안: 경로 검증, 권한 확인, 입력 검증 필수
  2. 에러 처리: 명확한 에러 메시지와 적절한 에러 코드 사용
  3. 문서화: 도구 설명과 입력 스키마를 상세히 작성
  4. 성능: 비동기 작업 사용, 대용량 데이터 스트리밍 고려
  5. 로깅: 디버깅을 위한 적절한 로깅 구현
  6. 테스트: 자동화 테스트 작성

보안 고려사항

위험 대응
경로 탐색 (Path Traversal) 허용 목록 사용, .. 검증
명령 주입 입력 검증, 이스케이핑
리소스 소진 크기 제한, 타임아웃 설정
민감 정보 노출 환경 변수, 시크릿 파일 제외

다음 단계

MCP 서버 만들기 완전 가이드

이 섹션에서는 MCP 서버를 처음부터 끝까지 만드는 과정을 단계별로 상세히 안내합니다. Python과 TypeScript 두 가지 언어로 완전한 서버를 구축하고, FastMCP를 활용한 빠른 개발 방법도 다룹니다.

처음부터 만드는 MCP 서버 (Python)

Python으로 MCP 서버를 처음부터 만드는 10단계 과정입니다. 각 단계마다 실행 가능한 코드와 함께 설명합니다.

1단계: 프로젝트 구조 설정

먼저 프로젝트 디렉토리 구조를 만듭니다. 깔끔한 구조는 유지보수와 배포에 필수적입니다.

Bash
# 프로젝트 디렉토리 생성
mkdir my-mcp-server
cd my-mcp-server

# 기본 구조 생성
mkdir -p src/my_mcp_server
mkdir -p tests
touch src/my_mcp_server/__init__.py
touch src/my_mcp_server/server.py
touch src/my_mcp_server/tools.py
touch src/my_mcp_server/resources.py
touch tests/__init__.py
touch tests/test_server.py

최종 디렉토리 구조는 다음과 같습니다:

Text 프로젝트 구조
my-mcp-server/
├── pyproject.toml
├── README.md
├── src/
│   └── my_mcp_server/
│       ├── __init__.py
│       ├── server.py      # 메인 서버 진입점
│       ├── tools.py        # 도구 정의
│       └── resources.py    # 리소스 정의
└── tests/
    ├── __init__.py
    └── test_server.py

pyproject.toml 파일을 작성합니다. 이 파일은 프로젝트 메타데이터와 의존성을 관리합니다:

TOML pyproject.toml
[project]
name = "my-mcp-server"
version = "0.1.0"
description = "내 첫 MCP 서버"
requires-python = ">=3.10"
dependencies = [
    "mcp>=1.0.0",
    "httpx>=0.25.0",
]

[project.optional-dependencies]
dev = [
    "pytest>=7.0",
    "pytest-asyncio>=0.21",
]

[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"

[project.scripts]
my-mcp-server = "my_mcp_server.server:main"

2단계: uv로 의존성 설치

uv는 Python의 빠른 패키지 관리자입니다. pip 대비 10~100배 빠른 설치 속도를 제공합니다.

Bash
# uv 설치 (아직 없는 경우)
curl -LsSf https://astral.sh/uv/install.sh | sh

# 가상 환경 생성 및 의존성 설치
uv venv
source .venv/bin/activate

# pyproject.toml 기반 의존성 설치
uv pip install -e ".[dev]"

# 또는 uv 프로젝트 매니저로 직접 추가
uv add mcp
uv add httpx
uv add --dev pytest pytest-asyncio
uv를 권장하는 이유: uv는 Rust로 작성된 초고속 패키지 관리자로, 가상 환경 생성과 패키지 설치를 매우 빠르게 처리합니다. MCP 공식 문서에서도 uv 사용을 권장합니다.

3단계: 서버 뼈대 코드 작성

MCP 서버의 핵심 뼈대를 작성합니다. Server 클래스를 생성하고 stdio 전송을 설정합니다.

Python src/my_mcp_server/__init__.py
"""MCP 서버 패키지"""
__version__ = "0.1.0"
Python src/my_mcp_server/server.py
"""MCP 서버 메인 모듈"""
import asyncio
import logging
from mcp.server import Server
from mcp.server.stdio import stdio_server

# 로거 설정
logger = logging.getLogger(__name__)

# 서버 인스턴스 생성
server = Server("my-mcp-server")

# 도구와 리소스를 별도 모듈에서 임포트하여 등록
from my_mcp_server import tools     # noqa: E402, F401
from my_mcp_server import resources  # noqa: E402, F401

async def run_server():
    """서버를 실행합니다."""
    logger.info("MCP 서버 시작 중...")

    async with stdio_server() as (read_stream, write_stream):
        await server.run(
            read_stream,
            write_stream,
            server.create_initialization_options()
        )

def main():
    """CLI 진입점"""
    logging.basicConfig(level=logging.INFO)
    asyncio.run(run_server())

if __name__ == "__main__":
    main()

4단계: 도구(Tool) 등록 - inputSchema 상세 작성법

도구는 MCP 서버의 핵심 기능입니다. AI 클라이언트가 호출할 수 있는 함수를 정의합니다. 각 도구에는 이름, 설명, 입력 스키마(inputSchema)가 필요합니다.

inputSchema의 중요성: inputSchema는 JSON Schema 형식으로 도구의 입력 매개변수를 정의합니다. AI 모델은 이 스키마를 읽고 올바른 형식의 인자를 생성합니다. 스키마가 명확할수록 AI의 도구 사용 정확도가 높아집니다.
Python src/my_mcp_server/tools.py
"""MCP 서버 도구 정의"""
import json
import httpx
from typing import Any
from mcp.types import Tool, TextContent
from my_mcp_server.server import server

# --- 도구 목록 핸들러 ---
@server.list_tools()
async def list_tools() -> list[Tool]:
    """사용 가능한 도구 목록을 반환합니다."""
    return [
        Tool(
            name="get_weather",
            description="지정된 도시의 현재 날씨 정보를 조회합니다. "
                        "도시명은 한글 또는 영문으로 입력할 수 있습니다.",
            inputSchema={
                "type": "object",
                "properties": {
                    "city": {
                        "type": "string",
                        "description": "날씨를 조회할 도시명 (예: 서울, Tokyo, New York)"
                    },
                    "units": {
                        "type": "string",
                        "enum": ["celsius", "fahrenheit"],
                        "description": "온도 단위 (기본값: celsius)",
                        "default": "celsius"
                    }
                },
                "required": ["city"]
            }
        ),
        Tool(
            name="calculate",
            description="수학 계산을 수행합니다. 사칙연산과 거듭제곱을 지원합니다.",
            inputSchema={
                "type": "object",
                "properties": {
                    "operation": {
                        "type": "string",
                        "enum": ["add", "subtract", "multiply", "divide", "power"],
                        "description": "수행할 연산 종류"
                    },
                    "a": {
                        "type": "number",
                        "description": "첫 번째 피연산자"
                    },
                    "b": {
                        "type": "number",
                        "description": "두 번째 피연산자"
                    }
                },
                "required": ["operation", "a", "b"]
            }
        ),
        Tool(
            name="search_web",
            description="웹 검색을 수행하여 최신 정보를 조회합니다.",
            inputSchema={
                "type": "object",
                "properties": {
                    "query": {
                        "type": "string",
                        "description": "검색 쿼리"
                    },
                    "max_results": {
                        "type": "integer",
                        "description": "최대 결과 수 (1~20)",
                        "minimum": 1,
                        "maximum": 20,
                        "default": 5
                    },
                    "language": {
                        "type": "string",
                        "description": "검색 결과 언어 코드 (예: ko, en, ja)",
                        "default": "ko"
                    }
                },
                "required": ["query"]
            }
        )
    ]

# --- 도구 실행 핸들러 ---
@server.call_tool()
async def call_tool(name: str, arguments: dict[str, Any]) -> list[TextContent]:
    """도구 호출을 처리합니다."""

    if name == "get_weather":
        city = arguments["city"]
        units = arguments.get("units", "celsius")

        # 실제로는 날씨 API를 호출합니다
        async with httpx.AsyncClient() as client:
            response = await client.get(
                "https://api.weatherapi.com/v1/current.json",
                params={"key": "YOUR_API_KEY", "q": city}
            )
            data = response.json()

        return [TextContent(
            type="text",
            text=json.dumps(data, ensure_ascii=False, indent=2)
        )]

    elif name == "calculate":
        op = arguments["operation"]
        a = arguments["a"]
        b = arguments["b"]

        operations = {
            "add": lambda: a + b,
            "subtract": lambda: a - b,
            "multiply": lambda: a * b,
            "divide": lambda: a / b if b != 0 else "오류: 0으로 나눌 수 없습니다",
            "power": lambda: a ** b,
        }
        result = operations[op]()

        return [TextContent(type="text", text=str(result))]

    else:
        raise ValueError(f"알 수 없는 도구: {name}")

5단계: 리소스(Resource) 등록 - URI 체계 설명

리소스는 AI 클라이언트에게 데이터를 제공하는 방법입니다. 각 리소스는 고유한 URI로 식별됩니다.

리소스 URI 체계: MCP 리소스 URI는 자유롭게 정의할 수 있습니다. 일반적인 패턴:
  • file:///path/to/file - 파일 시스템 리소스
  • config://app-name - 애플리케이션 설정
  • db://database/table - 데이터베이스 리소스
  • api://service/endpoint - API 엔드포인트 데이터
Python src/my_mcp_server/resources.py
"""MCP 서버 리소스 정의"""
import json
import os
from datetime import datetime
from mcp.types import Resource, TextContent
from my_mcp_server.server import server

# --- 리소스 목록 핸들러 ---
@server.list_resources()
async def list_resources() -> list[Resource]:
    """사용 가능한 리소스 목록을 반환합니다."""
    return [
        Resource(
            uri="config://app",
            name="앱 설정",
            description="현재 애플리케이션의 설정 정보",
            mimeType="application/json"
        ),
        Resource(
            uri="status://server",
            name="서버 상태",
            description="서버의 현재 상태 정보",
            mimeType="application/json"
        ),
        Resource(
            uri="file:///var/log/app.log",
            name="앱 로그",
            description="애플리케이션 최근 로그",
            mimeType="text/plain"
        )
    ]

# --- 리소스 읽기 핸들러 ---
@server.read_resource()
async def read_resource(uri: str) -> str:
    """리소스 내용을 읽습니다."""

    if uri == "config://app":
        config = {
            "app_name": "My MCP Server",
            "version": "0.1.0",
            "debug": os.environ.get("DEBUG", "false"),
            "log_level": os.environ.get("LOG_LEVEL", "INFO"),
        }
        return json.dumps(config, indent=2, ensure_ascii=False)

    elif uri == "status://server":
        status = {
            "status": "running",
            "uptime": "정상",
            "timestamp": datetime.now().isoformat(),
        }
        return json.dumps(status, indent=2, ensure_ascii=False)

    elif uri.startswith("file://"):
        path = uri[7:]
        if not os.path.exists(path):
            return f"파일을 찾을 수 없습니다: {path}"
        with open(path, "r", encoding="utf-8") as f:
            return f.read()

    raise ValueError(f"알 수 없는 리소스: {uri}")

6단계: 프롬프트(Prompt) 등록 - 템플릿 패턴

프롬프트는 재사용 가능한 프롬프트 템플릿을 정의합니다. AI 클라이언트가 특정 작업에 최적화된 프롬프트를 요청할 수 있습니다.

Python src/my_mcp_server/prompts.py
"""MCP 서버 프롬프트 정의"""
from mcp.types import Prompt, PromptArgument, PromptMessage, TextContent
from my_mcp_server.server import server

@server.list_prompts()
async def list_prompts() -> list[Prompt]:
    """사용 가능한 프롬프트 템플릿 목록"""
    return [
        Prompt(
            name="code-review",
            description="코드 리뷰를 수행하는 프롬프트",
            arguments=[
                PromptArgument(
                    name="code",
                    description="리뷰할 코드",
                    required=True
                ),
                PromptArgument(
                    name="language",
                    description="프로그래밍 언어 (예: python, javascript)",
                    required=False
                )
            ]
        ),
        Prompt(
            name="explain-error",
            description="에러 메시지를 분석하고 해결책을 제안합니다",
            arguments=[
                PromptArgument(
                    name="error",
                    description="에러 메시지 또는 스택 트레이스",
                    required=True
                ),
                PromptArgument(
                    name="context",
                    description="에러가 발생한 상황 설명",
                    required=False
                )
            ]
        )
    ]

@server.get_prompt()
async def get_prompt(name: str, arguments: dict | None = None) -> list[PromptMessage]:
    """프롬프트 템플릿을 렌더링합니다."""
    args = arguments or {}

    if name == "code-review":
        code = args.get("code", "")
        language = args.get("language", "알 수 없음")
        return [
            PromptMessage(
                role="user",
                content=TextContent(
                    type="text",
                    text=f"""다음 {language} 코드를 리뷰해주세요.

검토 항목:
1. 코드 품질 및 가독성
2. 잠재적 버그나 에러
3. 성능 개선 가능성
4. 보안 취약점
5. 모범 사례 준수 여부

코드:
```
{code}
```"""
                )
            )
        ]

    elif name == "explain-error":
        error = args.get("error", "")
        context = args.get("context", "추가 컨텍스트 없음")
        return [
            PromptMessage(
                role="user",
                content=TextContent(
                    type="text",
                    text=f"""다음 에러를 분석하고 해결책을 제안해주세요.

에러 메시지:
```
{error}
```

상황: {context}

다음을 포함하여 답변해주세요:
1. 에러의 원인
2. 구체적인 해결 방법
3. 향후 예방 방법"""
                )
            )
        ]

    raise ValueError(f"알 수 없는 프롬프트: {name}")

7단계: 에러 처리 패턴

MCP 서버에서 발생할 수 있는 다양한 에러를 체계적으로 처리하는 패턴입니다.

Python 에러 처리 유틸리티
import logging
import traceback
from functools import wraps
from mcp.types import TextContent, INTERNAL_ERROR, INVALID_PARAMS

logger = logging.getLogger(__name__)

# MCP 표준 에러 코드
ERROR_CODES = {
    "PARSE_ERROR": -32700,
    "INVALID_REQUEST": -32600,
    "METHOD_NOT_FOUND": -32601,
    "INVALID_PARAMS": -32602,
    "INTERNAL_ERROR": -32603,
}

def safe_tool(func):
    """도구 함수에 에러 처리를 추가하는 데코레이터"""
    @wraps(func)
    async def wrapper(*args, **kwargs):
        try:
            return await func(*args, **kwargs)
        except ValueError as e:
            logger.warning(f"잘못된 입력: {e}")
            return [TextContent(
                type="text",
                text=f"입력 오류: {e}"
            )]
        except FileNotFoundError as e:
            logger.warning(f"파일 없음: {e}")
            return [TextContent(
                type="text",
                text=f"파일을 찾을 수 없습니다: {e}"
            )]
        except PermissionError as e:
            logger.error(f"권한 오류: {e}")
            return [TextContent(
                type="text",
                text=f"권한이 없습니다: {e}"
            )]
        except Exception as e:
            logger.error(f"예기치 못한 오류: {traceback.format_exc()}")
            return [TextContent(
                type="text",
                text=f"내부 오류가 발생했습니다: {type(e).__name__}: {e}"
            )]
    return wrapper

8단계: 로깅 구현

MCP 서버의 디버깅과 모니터링을 위한 로깅 설정입니다. 중요: MCP 서버는 stdout을 JSON-RPC 통신에 사용하므로, 로그는 반드시 stderr로 출력해야 합니다.

stdout 사용 금지: print() 함수로 stdout에 출력하면 MCP 프로토콜 통신이 깨집니다. 반드시 logging 모듈을 사용하고 stderr 핸들러를 설정하세요.
Python 로깅 설정
import logging
import sys

def setup_logging(level: str = "INFO") -> logging.Logger:
    """MCP 서버용 로깅을 설정합니다.

    중요: MCP는 stdout을 JSON-RPC에 사용하므로
    로그는 반드시 stderr로 출력합니다.
    """
    logger = logging.getLogger("my-mcp-server")
    logger.setLevel(getattr(logging, level.upper()))

    # stderr 핸들러 (stdout 사용 금지!)
    handler = logging.StreamHandler(sys.stderr)
    handler.setLevel(getattr(logging, level.upper()))

    # 포맷 설정
    formatter = logging.Formatter(
        "%(asctime)s [%(levelname)s] %(name)s: %(message)s",
        datefmt="%Y-%m-%d %H:%M:%S"
    )
    handler.setFormatter(formatter)
    logger.addHandler(handler)

    return logger

# 사용 예시
logger = setup_logging("DEBUG")
logger.debug("디버그 메시지")
logger.info("서버가 시작되었습니다")
logger.warning("경고: 설정 파일이 없습니다")
logger.error("에러 발생")

9단계: 서버 실행 및 테스트 (MCP Inspector)

작성한 서버를 실행하고 MCP Inspector로 테스트합니다.

Bash 서버 실행
# 직접 실행
python -m my_mcp_server.server

# 또는 pyproject.toml에 정의한 스크립트로 실행
my-mcp-server

# MCP Inspector로 테스트 (웹 UI 제공)
npx @modelcontextprotocol/inspector python -m my_mcp_server.server

# MCP Inspector가 브라우저를 열고 대화형 테스트 UI를 제공합니다
# 기본 주소: http://localhost:5173
MCP Inspector 활용: Inspector는 MCP 서버를 시각적으로 테스트할 수 있는 웹 도구입니다. 등록된 도구, 리소스, 프롬프트를 확인하고 직접 호출하여 결과를 확인할 수 있습니다. 개발 중에 항상 Inspector를 열어두는 것을 권장합니다.

10단계: Claude Desktop에 연결하여 테스트

개발한 MCP 서버를 Claude Desktop에 등록하여 실제 AI와 함께 테스트합니다.

JSON claude_desktop_config.json
{
  "mcpServers": {
    "my-mcp-server": {
      "command": "python",
      "args": ["-m", "my_mcp_server.server"],
      "cwd": "/path/to/my-mcp-server",
      "env": {
        "DEBUG": "true",
        "LOG_LEVEL": "DEBUG"
      }
    }
  }
}

설정 파일 위치:

  • macOS: ~/Library/Application Support/Claude/claude_desktop_config.json
  • Windows: %APPDATA%\Claude\claude_desktop_config.json
  • Linux: ~/.config/Claude/claude_desktop_config.json
uv 사용 시 설정: uv를 사용하는 경우 command를 "uv"로, args를 ["run", "my-mcp-server"]로 설정하면 가상 환경을 자동으로 활성화합니다.

처음부터 만드는 MCP 서버 (TypeScript)

TypeScript로 MCP 서버를 만드는 단계별 가이드입니다. 타입 안전성과 IDE 지원이 뛰어나며, Node.js 생태계를 활용할 수 있습니다.

1단계: npm init 및 package.json 설정

Bash
# 프로젝트 생성
mkdir my-mcp-server-ts
cd my-mcp-server-ts
npm init -y

# 디렉토리 구조 생성
mkdir -p src
mkdir -p tests
JSON package.json
{
  "name": "my-mcp-server-ts",
  "version": "0.1.0",
  "description": "TypeScript MCP 서버",
  "type": "module",
  "main": "dist/index.js",
  "bin": {
    "my-mcp-server-ts": "dist/index.js"
  },
  "scripts": {
    "build": "tsc",
    "dev": "tsc --watch",
    "start": "node dist/index.js",
    "inspect": "npx @modelcontextprotocol/inspector node dist/index.js",
    "test": "vitest"
  },
  "dependencies": {
    "@modelcontextprotocol/sdk": "^1.0.0"
  },
  "devDependencies": {
    "typescript": "^5.3.0",
    "@types/node": "^20.0.0",
    "vitest": "^1.0.0"
  }
}

2단계: TypeScript 설정

JSON tsconfig.json
{
  "compilerOptions": {
    "target": "ES2022",
    "module": "Node16",
    "moduleResolution": "Node16",
    "outDir": "./dist",
    "rootDir": "./src",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "declaration": true
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules", "dist", "tests"]
}

3단계: SDK 설치

Bash
# 핵심 의존성 설치
npm install @modelcontextprotocol/sdk

# 개발 의존성 설치
npm install -D typescript @types/node vitest

4단계: 서버 뼈대 (setRequestHandler 패턴)

TypeScript src/index.ts
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import {
  CallToolRequestSchema,
  ListToolsRequestSchema,
  ListResourcesRequestSchema,
  ReadResourceRequestSchema,
} from "@modelcontextprotocol/sdk/types.js";

// 서버 인스턴스 생성
const server = new Server(
  {
    name: "my-mcp-server-ts",
    version: "0.1.0",
  },
  {
    capabilities: {
      tools: {},
      resources: {},
    },
  }
);

// 도구 목록 핸들러
server.setRequestHandler(ListToolsRequestSchema, async () => {
  return {
    tools: [
      {
        name: "get_weather",
        description: "지정된 도시의 현재 날씨 정보를 조회합니다",
        inputSchema: {
          type: "object" as const,
          properties: {
            city: {
              type: "string",
              description: "날씨를 조회할 도시명",
            },
            units: {
              type: "string",
              enum: ["celsius", "fahrenheit"],
              description: "온도 단위",
            },
          },
          required: ["city"],
        },
      },
      {
        name: "calculate",
        description: "수학 계산을 수행합니다",
        inputSchema: {
          type: "object" as const,
          properties: {
            operation: {
              type: "string",
              enum: ["add", "subtract", "multiply", "divide"],
              description: "수행할 연산",
            },
            a: { type: "number", description: "첫 번째 피연산자" },
            b: { type: "number", description: "두 번째 피연산자" },
          },
          required: ["operation", "a", "b"],
        },
      },
    ],
  };
});

// 도구 실행 핸들러
server.setRequestHandler(CallToolRequestSchema, async (request) => {
  const { name, arguments: args } = request.params;

  switch (name) {
    case "get_weather": {
      const city = args?.city as string;
      // 실제로는 날씨 API를 호출합니다
      return {
        content: [
          {
            type: "text",
            text: `${city}의 현재 날씨: 맑음, 22도`,
          },
        ],
      };
    }
    case "calculate": {
      const { operation, a, b } = args as {
        operation: string;
        a: number;
        b: number;
      };
      let result: number;
      switch (operation) {
        case "add": result = a + b; break;
        case "subtract": result = a - b; break;
        case "multiply": result = a * b; break;
        case "divide":
          if (b === 0) throw new Error("0으로 나눌 수 없습니다");
          result = a / b;
          break;
        default: throw new Error(`알 수 없는 연산: ${operation}`);
      }
      return {
        content: [{ type: "text", text: String(result) }],
      };
    }
    default:
      throw new Error(`알 수 없는 도구: ${name}`);
  }
});

// 리소스 목록 핸들러
server.setRequestHandler(ListResourcesRequestSchema, async () => {
  return {
    resources: [
      {
        uri: "config://app",
        name: "앱 설정",
        description: "현재 애플리케이션 설정",
        mimeType: "application/json",
      },
    ],
  };
});

// 리소스 읽기 핸들러
server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
  const { uri } = request.params;

  if (uri === "config://app") {
    return {
      contents: [
        {
          uri,
          mimeType: "application/json",
          text: JSON.stringify(
            { appName: "My Server", version: "0.1.0", debug: true },
            null,
            2
          ),
        },
      ],
    };
  }

  throw new Error(`알 수 없는 리소스: ${uri}`);
});

// 서버 실행
async function main() {
  const transport = new StdioServerTransport();
  await server.connect(transport);
  console.error("MCP 서버가 stdio에서 실행 중입니다");
}

main().catch(console.error);

5단계: 빌드 및 실행

Bash
# TypeScript 빌드
npm run build

# 서버 실행
npm start

# MCP Inspector로 테스트
npm run inspect

# Claude Desktop 설정 (claude_desktop_config.json)
# {
#   "mcpServers": {
#     "my-ts-server": {
#       "command": "node",
#       "args": ["/path/to/my-mcp-server-ts/dist/index.js"]
#     }
#   }
# }

FastMCP로 빠르게 만들기

FastMCP는 Python MCP 서버 개발을 획기적으로 단순화하는 고수준 프레임워크입니다. Flask처럼 데코레이터 기반으로 도구, 리소스, 프롬프트를 등록하며, 타입 힌트에서 자동으로 JSON Schema를 생성합니다.

FastMCP vs 저수준 SDK: 저수준 SDK에서는 inputSchema를 직접 JSON Schema로 작성해야 하지만, FastMCP는 Python 타입 힌트를 분석하여 자동 생성합니다. 코드량이 50~70% 줄어들며 실수할 가능성도 크게 감소합니다.
Bash
# FastMCP 설치
pip install fastmcp
# 또는
uv add fastmcp

기본 서버 만들기

Python server.py
from fastmcp import FastMCP

# 서버 인스턴스 생성 (이름과 설정)
mcp = FastMCP(
    "My Fast Server",
    dependencies=["httpx", "beautifulsoup4"]
)

# --- 도구 등록 (데코레이터로 간단하게) ---

@mcp.tool()
def add(a: int, b: int) -> int:
    """두 수를 더합니다.

    Args:
        a: 첫 번째 숫자
        b: 두 번째 숫자

    Returns:
        두 수의 합
    """
    return a + b

@mcp.tool()
def multiply(a: float, b: float) -> float:
    """두 수를 곱합니다."""
    return a * b

@mcp.tool()
async def fetch_url(url: str, timeout: int = 30) -> str:
    """URL에서 웹 페이지 내용을 가져옵니다.

    Args:
        url: 가져올 웹 페이지 URL
        timeout: 요청 타임아웃 (초, 기본값: 30)
    """
    import httpx
    async with httpx.AsyncClient() as client:
        response = await client.get(url, timeout=timeout)
        return response.text

# --- 리소스 등록 ---

@mcp.resource("config://app")
def get_config() -> str:
    """앱 설정을 반환합니다."""
    import json
    config = {"name": "My App", "version": "1.0", "debug": True}
    return json.dumps(config, indent=2)

@mcp.resource("status://server")
def get_status() -> str:
    """서버 상태를 반환합니다."""
    return "서버 정상 운영 중"

# --- 프롬프트 등록 ---

@mcp.prompt()
def review_prompt(code: str) -> str:
    """코드 리뷰 프롬프트를 생성합니다."""
    return f"다음 코드를 리뷰해주세요:\n\n```\n{code}\n```"

@mcp.prompt()
def debug_prompt(error: str, context: str = "") -> str:
    """디버깅 프롬프트를 생성합니다."""
    msg = f"다음 에러를 분석해주세요:\n\n{error}"
    if context:
        msg += f"\n\n상황: {context}"
    return msg

FastMCP 자동 스키마 생성 원리

FastMCP가 타입 힌트에서 어떻게 inputSchema를 생성하는지 비교합니다:

Python 타입 힌트에서 스키마 자동 생성
# FastMCP에서 이 함수를 등록하면:
@mcp.tool()
def search(
    query: str,
    max_results: int = 10,
    include_archived: bool = False
) -> list[str]:
    """데이터를 검색합니다.

    Args:
        query: 검색 쿼리 문자열
        max_results: 최대 결과 수
        include_archived: 보관된 항목 포함 여부
    """
    ...

# FastMCP가 자동 생성하는 inputSchema:
# {
#   "type": "object",
#   "properties": {
#     "query": {
#       "type": "string",
#       "description": "검색 쿼리 문자열"
#     },
#     "max_results": {
#       "type": "integer",
#       "default": 10,
#       "description": "최대 결과 수"
#     },
#     "include_archived": {
#       "type": "boolean",
#       "default": false,
#       "description": "보관된 항목 포함 여부"
#     }
#   },
#   "required": ["query"]
# }

FastMCP로 서버 실행

Bash
# 직접 실행 (stdio 모드)
fastmcp run server.py

# MCP Inspector로 테스트
fastmcp dev server.py

# Claude Desktop에 설치
fastmcp install server.py --name "My Fast Server"

저수준 SDK vs FastMCP 비교

항목 저수준 SDK (mcp) FastMCP
inputSchema 작성 JSON Schema 수동 작성 타입 힌트에서 자동 생성
도구 등록 list_tools + call_tool 분리 @mcp.tool() 데코레이터 하나
리소스 등록 list_resources + read_resource 분리 @mcp.resource(uri) 하나
서버 실행 코드 asyncio + stdio_server 설정 필요 fastmcp run 명령어
코드량 많음 (100~200줄) 적음 (30~50줄)
유연성 높음 (완전한 제어) 보통 (프레임워크 규칙 준수)
학습 난이도 중간~높음 낮음

inputSchema 작성 가이드

inputSchema는 MCP 도구의 입력 매개변수를 정의하는 JSON Schema입니다. AI 모델은 이 스키마를 읽고 올바른 형식의 인자를 생성합니다. 스키마를 명확하고 상세하게 작성할수록 AI의 도구 사용 정확도가 높아집니다.

기본 타입

JSON 기본 타입 예시
{
  "type": "object",
  "properties": {
    // 문자열 타입
    "name": {
      "type": "string",
      "description": "사용자 이름"
    },
    // 숫자 타입 (정수)
    "age": {
      "type": "integer",
      "description": "나이",
      "minimum": 0,
      "maximum": 150
    },
    // 숫자 타입 (실수)
    "score": {
      "type": "number",
      "description": "점수 (0.0 ~ 100.0)",
      "minimum": 0.0,
      "maximum": 100.0
    },
    // 불리언 타입
    "active": {
      "type": "boolean",
      "description": "활성화 여부",
      "default": true
    }
  },
  "required": ["name", "age"]
}

열거형(Enum) 제약

enum을 사용하면 허용되는 값을 제한할 수 있습니다. AI 모델이 정확한 값을 선택하도록 돕습니다.

JSON Enum 사용 예시
{
  "type": "object",
  "properties": {
    "language": {
      "type": "string",
      "enum": ["python", "javascript", "typescript", "rust", "go"],
      "description": "프로그래밍 언어"
    },
    "priority": {
      "type": "string",
      "enum": ["low", "medium", "high", "critical"],
      "description": "우선순위 수준",
      "default": "medium"
    },
    "format": {
      "type": "string",
      "enum": ["json", "csv", "xml", "yaml"],
      "description": "출력 형식"
    }
  },
  "required": ["language"]
}

중첩 객체

JSON 중첩 객체 스키마
{
  "type": "object",
  "properties": {
    "query": {
      "type": "string",
      "description": "검색 쿼리"
    },
    "filters": {
      "type": "object",
      "description": "검색 필터 조건",
      "properties": {
        "date_range": {
          "type": "object",
          "properties": {
            "start": {
              "type": "string",
              "description": "시작 날짜 (YYYY-MM-DD)"
            },
            "end": {
              "type": "string",
              "description": "종료 날짜 (YYYY-MM-DD)"
            }
          }
        },
        "category": {
          "type": "string",
          "description": "카테고리 필터"
        },
        "min_score": {
          "type": "number",
          "description": "최소 점수 필터",
          "minimum": 0
        }
      }
    },
    "options": {
      "type": "object",
      "description": "검색 옵션",
      "properties": {
        "page": {
          "type": "integer",
          "default": 1,
          "minimum": 1,
          "description": "페이지 번호"
        },
        "per_page": {
          "type": "integer",
          "default": 20,
          "minimum": 1,
          "maximum": 100,
          "description": "페이지당 결과 수"
        }
      }
    }
  },
  "required": ["query"]
}

배열 타입

JSON 배열 스키마 예시
{
  "type": "object",
  "properties": {
    // 문자열 배열
    "tags": {
      "type": "array",
      "items": {
        "type": "string"
      },
      "description": "태그 목록",
      "minItems": 1,
      "maxItems": 10
    },
    // 객체 배열
    "items": {
      "type": "array",
      "description": "항목 목록",
      "items": {
        "type": "object",
        "properties": {
          "name": {
            "type": "string",
            "description": "항목 이름"
          },
          "quantity": {
            "type": "integer",
            "minimum": 1,
            "description": "수량"
          }
        },
        "required": ["name", "quantity"]
      }
    },
    // 숫자 배열
    "scores": {
      "type": "array",
      "items": {
        "type": "number",
        "minimum": 0,
        "maximum": 100
      },
      "description": "점수 배열 (0~100)"
    }
  },
  "required": ["tags"]
}

description 작성 모범 사례

description 필드는 AI 모델이 매개변수를 이해하는 핵심 정보입니다. 명확하고 구체적으로 작성하세요.

나쁜 예 좋은 예 이유
"path" "파일 경로 (절대 경로, 예: /home/user/doc.txt)" 형식과 예시를 포함
"날짜" "시작 날짜 (YYYY-MM-DD 형식, 예: 2024-01-15)" 날짜 형식을 명시
"숫자" "페이지당 결과 수 (1~100, 기본값: 20)" 범위와 기본값을 명시
"타입" "파일 유형 필터 (예: 'image', 'document', 'all')" 허용값을 예시로 제공

복합 스키마 실전 예제

실제 프로덕션 MCP 서버에서 사용하는 복합적인 inputSchema 예제입니다:

JSON 데이터베이스 쿼리 도구 스키마
{
  "type": "object",
  "properties": {
    "table": {
      "type": "string",
      "description": "쿼리할 테이블명 (예: users, orders, products)"
    },
    "columns": {
      "type": "array",
      "items": { "type": "string" },
      "description": "조회할 컬럼 목록 (빈 배열이면 전체 컬럼)"
    },
    "where": {
      "type": "array",
      "description": "WHERE 조건 목록",
      "items": {
        "type": "object",
        "properties": {
          "column": { "type": "string", "description": "컬럼명" },
          "operator": {
            "type": "string",
            "enum": ["=", "!=", ">", "<", ">=", "<=", "LIKE", "IN"],
            "description": "비교 연산자"
          },
          "value": {
            "description": "비교 값 (문자열, 숫자, 또는 배열)"
          }
        },
        "required": ["column", "operator", "value"]
      }
    },
    "order_by": {
      "type": "object",
      "description": "정렬 조건",
      "properties": {
        "column": { "type": "string", "description": "정렬 컬럼" },
        "direction": {
          "type": "string",
          "enum": ["ASC", "DESC"],
          "default": "ASC",
          "description": "정렬 방향"
        }
      }
    },
    "limit": {
      "type": "integer",
      "default": 50,
      "minimum": 1,
      "maximum": 1000,
      "description": "최대 결과 수 (기본값: 50, 최대: 1000)"
    }
  },
  "required": ["table"]
}

MCP 서버 아키텍처 패턴

MCP 서버는 용도에 따라 다양한 아키텍처 패턴으로 설계할 수 있습니다. 아래에서 5가지 주요 패턴을 살펴보겠습니다.

MCP 서버 아키텍처 패턴 단일 도구 서버 Tool A Tool B Tools only 리소스 중심 서버 Resource A Resource B Resources only 하이브리드 서버 Tools Resources Prompts 프록시 서버 MCP Server External API 집계 서버 Source A Source B Source C AI Client (Claude) 패턴별 사용 시나리오 단일 도구 / 리소스 중심 - 단순한 유틸리티 기능 - 데이터 조회 전용 서버 - 빠른 프로토타이핑 하이브리드 (권장) - 도구 + 리소스 + 프롬프트 통합 - 완전한 기능을 제공하는 서버 - 프로덕션 환경에 적합 프록시 / 집계 - 외부 API 래핑 - 다중 데이터 소스 통합 - 엔터프라이즈 통합 권장: 하이브리드 패턴으로 시작하여 필요에 따라 확장

패턴 1: 단일 도구 서버

가장 단순한 형태로, 특정 기능을 수행하는 도구만 제공합니다. 계산기, 변환기, 단순 API 래퍼 등에 적합합니다.

Python 단일 도구 서버 예시
from fastmcp import FastMCP

mcp = FastMCP("Unit Converter")

@mcp.tool()
def celsius_to_fahrenheit(celsius: float) -> float:
    """섭씨를 화씨로 변환합니다."""
    return celsius * 9 / 5 + 32

@mcp.tool()
def kg_to_pounds(kg: float) -> float:
    """킬로그램을 파운드로 변환합니다."""
    return kg * 2.20462

@mcp.tool()
def km_to_miles(km: float) -> float:
    """킬로미터를 마일로 변환합니다."""
    return km * 0.621371

패턴 4: 프록시 서버 (외부 API 래핑)

외부 REST API를 MCP 도구로 래핑하여 AI가 사용할 수 있게 합니다. 인증, 에러 처리, 응답 형식 변환을 담당합니다.

Python 프록시 서버 예시
import os
import httpx
from fastmcp import FastMCP

mcp = FastMCP("GitHub API Proxy")
GITHUB_TOKEN = os.environ.get("GITHUB_TOKEN", "")

async def github_request(endpoint: str) -> dict:
    """GitHub API 요청 헬퍼"""
    async with httpx.AsyncClient() as client:
        response = await client.get(
            f"https://api.github.com{endpoint}",
            headers={
                "Authorization": f"token {GITHUB_TOKEN}",
                "Accept": "application/vnd.github.v3+json"
            }
        )
        response.raise_for_status()
        return response.json()

@mcp.tool()
async def list_repos(username: str, sort: str = "updated") -> str:
    """GitHub 사용자의 레포지토리 목록을 조회합니다.

    Args:
        username: GitHub 사용자명
        sort: 정렬 기준 (created, updated, pushed, full_name)
    """
    data = await github_request(
        f"/users/{username}/repos?sort={sort}"
    )
    repos = [{"name": r["name"], "stars": r["stargazers_count"],
              "description": r["description"]} for r in data[:10]]
    import json
    return json.dumps(repos, ensure_ascii=False, indent=2)

@mcp.tool()
async def get_repo_issues(
    owner: str,
    repo: str,
    state: str = "open"
) -> str:
    """레포지토리의 이슈 목록을 조회합니다.

    Args:
        owner: 레포지토리 소유자
        repo: 레포지토리 이름
        state: 이슈 상태 (open, closed, all)
    """
    data = await github_request(
        f"/repos/{owner}/{repo}/issues?state={state}"
    )
    issues = [{"number": i["number"], "title": i["title"],
               "state": i["state"]} for i in data[:20]]
    import json
    return json.dumps(issues, ensure_ascii=False, indent=2)

패턴 5: 집계 서버

여러 데이터 소스를 하나의 MCP 서버로 통합합니다. 대시보드, 모니터링, 종합 분석 등에 활용합니다.

Python 집계 서버 예시
from fastmcp import FastMCP
import json

mcp = FastMCP("System Monitor")

@mcp.tool()
async def system_overview() -> str:
    """시스템 전체 상태를 집계합니다.
    CPU, 메모리, 디스크 정보를 통합하여 반환합니다."""
    import psutil

    overview = {
        "cpu": {
            "percent": psutil.cpu_percent(interval=1),
            "count": psutil.cpu_count(),
        },
        "memory": {
            "total_gb": round(psutil.virtual_memory().total / (1024**3), 2),
            "used_percent": psutil.virtual_memory().percent,
        },
        "disk": {
            "total_gb": round(psutil.disk_usage("/").total / (1024**3), 2),
            "used_percent": psutil.disk_usage("/").percent,
        },
    }
    return json.dumps(overview, indent=2)

@mcp.resource("monitor://dashboard")
def dashboard_data() -> str:
    """대시보드용 집계 데이터를 반환합니다."""
    import psutil
    data = {
        "cpu_percent": psutil.cpu_percent(),
        "memory_percent": psutil.virtual_memory().percent,
        "disk_percent": psutil.disk_usage("/").percent,
        "process_count": len(psutil.pids()),
    }
    return json.dumps(data, indent=2)

MCP 서버 테스트 전략

MCP 서버의 안정성과 신뢰성을 보장하기 위한 체계적인 테스트 전략입니다. MCP Inspector를 이용한 대화형 테스트부터 CI/CD 자동화까지 다룹니다.

MCP Inspector로 대화형 테스트

MCP Inspector는 MCP 서버를 시각적으로 테스트할 수 있는 웹 기반 도구입니다. 등록된 도구, 리소스, 프롬프트를 즉시 확인하고 호출할 수 있습니다.

Bash Inspector 실행
# Python 서버 테스트
npx @modelcontextprotocol/inspector python -m my_mcp_server.server

# FastMCP 서버 테스트
fastmcp dev server.py

# TypeScript 서버 테스트
npx @modelcontextprotocol/inspector node dist/index.js

# 환경 변수 전달
GITHUB_TOKEN=xxx npx @modelcontextprotocol/inspector python server.py

# Inspector가 http://localhost:5173에서 실행됩니다
# - Tools 탭: 도구 목록 확인 및 호출 테스트
# - Resources 탭: 리소스 목록 확인 및 읽기 테스트
# - Prompts 탭: 프롬프트 목록 확인 및 렌더링 테스트

pytest를 사용한 단위 테스트

MCP 서버의 각 도구와 리소스를 단위 테스트하는 완전한 예제입니다.

Python tests/test_server.py
"""MCP 서버 단위 테스트"""
import pytest
import json
from unittest.mock import AsyncMock, patch
from mcp.types import TextContent

# 서버 모듈에서 핸들러를 가져옵니다
from my_mcp_server.tools import call_tool, list_tools

# --- 도구 목록 테스트 ---

@pytest.mark.asyncio
async def test_list_tools():
    """도구 목록이 올바르게 반환되는지 테스트"""
    tools = await list_tools()

    # 도구 수 확인
    assert len(tools) >= 2

    # 도구 이름 확인
    tool_names = [t.name for t in tools]
    assert "get_weather" in tool_names
    assert "calculate" in tool_names

    # inputSchema 검증
    calc_tool = next(t for t in tools if t.name == "calculate")
    schema = calc_tool.inputSchema
    assert schema["type"] == "object"
    assert "operation" in schema["properties"]
    assert "operation" in schema["required"]

# --- 계산 도구 테스트 ---

@pytest.mark.asyncio
async def test_calculate_add():
    """덧셈 연산 테스트"""
    result = await call_tool(
        "calculate",
        {"operation": "add", "a": 5, "b": 3}
    )
    assert len(result) == 1
    assert result[0].text == "8"

@pytest.mark.asyncio
async def test_calculate_divide_by_zero():
    """0으로 나누기 에러 처리 테스트"""
    result = await call_tool(
        "calculate",
        {"operation": "divide", "a": 10, "b": 0}
    )
    assert "0으로 나눌 수 없습니다" in result[0].text

@pytest.mark.asyncio
async def test_unknown_tool():
    """알 수 없는 도구 호출 시 에러 테스트"""
    with pytest.raises(ValueError, match="알 수 없는 도구"):
        await call_tool("nonexistent", {})

# --- 리소스 테스트 ---

@pytest.mark.asyncio
async def test_read_config_resource():
    """config 리소스 읽기 테스트"""
    from my_mcp_server.resources import read_resource
    result = await read_resource("config://app")
    config = json.loads(result)
    assert "app_name" in config
    assert "version" in config

@pytest.mark.asyncio
async def test_unknown_resource():
    """알 수 없는 리소스 URI 테스트"""
    from my_mcp_server.resources import read_resource
    with pytest.raises(ValueError):
        await read_resource("unknown://test")
Bash 테스트 실행
# 전체 테스트 실행
pytest tests/ -v

# 특정 테스트만 실행
pytest tests/test_server.py::test_calculate_add -v

# 커버리지 포함
pytest tests/ --cov=my_mcp_server --cov-report=html

통합 테스트 패턴

실제 MCP 프로토콜을 통해 서버를 테스트하는 통합 테스트 패턴입니다.

Python tests/test_integration.py
"""MCP 서버 통합 테스트"""
import pytest
import asyncio
from mcp import ClientSession, StdioServerParameters
from mcp.client.stdio import stdio_client

@pytest.fixture
async def mcp_client():
    """MCP 클라이언트를 생성하여 서버에 연결합니다."""
    server_params = StdioServerParameters(
        command="python",
        args=["-m", "my_mcp_server.server"],
    )

    async with stdio_client(server_params) as (read, write):
        async with ClientSession(read, write) as session:
            await session.initialize()
            yield session

@pytest.mark.asyncio
async def test_server_initialization(mcp_client):
    """서버 초기화 테스트"""
    # 세션이 정상적으로 초기화되면 성공
    assert mcp_client is not None

@pytest.mark.asyncio
async def test_list_tools_via_protocol(mcp_client):
    """프로토콜을 통한 도구 목록 조회 테스트"""
    result = await mcp_client.list_tools()
    assert len(result.tools) > 0

    tool_names = [t.name for t in result.tools]
    assert "calculate" in tool_names

@pytest.mark.asyncio
async def test_call_tool_via_protocol(mcp_client):
    """프로토콜을 통한 도구 호출 테스트"""
    result = await mcp_client.call_tool(
        "calculate",
        {"operation": "multiply", "a": 6, "b": 7}
    )
    assert result.content[0].text == "42"

@pytest.mark.asyncio
async def test_list_resources_via_protocol(mcp_client):
    """프로토콜을 통한 리소스 목록 조회 테스트"""
    result = await mcp_client.list_resources()
    assert len(result.resources) > 0

    uris = [r.uri for r in result.resources]
    assert "config://app" in uris

CI/CD 자동화 테스트

YAML .github/workflows/test.yml
name: MCP Server Tests

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

jobs:
  test:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        python-version: ["3.10", "3.11", "3.12"]

    steps:
    - uses: actions/checkout@v4

    - name: Set up Python
      uses: actions/setup-python@v5
      with:
        python-version: ${{ matrix.python-version }}

    - name: Install uv
      run: curl -LsSf https://astral.sh/uv/install.sh | sh

    - name: Install dependencies
      run: uv pip install -e ".[dev]"

    - name: Run unit tests
      run: pytest tests/ -v --tb=short

    - name: Run tests with coverage
      run: pytest tests/ --cov=my_mcp_server --cov-report=xml

    - name: Upload coverage
      uses: codecov/codecov-action@v3
      with:
        file: coverage.xml

MCP 서버 배포 가이드

개발한 MCP 서버를 다른 사용자가 쉽게 설치하고 사용할 수 있도록 패키징하고 배포하는 방법입니다.

pip/PyPI 패키지로 배포 (Python)

Python MCP 서버를 PyPI에 배포하면 pip install 또는 uvx 명령으로 설치할 수 있습니다.

TOML pyproject.toml (배포용)
[project]
name = "my-mcp-server"
version = "1.0.0"
description = "MCP 서버 - 유용한 도구 모음"
readme = "README.md"
license = { text = "MIT" }
requires-python = ">=3.10"
authors = [
    { name = "개발자", email = "dev@example.com" },
]
keywords = ["mcp", "ai", "claude", "tools"]
classifiers = [
    "Development Status :: 4 - Beta",
    "Programming Language :: Python :: 3",
    "License :: OSI Approved :: MIT License",
]
dependencies = [
    "mcp>=1.0.0",
    "httpx>=0.25.0",
]

[project.scripts]
my-mcp-server = "my_mcp_server.server:main"

[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
Bash PyPI 배포 과정
# 빌드 도구 설치
pip install build twine

# 패키지 빌드
python -m build

# 빌드 결과 확인 (dist/ 디렉토리)
ls dist/
# my_mcp_server-1.0.0-py3-none-any.whl
# my_mcp_server-1.0.0.tar.gz

# TestPyPI에 먼저 테스트 업로드
twine upload --repository testpypi dist/*

# 실제 PyPI에 업로드
twine upload dist/*

# 사용자는 이제 이렇게 설치할 수 있습니다
pip install my-mcp-server
# 또는
uvx my-mcp-server

npm 패키지로 배포 (TypeScript)

JSON package.json (배포용)
{
  "name": "@myorg/mcp-server",
  "version": "1.0.0",
  "description": "MCP 서버 - 유용한 도구 모음",
  "type": "module",
  "main": "dist/index.js",
  "types": "dist/index.d.ts",
  "bin": {
    "my-mcp-server": "dist/index.js"
  },
  "files": ["dist"],
  "scripts": {
    "build": "tsc",
    "prepublishOnly": "npm run build"
  },
  "keywords": ["mcp", "ai", "claude"],
  "license": "MIT"
}
Bash npm 배포 과정
# 빌드
npm run build

# dist/index.js 파일 상단에 shebang 추가
# #!/usr/bin/env node

# npm 로그인
npm login

# 배포
npm publish

# 스코프 패키지의 경우 public 접근 설정
npm publish --access public

# 사용자는 이렇게 실행할 수 있습니다
npx @myorg/mcp-server

Docker 이미지로 배포

Docker를 사용하면 환경 의존성 문제 없이 서버를 배포할 수 있습니다.

Dockerfile Dockerfile
FROM python:3.12-slim

WORKDIR /app

# 의존성 먼저 설치 (캐시 활용)
COPY pyproject.toml .
RUN pip install --no-cache-dir .

# 소스 코드 복사
COPY src/ src/

# MCP 서버는 stdio 통신이므로
# ENTRYPOINT로 서버 명령 설정
ENTRYPOINT ["python", "-m", "my_mcp_server.server"]
Bash Docker 빌드 및 사용
# 이미지 빌드
docker build -t my-mcp-server .

# Claude Desktop 설정에서 Docker로 실행
# claude_desktop_config.json:
# {
#   "mcpServers": {
#     "my-server": {
#       "command": "docker",
#       "args": ["run", "-i", "--rm", "my-mcp-server"]
#     }
#   }
# }
Docker 실행 시 주의사항: MCP 서버는 stdio(stdin/stdout)으로 통신하므로 Docker 실행 시 반드시 -i(interactive) 플래그를 사용해야 합니다. -t(tty) 플래그는 사용하지 마세요. TTY 모드는 바이너리 프로토콜 통신을 방해합니다.

Smithery 레지스트리 등록

Smithery는 MCP 서버 전용 레지스트리입니다. 등록하면 다른 사용자가 쉽게 검색하고 설치할 수 있습니다.

JSON smithery.json
{
  "name": "my-mcp-server",
  "description": "유용한 도구를 제공하는 MCP 서버",
  "version": "1.0.0",
  "runtime": "python",
  "entrypoint": "my_mcp_server.server",
  "capabilities": {
    "tools": true,
    "resources": true,
    "prompts": true
  },
  "author": "개발자",
  "license": "MIT",
  "repository": "https://github.com/user/my-mcp-server"
}
Bash Smithery 등록
# Smithery CLI 설치
npx @smithery/cli login

# 서버 등록
npx @smithery/cli publish

# 사용자가 Smithery에서 설치
npx @smithery/cli install my-mcp-server

MCP 서버 디버깅 가이드

MCP 서버 개발 중 발생하는 문제를 효율적으로 진단하고 해결하는 방법입니다.

MCP Inspector 사용법 상세

MCP Inspector는 서버 디버깅의 핵심 도구입니다. 웹 UI를 통해 서버와 직접 상호작용하며 문제를 진단합니다.

Bash Inspector 활용
# 기본 실행
npx @modelcontextprotocol/inspector python -m my_mcp_server.server

# 환경 변수 전달
DEBUG=true LOG_LEVEL=DEBUG \
  npx @modelcontextprotocol/inspector python -m my_mcp_server.server

# Inspector에서 확인할 항목:
# 1. Connection 탭: 서버 연결 상태, 초기화 메시지
# 2. Tools 탭:
#    - 등록된 도구 목록과 inputSchema 확인
#    - 각 도구를 직접 호출하고 결과 확인
#    - 잘못된 인자를 전달하여 에러 처리 검증
# 3. Resources 탭:
#    - 리소스 URI 목록 확인
#    - 각 리소스의 내용을 읽어보기
# 4. Prompts 탭:
#    - 프롬프트 템플릿 목록 확인
#    - 인자를 전달하여 렌더링 결과 미리보기

로그 레벨 설정

상황에 따라 적절한 로그 레벨을 설정하여 필요한 정보를 확인합니다.

Python 환경별 로깅 설정
import logging
import sys
import os

def configure_logging():
    """환경에 따른 로깅 설정"""
    level = os.environ.get("LOG_LEVEL", "INFO").upper()

    # 루트 로거 설정 (stderr로 출력)
    logging.basicConfig(
        level=getattr(logging, level),
        format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
        stream=sys.stderr,  # 반드시 stderr!
    )

    # 개발 모드: 상세 로깅
    if os.environ.get("DEBUG") == "true":
        logging.getLogger("my-mcp-server").setLevel(logging.DEBUG)
        logging.getLogger("mcp").setLevel(logging.DEBUG)

    # 프로덕션 모드: 경고 이상만
    if os.environ.get("ENV") == "production":
        logging.getLogger().setLevel(logging.WARNING)

# 도구에서 로깅 활용
logger = logging.getLogger("my-mcp-server.tools")

@server.call_tool()
async def call_tool(name: str, arguments: dict) -> list:
    logger.info(f"도구 호출: {name}")
    logger.debug(f"인자: {arguments}")

    try:
        result = await _execute_tool(name, arguments)
        logger.info(f"도구 {name} 실행 성공")
        logger.debug(f"결과: {result}")
        return result
    except Exception as e:
        logger.error(f"도구 {name} 실행 실패: {e}", exc_info=True)
        raise

프로토콜 메시지 덤프

MCP 서버와 클라이언트 간의 JSON-RPC 메시지를 직접 확인하여 프로토콜 수준의 문제를 진단합니다.

Python 프로토콜 메시지 로깅
import logging
import json
import sys

# MCP 내부 로거를 DEBUG로 설정하면
# 프로토콜 메시지를 볼 수 있습니다
logging.basicConfig(
    level=logging.DEBUG,
    stream=sys.stderr,
    format="%(name)s: %(message)s"
)

# MCP SDK의 내부 로거 활성화
logging.getLogger("mcp.server").setLevel(logging.DEBUG)
logging.getLogger("mcp.server.stdio").setLevel(logging.DEBUG)

# 출력 예시 (stderr):
# mcp.server: Received request: {"jsonrpc":"2.0","id":1,"method":"tools/list"}
# mcp.server: Sending response: {"jsonrpc":"2.0","id":1,"result":{...}}

일반적인 오류와 해결 방법

오류 원인 해결 방법
Connection refused 서버가 실행되지 않았거나 경로가 잘못됨 commandargs 경로를 절대 경로로 확인
Server disconnected 서버 프로세스가 비정상 종료됨 stderr 로그를 확인하여 크래시 원인 파악
Tool not found 도구가 등록되지 않았거나 이름 불일치 list_tools 응답에서 등록된 이름 확인
Invalid params inputSchema와 맞지 않는 인자 Inspector에서 스키마를 확인하고 필수 필드 검증
stdout에 로그 출력 print() 사용으로 프로토콜 오염 모든 로그를 logging(stderr)으로 변경
ModuleNotFoundError 패키지가 설치되지 않음 cwd와 가상 환경 경로 확인, uv run 사용
JSON decode error 서버 응답이 올바른 JSON이 아님 stdout에 비 JSON 텍스트가 출력되는지 확인
Timeout 도구 실행이 너무 오래 걸림 비동기 처리, 타임아웃 설정, 진행 상황 보고
Permission denied 파일/네트워크 접근 권한 부족 서버 실행 환경의 권한 확인, Docker 볼륨 마운트
Schema validation failed inputSchema가 잘못 정의됨 JSON Schema 유효성 검사기로 스키마 검증
디버깅 체크리스트:
  1. 서버를 단독으로 실행하여 시작 에러가 없는지 확인
  2. MCP Inspector로 도구/리소스 목록이 정상 반환되는지 확인
  3. Inspector에서 각 도구를 직접 호출하여 결과 확인
  4. 에러 상황(잘못된 인자, 없는 파일 등)에서의 응답 확인
  5. Claude Desktop 로그(~/Library/Logs/Claude/mcp*.log)에서 연결 문제 확인

FastMCP로 빠른 서버 개발

FastMCP는 MCP 서버 개발을 극적으로 간소화하는 고수준 프레임워크입니다. 데코레이터 기반 API로 보일러플레이트 코드 없이 서버를 구축할 수 있습니다.

FastMCP vs 저수준 SDK
  • FastMCP: 데코레이터 기반, 자동 스키마 생성, 빠른 프로토타이핑에 최적
  • 저수준 SDK: 세밀한 제어, 커스텀 전송 계층, 대규모 프로덕션 서버에 적합

FastMCP 설치

Bash
# pip 사용
pip install fastmcp

# uv 사용 (권장)
uv add fastmcp

# 개발 의존성 포함
uv add fastmcp[dev]

FastMCP 기본 서버

Python server.py
#!/usr/bin/env python3
"""FastMCP 기본 서버 예제"""

from fastmcp import FastMCP

# 서버 인스턴스 생성 — 이름만 지정하면 끝
mcp = FastMCP("my-server")

# 도구 등록: 데코레이터 + 타입 힌트 = 자동 inputSchema 생성
@mcp.tool()
def add(a: int, b: int) -> int:
    """두 정수를 더합니다."""
    return a + b

@mcp.tool()
def greet(name: str, language: str = "ko") -> str:
    """이름에 맞는 인사를 반환합니다.

    Args:
        name: 인사할 사람 이름
        language: 인사 언어 (ko, en, ja)
    """
    greetings = {
        "ko": f"안녕하세요, {name}님!",
        "en": f"Hello, {name}!",
        "ja": f"こんにちは、{name}さん!",
    }
    return greetings.get(language, greetings["ko"])

# 리소스 등록
@mcp.resource("config://app")
def get_config() -> str:
    """애플리케이션 설정을 반환합니다."""
    import json
    return json.dumps({"version": "2.0", "debug": False})

# 프롬프트 등록
@mcp.prompt()
def review_code(code: str, lang: str = "python") -> str:
    """코드 리뷰 프롬프트를 생성합니다."""
    return f"""다음 {lang} 코드를 리뷰해주세요:

```{lang}
{code}
```

품질, 보안, 성능 관점에서 분석해주세요."""

# 서버 실행
if __name__ == "__main__":
    mcp.run()
FastMCP의 자동 스키마 생성

Python 타입 힌트와 docstring에서 inputSchema가 자동 생성됩니다. 위의 greet 도구는 다음 JSON Schema를 자동으로 만듭니다:

{
  "type": "object",
  "properties": {
    "name": {"type": "string", "description": "인사할 사람 이름"},
    "language": {"type": "string", "default": "ko", "description": "인사 언어 (ko, en, ja)"}
  },
  "required": ["name"]
}

Context 객체 활용

FastMCP의 Context 객체는 로깅, 진행 상황 보고, 리소스 접근 등 런타임 기능을 제공합니다.

Python context_example.py
from fastmcp import FastMCP, Context

mcp = FastMCP("context-demo")

@mcp.tool()
async def process_files(
    directory: str,
    ctx: Context
) -> str:
    """디렉토리의 파일들을 처리합니다."""
    import os

    files = os.listdir(directory)
    total = len(files)

    # 로깅
    await ctx.info(f"처리할 파일 수: {total}")

    results = []
    for i, filename in enumerate(files):
        # 진행 상황 보고
        await ctx.report_progress(i, total)

        filepath = os.path.join(directory, filename)
        if os.path.isfile(filepath):
            size = os.path.getsize(filepath)
            results.append(f"{filename}: {size} bytes")

    await ctx.report_progress(total, total)
    return "\n".join(results)

Pydantic 모델과 FastMCP

Python pydantic_server.py
from pydantic import BaseModel, Field
from typing import Optional, List
from fastmcp import FastMCP

mcp = FastMCP("structured-server")

# Pydantic 모델로 복잡한 입력 정의
class SearchQuery(BaseModel):
    """파일 검색 쿼리"""
    directory: str = Field(description="검색 대상 디렉토리")
    pattern: str = Field(description="파일명 패턴 (glob)")
    max_results: int = Field(default=20, ge=1, le=100,
                             description="최대 결과 수")
    include_hidden: bool = Field(default=False,
                                description="숨김 파일 포함 여부")

class SearchResult(BaseModel):
    """검색 결과"""
    path: str
    size: int
    modified: float

@mcp.tool()
def search_files(query: SearchQuery) -> List[SearchResult]:
    """파일을 검색합니다. Pydantic 모델로 구조화된 입력을 받습니다."""
    import os, glob

    search_path = os.path.join(query.directory, query.pattern)
    files = glob.glob(search_path, recursive=True)

    results = []
    for f in files[:query.max_results]:
        if not query.include_hidden and os.path.basename(f).startswith("."):
            continue
        stat = os.stat(f)
        results.append(SearchResult(
            path=f, size=stat.st_size, modified=stat.st_mtime
        ))

    return results

동적 리소스 템플릿

Python
@mcp.resource("db://users/{user_id}")
def get_user(user_id: int) -> str:
    """사용자 정보를 반환합니다."""
    import json
    # 실제로는 DB에서 조회
    user = {"id": user_id, "name": "사용자", "role": "admin"}
    return json.dumps(user, ensure_ascii=False)

@mcp.resource("log://app/{date}")
def get_logs(date: str) -> str:
    """특정 날짜의 로그를 반환합니다."""
    log_path = f"/var/log/app/{date}.log"
    try:
        with open(log_path) as f:
            return f.read()
    except FileNotFoundError:
        return f"로그 없음: {date}"

inputSchema 설계 가이드

MCP 도구의 inputSchema는 AI 클라이언트가 도구를 올바르게 호출할 수 있도록 하는 JSON Schema입니다. 잘 설계된 스키마는 AI의 도구 선택 정확도와 매개변수 전달 품질을 크게 향상시킵니다.

기본 스키마 구조

JSON inputSchema 기본 형식
{
  "type": "object",
  "properties": {
    "query": {
      "type": "string",
      "description": "검색할 키워드 또는 구문"
    },
    "max_results": {
      "type": "integer",
      "description": "반환할 최대 결과 수",
      "default": 10,
      "minimum": 1,
      "maximum": 100
    },
    "format": {
      "type": "string",
      "description": "출력 형식",
      "enum": ["json", "text", "csv"]
    }
  },
  "required": ["query"]
}

JSON Schema 타입별 매핑

JSON Schema 타입 Python 타입 TypeScript 타입 사용 예시
"string" str string 파일 경로, 검색어, 이름
"integer" int number 개수, ID, 인덱스
"number" float number 좌표, 비율, 임계값
"boolean" bool boolean 플래그, 옵션 토글
"array" List[T] T[] 태그 목록, 경로 배열
"object" Dict[str, Any] Record<string, any> 설정 객체, 메타데이터

고급 스키마 패턴

TypeScript 고급 inputSchema 예제
// 중첩 객체, 배열, enum을 활용한 복잡한 스키마
const deployTool: Tool = {
  name: "deploy_service",
  description: "서비스를 지정된 환경에 배포합니다",
  inputSchema: {
    type: "object",
    properties: {
      service: {
        type: "string",
        description: "배포할 서비스 이름"
      },
      environment: {
        type: "string",
        enum: ["dev", "staging", "production"],
        description: "배포 대상 환경"
      },
      config: {
        type: "object",
        description: "배포 설정",
        properties: {
          replicas: {
            type: "integer",
            minimum: 1,
            maximum: 10,
            description: "인스턴스 수"
          },
          env_vars: {
            type: "object",
            additionalProperties: { type: "string" },
            description: "환경 변수 (키-값 쌍)"
          }
        }
      },
      tags: {
        type: "array",
        items: { type: "string" },
        description: "배포 태그 목록"
      },
      dry_run: {
        type: "boolean",
        default: false,
        description: "실제 배포 없이 검증만 수행"
      }
    },
    required: ["service", "environment"]
  }
};
inputSchema 설계 주의사항
  • description 필드를 모든 property에 작성하세요 -- AI가 매개변수 의미를 파악하는 핵심 단서입니다
  • required 배열에 필수 매개변수만 포함하세요 -- 선택 매개변수에는 default 값을 지정하세요
  • enum을 적극 활용하세요 -- 유효 값을 제한하면 AI가 올바른 값을 선택합니다
  • 도구의 description에 사용 시나리오를 명시하세요 -- AI가 언제 이 도구를 선택할지 판단합니다

MCP 서버 아키텍처 패턴

MCP 서버는 다양한 아키텍처 패턴으로 구성할 수 있습니다. 서버의 규모와 용도에 따라 적절한 패턴을 선택합니다.

서버 내부 구조

AI 클라이언트 (Claude, IDE) stdio/SSE MCP Server Protocol Handler initialize tools/list tools/call resources/read prompts/get Tool Registry inputSchema 검증 핸들러 디스패치 비즈니스 로직 도구 구현 리소스 제공 프롬프트 생성 에러 처리 외부 연동 파일시스템 데이터베이스 외부 API 시스템 명령 공통 미들웨어 인증 로깅 Rate Limit 입력 검증 에러 변환

모듈화 패턴

규모가 큰 서버는 도구를 모듈별로 분리하여 관리합니다.

Python modular_server.py
#!/usr/bin/env python3
"""모듈화된 MCP 서버 구조"""

from fastmcp import FastMCP

# 메인 서버
app = FastMCP("modular-server")

# ===== 파일 관련 도구 모듈 =====
import os
from pathlib import Path

ALLOWED_BASE = Path.home() / "Projects"

def validate_path(path: str) -> Path:
    """경로를 검증하고 절대 경로를 반환합니다."""
    resolved = Path(path).resolve()
    if not str(resolved).startswith(str(ALLOWED_BASE)):
        raise ValueError("허용되지 않은 경로입니다")
    return resolved

@app.tool()
def read_project_file(path: str) -> str:
    """프로젝트 파일을 읽습니다."""
    validated = validate_path(path)
    return validated.read_text(encoding="utf-8")

@app.tool()
def list_project_files(
    directory: str,
    extension: str = "*"
) -> list[str]:
    """프로젝트 디렉토리 내 파일을 나열합니다."""
    validated = validate_path(directory)
    pattern = f"*.{extension}" if extension != "*" else "*"
    return [str(f) for f in validated.glob(pattern)]

# ===== Git 관련 도구 모듈 =====
import subprocess

@app.tool()
def git_status(repo_path: str) -> str:
    """Git 저장소 상태를 반환합니다."""
    validated = validate_path(repo_path)
    result = subprocess.run(
        ["git", "status", "--porcelain"],
        cwd=str(validated),
        capture_output=True, text=True
    )
    return result.stdout or "작업 디렉토리가 깨끗합니다"

@app.tool()
def git_log(
    repo_path: str,
    count: int = 10
) -> str:
    """최근 Git 커밋 이력을 반환합니다."""
    validated = validate_path(repo_path)
    result = subprocess.run(
        ["git", "log", f"--oneline", f"-{count}"],
        cwd=str(validated),
        capture_output=True, text=True
    )
    return result.stdout

if __name__ == "__main__":
    app.run()

테스트 및 디버깅 심화

MCP 서버 개발에서 테스트와 디버깅은 특히 중요합니다. stdio 기반 통신이므로 일반적인 HTTP 서버와 다른 접근 방식이 필요합니다.

MCP Inspector 활용

MCP Inspector는 서버를 시각적으로 테스트할 수 있는 공식 개발 도구입니다.

Bash
# Python 서버 Inspector로 실행
npx @modelcontextprotocol/inspector python server.py

# FastMCP 서버
npx @modelcontextprotocol/inspector uv run server.py

# TypeScript 서버
npx @modelcontextprotocol/inspector node dist/index.js

# 환경 변수 전달
API_KEY=xxx npx @modelcontextprotocol/inspector python server.py
MCP Inspector 기능
  • 도구/리소스/프롬프트 목록 조회
  • 도구를 매개변수와 함께 직접 호출하고 결과 확인
  • JSON-RPC 메시지 로그 실시간 확인
  • 서버 능력(capabilities) 검사
  • http://localhost:5173에서 웹 UI 접근

단위 테스트 작성

Python pytest 기반 테스트

Python test_server.py
import pytest
import json
from unittest.mock import patch, MagicMock

# 서버 모듈에서 도구 함수를 직접 임포트하여 테스트
from server import read_project_file, list_project_files

class TestFileTools:
    """파일 관련 도구 테스트"""

    def test_read_file_success(self, tmp_path):
        """파일 읽기 성공 테스트"""
        test_file = tmp_path / "test.txt"
        test_file.write_text("테스트 내용")

        # 경로 검증을 임시로 우회
        with patch("server.validate_path",
                    return_value=test_file):
            result = read_project_file(str(test_file))

        assert result == "테스트 내용"

    def test_read_file_not_found(self):
        """존재하지 않는 파일 읽기 테스트"""
        with pytest.raises(FileNotFoundError):
            read_project_file("/nonexistent/file.txt")

    def test_path_traversal_blocked(self):
        """경로 탐색 공격 차단 테스트"""
        with pytest.raises(ValueError, match="허용되지 않은"):
            read_project_file("../../etc/passwd")

TypeScript Jest 테스트

TypeScript server.test.ts
import { describe, it, expect } from "@jest/globals";
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { StdioClientTransport } from
    "@modelcontextprotocol/sdk/client/stdio.js";

describe("MCP Server", () => {
  let client: Client;

  beforeAll(async () => {
    const transport = new StdioClientTransport({
      command: "node",
      args: ["dist/index.js"]
    });
    client = new Client(
      { name: "test-client", version: "1.0.0" },
      { capabilities: {} }
    );
    await client.connect(transport);
  });

  afterAll(async () => {
    await client.close();
  });

  it("도구 목록을 반환한다", async () => {
    const result = await client.listTools();
    expect(result.tools.length).toBeGreaterThan(0);
  });

  it("hello 도구를 호출한다", async () => {
    const result = await client.callTool({
      name: "hello",
      arguments: { name: "테스트" }
    });
    expect(result.content[0].text).toContain("테스트");
  });
});

디버깅 기법

stderr 로깅 패턴

MCP 서버는 stdout을 JSON-RPC 통신에 사용하므로, 디버그 출력은 반드시 stderr로 보내야 합니다.

Python debug_logging.py
import sys
import logging

# stderr로 로깅 설정 (stdout은 MCP 프로토콜 전용)
logging.basicConfig(
    level=logging.DEBUG,
    format="%(asctime)s [%(levelname)s] %(message)s",
    stream=sys.stderr  # 핵심: stderr로 출력
)
logger = logging.getLogger("mcp-server")

@mcp.tool()
def process_data(data: str) -> str:
    """데이터를 처리합니다."""
    logger.debug(f"입력 데이터: {data[:100]}...")

    try:
        result = do_processing(data)
        logger.info(f"처리 완료: {len(result)} bytes")
        return result
    except Exception as e:
        logger.error(f"처리 실패: {e}", exc_info=True)
        raise
stdout 사용 금지

MCP 서버에서 print()를 사용하면 stdout에 출력되어 JSON-RPC 통신이 깨집니다. 디버깅 시 반드시 logging 모듈로 stderr에 출력하거나, print(..., file=sys.stderr)를 사용하세요.

Claude Desktop 로그 확인

Bash
# macOS - Claude Desktop MCP 로그 위치
tail -f ~/Library/Logs/Claude/mcp*.log

# 특정 서버 로그만 필터링
tail -f ~/Library/Logs/Claude/mcp*.log | grep "my-server"

# Windows
# %APPDATA%\Claude\logs\mcp*.log

자주 발생하는 오류와 해결

오류 원인 해결 방법
Connection refused 서버 프로세스 시작 실패 명령어 경로 확인, 의존성 설치 확인
Invalid JSON stdout에 디버그 출력 혼입 print()logging으로 교체
Tool not found 도구 등록 누락 또는 이름 불일치 tools/list 응답 확인
Schema validation error 입력이 inputSchema와 불일치 스키마의 required, type 필드 확인
Timeout 도구 실행이 너무 오래 걸림 비동기 처리, 타임아웃 설정 추가
Permission denied 파일/디렉토리 접근 권한 부족 서버 실행 사용자 권한 확인

JSON-RPC 메시지 추적

Python debug_wrapper.py
#!/usr/bin/env python3
"""JSON-RPC 메시지를 로깅하는 디버그 래퍼"""

import sys
import json
import subprocess
import threading

def log_stream(name: str, stream, output):
    """스트림의 내용을 로깅하면서 전달합니다."""
    for line in stream:
        # stderr에 로깅
        print(
            f"[{name}] {line.rstrip()}",
            file=sys.stderr
        )
        # 원래 스트림으로 전달
        output.write(line)
        output.flush()

# 실제 서버를 서브프로세스로 실행
proc = subprocess.Popen(
    ["python", "server.py"],
    stdin=subprocess.PIPE,
    stdout=subprocess.PIPE,
    stderr=subprocess.PIPE,
    text=True
)

# stdin 로깅 (클라이언트 -> 서버)
t_in = threading.Thread(
    target=log_stream,
    args=("REQ", sys.stdin, proc.stdin)
)
# stdout 로깅 (서버 -> 클라이언트)
t_out = threading.Thread(
    target=log_stream,
    args=("RES", proc.stdout, sys.stdout)
)

t_in.start()
t_out.start()
t_in.join()
t_out.join()

핵심 정리

  • MCP 서버 개발의 핵심 개념과 흐름을 정리합니다.
  • Python 서버 개발를 단계별로 이해합니다.
  • 실전 적용 시 기준과 주의점을 확인합니다.

실무 팁

  • 입력/출력 예시를 고정해 재현성을 확보하세요.
  • MCP 서버 개발 범위를 작게 잡고 단계적으로 확장하세요.
  • Python 서버 개발 조건을 문서화해 대응 시간을 줄이세요.