Skip to content
Published on

MCP 서버 직접 만들기: 원리부터 구축까지 — Grafana, Mermaid, PPT, DB 연동 MCP 생태계 완전 해부

Authors

1. MCP 프로토콜 내부 구조 해부

MCP(Model Context Protocol)는 AI 모델과 외부 시스템을 연결하는 표준 프로토콜입니다. 이번 글에서는 이전 MCP 개요 가이드에서 다루지 못했던 프로토콜 내부 동작 원리를 깊이 파헤치고, 직접 MCP 서버를 구축하는 방법을 알아봅니다.

1-1. JSON-RPC 2.0 기반 메시지 구조

MCP의 모든 통신은 JSON-RPC 2.0 사양을 따릅니다. 메시지는 크게 세 가지 유형으로 나뉩니다.

Request (요청): 클라이언트 또는 서버가 상대방에게 작업을 요청할 때 보냅니다.

{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/call",
  "params": {
    "name": "get_weather",
    "arguments": {
      "city": "Seoul"
    }
  }
}

Response (응답): 요청에 대한 결과를 반환합니다.

{
  "jsonrpc": "2.0",
  "id": 1,
  "result": {
    "content": [
      {
        "type": "text",
        "text": "Seoul: 15 degrees, partly cloudy"
      }
    ]
  }
}

Notification (알림): 응답을 기대하지 않는 단방향 메시지입니다. id 필드가 없습니다.

{
  "jsonrpc": "2.0",
  "method": "notifications/initialized"
}

Error Response (에러 응답): 요청 처리에 실패했을 때 반환합니다.

{
  "jsonrpc": "2.0",
  "id": 1,
  "error": {
    "code": -32602,
    "message": "Invalid params: city is required"
  }
}

주요 에러 코드는 다음과 같습니다:

코드의미설명
-32700Parse ErrorJSON 파싱 실패
-32600Invalid Request잘못된 요청 형식
-32601Method Not Found존재하지 않는 메서드
-32602Invalid Params잘못된 파라미터
-32603Internal Error서버 내부 오류

1-2. 연결 라이프사이클

MCP 연결은 명확한 단계를 거칩니다:

Phase 1 - Initialize (초기화)

클라이언트가 서버에 initialize 요청을 보내며, 양측의 프로토콜 버전과 기능(Capabilities)을 교환합니다.

{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "initialize",
  "params": {
    "protocolVersion": "2025-03-26",
    "capabilities": {
      "roots": {
        "listChanged": true
      },
      "sampling": {}
    },
    "clientInfo": {
      "name": "Claude Desktop",
      "version": "1.5.0"
    }
  }
}

서버가 응답합니다:

{
  "jsonrpc": "2.0",
  "id": 1,
  "result": {
    "protocolVersion": "2025-03-26",
    "capabilities": {
      "tools": {
        "listChanged": true
      },
      "resources": {
        "subscribe": true,
        "listChanged": true
      },
      "prompts": {
        "listChanged": true
      }
    },
    "serverInfo": {
      "name": "Weather MCP Server",
      "version": "1.0.0"
    }
  }
}

Phase 2 - Initialized (초기화 완료)

클라이언트가 notifications/initialized 알림을 보내 초기화 완료를 선언합니다.

{
  "jsonrpc": "2.0",
  "method": "notifications/initialized"
}

Phase 3 - Normal Operation (정상 운영)

이제 클라이언트와 서버가 자유롭게 메시지를 주고받습니다:

  • 클라이언트가 tools/list로 사용 가능한 도구 목록을 요청
  • 클라이언트가 tools/call로 특정 도구를 호출
  • 클라이언트가 resources/read로 리소스를 읽기
  • 서버가 notifications/tools/list_changed로 도구 목록 변경 알림

Phase 4 - Shutdown (종료)

클라이언트가 연결을 종료하면 Transport 레이어가 정리됩니다.

1-3. Transport 방식

MCP는 두 가지 Transport 방식을 지원합니다.

stdio (Standard Input/Output)

가장 일반적인 방식으로, 로컬에서 MCP 서버 프로세스를 직접 실행합니다.

  • 클라이언트가 서버 프로세스를 spawn
  • stdin으로 요청을 보내고 stdout으로 응답을 받음
  • 각 메시지는 newline으로 구분
  • 디버그 로그는 stderr로 출력
  • Claude Desktop, Claude Code에서 주로 사용
Client                    Server Process
  |                           |
  |--- spawn process -------->|
  |                           |
  |--- stdin: JSON-RPC ------>|
  |<-- stdout: JSON-RPC -----|
  |                           |
  |--- stdin: JSON-RPC ------>|
  |<-- stdout: JSON-RPC -----|
  |                           |
  |--- kill process --------->|

Streamable HTTP

원격 서버에 접속할 때 사용합니다. 2025년 3월 스펙 업데이트로 기존 HTTP+SSE 방식을 대체합니다.

  • HTTP POST로 요청을 보냄
  • 서버가 SSE(Server-Sent Events)로 스트리밍 응답
  • 원격 배포, 멀티 테넌트 시나리오에 적합
  • 세션 관리를 위한 Mcp-Session-Id 헤더 사용
Client                    HTTP Server
  |                           |
  |--- POST /mcp ----------->|
  |    (initialize)           |
  |<-- SSE: result -----------|
  |<-- Mcp-Session-Id --------|
  |                           |
  |--- POST /mcp ----------->|
  |    (tools/call)           |
  |<-- SSE: result -----------|
  |                           |
  |--- DELETE /mcp ---------->|
  |    (session close)        |

어떤 Transport를 선택할까?

기준stdioStreamable HTTP
사용 사례로컬 개발, 데스크톱 앱원격 서버, 클라우드 배포
설정 난이도매우 쉬움중간
보안로컬 프로세스 격리OAuth 2.0, TLS 필요
다중 사용자불가가능
네트워크불필요필요
대표 예시Claude Desktop팀 공유 MCP 서버

1-4. 세 가지 핵심 프리미티브

MCP 서버는 세 가지 유형의 기능을 AI에게 제공합니다.

Resources (리소스) - 읽기 전용 데이터

URI를 통해 식별되는 데이터를 제공합니다. 파일 내용, DB 스키마, API 응답 등이 해당합니다.

{
  "jsonrpc": "2.0",
  "id": 2,
  "method": "resources/read",
  "params": {
    "uri": "file:///project/schema.sql"
  }
}

응답:

{
  "jsonrpc": "2.0",
  "id": 2,
  "result": {
    "contents": [
      {
        "uri": "file:///project/schema.sql",
        "mimeType": "text/plain",
        "text": "CREATE TABLE users (id INT PRIMARY KEY, name VARCHAR(100));"
      }
    ]
  }
}

특징:

  • 애플리케이션이 컨텍스트로 활용 (사용자 제어)
  • URI 기반 식별 (file://, db://, api:// 등)
  • 구독을 통해 변경 알림 수신 가능
  • 텍스트 또는 바이너리(base64) 컨텐츠 지원

Tools (도구) - 실행 가능한 함수

AI 모델이 호출할 수 있는 함수를 정의합니다. JSON Schema로 입력 파라미터를 명시합니다.

{
  "jsonrpc": "2.0",
  "id": 3,
  "method": "tools/list"
}

응답:

{
  "jsonrpc": "2.0",
  "id": 3,
  "result": {
    "tools": [
      {
        "name": "get_weather",
        "description": "Get current weather for a city",
        "inputSchema": {
          "type": "object",
          "properties": {
            "city": {
              "type": "string",
              "description": "City name"
            }
          },
          "required": ["city"]
        }
      }
    ]
  }
}

특징:

  • 모델이 자율적으로 호출 (모델 제어)
  • JSON Schema로 파라미터 타입 검증
  • 부작용(side effect)을 가질 수 있음
  • 사용자 확인이 필요할 수 있음 (보안)

Prompts (프롬프트 템플릿) - 재사용 가능한 프롬프트

자주 사용하는 프롬프트 패턴을 미리 정의합니다.

{
  "jsonrpc": "2.0",
  "id": 4,
  "method": "prompts/get",
  "params": {
    "name": "code_review",
    "arguments": {
      "language": "python"
    }
  }
}

응답:

{
  "jsonrpc": "2.0",
  "id": 4,
  "result": {
    "description": "Code review prompt for Python",
    "messages": [
      {
        "role": "user",
        "content": {
          "type": "text",
          "text": "Please review the following Python code for best practices, bugs, and performance issues."
        }
      }
    ]
  }
}

1-5. Capability Negotiation 상세

초기화 시 클라이언트와 서버는 각자 지원하는 기능을 교환합니다. 서버는 제공하려는 프리미티브만 선언하면 됩니다.

서버 Capabilities 예시:

{
  "capabilities": {
    "tools": {
      "listChanged": true
    }
  }
}

위 서버는 Tools만 제공하며, 도구 목록이 변경되면 알림을 보낼 수 있습니다. Resources나 Prompts는 제공하지 않습니다.

전체 기능을 제공하는 서버:

{
  "capabilities": {
    "tools": { "listChanged": true },
    "resources": { "subscribe": true, "listChanged": true },
    "prompts": { "listChanged": true },
    "logging": {}
  }
}

클라이언트 Capabilities 예시:

{
  "capabilities": {
    "roots": { "listChanged": true },
    "sampling": {}
  }
}
  • roots: 클라이언트가 작업 중인 디렉토리/프로젝트 정보 제공
  • sampling: 서버가 클라이언트의 LLM을 역으로 호출할 수 있음

1-6. Sampling: 서버에서 LLM 역호출

Sampling은 MCP의 독특한 기능으로, 서버가 클라이언트의 AI 모델에게 역으로 요청을 보낼 수 있습니다.

{
  "jsonrpc": "2.0",
  "id": 10,
  "method": "sampling/createMessage",
  "params": {
    "messages": [
      {
        "role": "user",
        "content": {
          "type": "text",
          "text": "Summarize this error log: Connection timeout after 30s..."
        }
      }
    ],
    "maxTokens": 500,
    "modelPreferences": {
      "hints": [{ "name": "claude-sonnet-4-20250514" }]
    }
  }
}

활용 시나리오:

  • 서버가 수집한 로그 데이터를 AI에게 분석 요청
  • 복잡한 데이터를 AI에게 요약 요청
  • 에이전트 워크플로우에서 중간 판단 요청

단, Sampling은 보안상 사용자 확인이 필수입니다. 클라이언트가 Human-in-the-loop으로 통제합니다.


2. Python FastMCP로 MCP 서버 만들기 (실습 1)

FastMCP는 Python으로 MCP 서버를 가장 쉽게 만들 수 있는 프레임워크입니다. MCP Python SDK의 공식 고수준 API이기도 합니다.

2-1. 날씨 MCP 서버 (가장 간단한 예제)

설치:

pip install fastmcp

weather_server.py:

from fastmcp import FastMCP

# MCP 서버 인스턴스 생성
mcp = FastMCP("Weather Server")


@mcp.tool()
def get_weather(city: str) -> str:
    """Get current weather for a city.

    Args:
        city: Name of the city (e.g., Seoul, Tokyo, New York)
    """
    # 실제로는 날씨 API를 호출하겠지만 여기서는 간단히 시뮬레이션
    weather_data = {
        "Seoul": "15 degrees, partly cloudy",
        "Tokyo": "18 degrees, sunny",
        "New York": "10 degrees, rainy",
        "London": "8 degrees, foggy",
    }
    result = weather_data.get(city, f"Weather data not available for {city}")
    return f"Weather in {city}: {result}"


@mcp.tool()
def get_forecast(city: str, days: int = 3) -> str:
    """Get weather forecast for a city.

    Args:
        city: Name of the city
        days: Number of days to forecast (1-7)
    """
    if days < 1 or days > 7:
        return "Days must be between 1 and 7"
    return f"{days}-day forecast for {city}: Mostly sunny with temperatures 10-20C"


@mcp.resource("weather://cities")
def list_cities() -> str:
    """List all supported cities."""
    cities = ["Seoul", "Tokyo", "New York", "London", "Paris"]
    return "\n".join(cities)


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

실행 방법:

# 직접 실행 (stdio 모드)
python weather_server.py

# 또는 FastMCP CLI로 실행
fastmcp run weather_server.py

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

Claude Desktop 연동 (claude_desktop_config.json):

{
  "mcpServers": {
    "weather": {
      "command": "python",
      "args": ["/path/to/weather_server.py"]
    }
  }
}

Claude Code 연동 (.mcp.json):

{
  "mcpServers": {
    "weather": {
      "command": "python",
      "args": ["weather_server.py"]
    }
  }
}

2-2. 데이터베이스 MCP 서버 (실전)

실무에서 가장 많이 쓰이는 DB 연동 MCP 서버를 만들어 봅시다.

import sqlite3
import json
from fastmcp import FastMCP

mcp = FastMCP("Database Server")

DB_PATH = "myapp.db"


def get_connection():
    """Get database connection."""
    conn = sqlite3.connect(DB_PATH)
    conn.row_factory = sqlite3.Row
    return conn


@mcp.resource("db://schema")
def get_schema() -> str:
    """Get the database schema information."""
    conn = get_connection()
    cursor = conn.cursor()
    cursor.execute(
        "SELECT sql FROM sqlite_master WHERE type='table' ORDER BY name"
    )
    tables = cursor.fetchall()
    conn.close()
    schema_lines = []
    for table in tables:
        if table[0]:
            schema_lines.append(table[0])
    return "\n\n".join(schema_lines)


@mcp.resource("db://tables")
def list_tables() -> str:
    """List all tables in the database."""
    conn = get_connection()
    cursor = conn.cursor()
    cursor.execute(
        "SELECT name FROM sqlite_master WHERE type='table' ORDER BY name"
    )
    tables = [row[0] for row in cursor.fetchall()]
    conn.close()
    return json.dumps(tables, indent=2)


@mcp.tool()
def query_database(sql: str) -> str:
    """Execute a read-only SQL query and return results.

    Args:
        sql: SQL SELECT query to execute
    """
    # 보안: SELECT만 허용
    if not sql.strip().upper().startswith("SELECT"):
        return "Error: Only SELECT queries are allowed for safety"

    conn = get_connection()
    try:
        cursor = conn.cursor()
        cursor.execute(sql)
        rows = cursor.fetchall()
        if not rows:
            return "No results found"

        columns = [description[0] for description in cursor.description]
        results = []
        for row in rows:
            results.append(dict(zip(columns, row)))

        return json.dumps(results, indent=2, ensure_ascii=False)
    except Exception as e:
        return f"Query error: {str(e)}"
    finally:
        conn.close()


@mcp.tool()
def insert_record(table: str, data: str) -> str:
    """Insert a record into a table.

    Args:
        table: Table name
        data: JSON string of column-value pairs
    """
    try:
        record = json.loads(data)
    except json.JSONDecodeError:
        return "Error: Invalid JSON data"

    columns = ", ".join(record.keys())
    placeholders = ", ".join(["?" for _ in record])
    values = list(record.values())

    conn = get_connection()
    try:
        cursor = conn.cursor()
        cursor.execute(
            f"INSERT INTO {table} ({columns}) VALUES ({placeholders})",
            values,
        )
        conn.commit()
        return f"Successfully inserted record with id {cursor.lastrowid}"
    except Exception as e:
        return f"Insert error: {str(e)}"
    finally:
        conn.close()


@mcp.tool()
def update_record(table: str, record_id: int, data: str) -> str:
    """Update a record in a table.

    Args:
        table: Table name
        record_id: ID of the record to update
        data: JSON string of column-value pairs to update
    """
    try:
        updates = json.loads(data)
    except json.JSONDecodeError:
        return "Error: Invalid JSON data"

    set_clause = ", ".join([f"{k} = ?" for k in updates.keys()])
    values = list(updates.values()) + [record_id]

    conn = get_connection()
    try:
        cursor = conn.cursor()
        cursor.execute(
            f"UPDATE {table} SET {set_clause} WHERE id = ?", values
        )
        conn.commit()
        return f"Successfully updated {cursor.rowcount} record(s)"
    except Exception as e:
        return f"Update error: {str(e)}"
    finally:
        conn.close()


@mcp.tool()
def delete_record(table: str, record_id: int) -> str:
    """Delete a record from a table.

    Args:
        table: Table name
        record_id: ID of the record to delete
    """
    conn = get_connection()
    try:
        cursor = conn.cursor()
        cursor.execute(f"DELETE FROM {table} WHERE id = ?", [record_id])
        conn.commit()
        return f"Successfully deleted {cursor.rowcount} record(s)"
    except Exception as e:
        return f"Delete error: {str(e)}"
    finally:
        conn.close()


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

이 서버는 다음을 제공합니다:

  • Resources: DB 스키마 정보와 테이블 목록
  • Tools: SELECT 쿼리, INSERT, UPDATE, DELETE 작업

Claude에게 "users 테이블에서 최근 가입한 10명을 보여줘"라고 말하면 자동으로 query_database 도구를 호출합니다.

2-3. 파일 시스템 MCP 서버

import os
from pathlib import Path
from fastmcp import FastMCP

mcp = FastMCP("Filesystem Server")

# 보안: 허용된 디렉토리만 접근 가능
ALLOWED_DIRS = [
    Path("/Users/dev/projects"),
    Path("/Users/dev/documents"),
]


def is_path_allowed(path: str) -> bool:
    """Check if the path is within allowed directories."""
    resolved = Path(path).resolve()
    return any(
        resolved == allowed or allowed in resolved.parents
        for allowed in ALLOWED_DIRS
    )


@mcp.tool()
def read_file(path: str) -> str:
    """Read contents of a file.

    Args:
        path: Absolute file path
    """
    if not is_path_allowed(path):
        return "Error: Access denied. Path is outside allowed directories."

    try:
        with open(path, "r", encoding="utf-8") as f:
            return f.read()
    except FileNotFoundError:
        return f"Error: File not found: {path}"
    except UnicodeDecodeError:
        return "Error: File is not a text file"


@mcp.tool()
def write_file(path: str, content: str) -> str:
    """Write content to a file.

    Args:
        path: Absolute file path
        content: Content to write
    """
    if not is_path_allowed(path):
        return "Error: Access denied. Path is outside allowed directories."

    try:
        with open(path, "w", encoding="utf-8") as f:
            f.write(content)
        return f"Successfully wrote to {path}"
    except Exception as e:
        return f"Error writing file: {str(e)}"


@mcp.tool()
def list_directory(path: str) -> str:
    """List contents of a directory.

    Args:
        path: Absolute directory path
    """
    if not is_path_allowed(path):
        return "Error: Access denied. Path is outside allowed directories."

    try:
        entries = []
        for entry in sorted(Path(path).iterdir()):
            entry_type = "dir" if entry.is_dir() else "file"
            size = entry.stat().st_size if entry.is_file() else 0
            entries.append(f"[{entry_type}] {entry.name} ({size} bytes)")
        return "\n".join(entries)
    except FileNotFoundError:
        return f"Error: Directory not found: {path}"


@mcp.tool()
def search_files(directory: str, pattern: str) -> str:
    """Search for files matching a glob pattern.

    Args:
        directory: Directory to search in
        pattern: Glob pattern (e.g., '*.py', '**/*.md')
    """
    if not is_path_allowed(directory):
        return "Error: Access denied."

    matches = list(Path(directory).glob(pattern))
    if not matches:
        return "No files found matching the pattern"
    return "\n".join(str(m) for m in matches[:50])


@mcp.resource("fs://allowed-dirs")
def get_allowed_dirs() -> str:
    """List allowed directories."""
    return "\n".join(str(d) for d in ALLOWED_DIRS)


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

핵심 보안 포인트:

  • ALLOWED_DIRS로 접근 가능한 디렉토리를 제한
  • Path.resolve()로 심볼릭 링크 우회를 방지
  • 상위 디렉토리 탐색(../)을 차단

3. TypeScript SDK로 MCP 서버 만들기 (실습 2)

TypeScript는 MCP 서버 개발에서 Python 다음으로 많이 사용됩니다. 공식 @modelcontextprotocol/sdk 패키지가 제공됩니다.

3-1. 기본 구조

설치:

npm init -y
npm install @modelcontextprotocol/sdk zod
npm install -D typescript @types/node

tsconfig.json:

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

src/index.ts (기본 구조):

import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
import { z } from 'zod'

// 서버 인스턴스 생성
const server = new McpServer({
  name: 'My MCP Server',
  version: '1.0.0',
})

// Tool 등록
server.tool(
  'hello',
  'Say hello to someone',
  {
    name: z.string().describe('Name of the person'),
  },
  async ({ name }) => {
    return {
      content: [
        {
          type: 'text',
          text: `Hello, ${name}! Welcome to MCP.`,
        },
      ],
    }
  }
)

// Resource 등록
server.resource('info', 'server://info', async (uri) => {
  return {
    contents: [
      {
        uri: uri.href,
        mimeType: 'text/plain',
        text: 'This is My MCP Server v1.0.0',
      },
    ],
  }
})

// Transport 연결 및 실행
async function main() {
  const transport = new StdioServerTransport()
  await server.connect(transport)
  console.error('MCP Server running on stdio')
}

main().catch(console.error)

실행:

npx tsc
node dist/index.js

Zod를 사용한 파라미터 검증이 TypeScript SDK의 핵심입니다. 타입 안전성과 자동 JSON Schema 생성을 모두 얻습니다.

3-2. GitHub Issue MCP 서버 (실전)

실전에서 유용한 GitHub 이슈 관리 MCP 서버를 만들어 봅시다.

import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
import { z } from 'zod'

const server = new McpServer({
  name: 'GitHub Issue Server',
  version: '1.0.0',
})

const GITHUB_TOKEN = process.env.GITHUB_TOKEN
const BASE_URL = 'https://api.github.com'

// 공통 헤더
function getHeaders() {
  return {
    Authorization: `Bearer ${GITHUB_TOKEN}`,
    Accept: 'application/vnd.github.v3+json',
    'Content-Type': 'application/json',
    'User-Agent': 'MCP-GitHub-Server',
  }
}

// Tool: 이슈 목록 조회
server.tool(
  'list_issues',
  'List issues in a GitHub repository',
  {
    owner: z.string().describe('Repository owner'),
    repo: z.string().describe('Repository name'),
    state: z.enum(['open', 'closed', 'all']).default('open').describe('Issue state filter'),
    labels: z.string().optional().describe('Comma-separated label names'),
    per_page: z.number().min(1).max(100).default(10).describe('Results per page'),
  },
  async ({ owner, repo, state, labels, per_page }) => {
    const params = new URLSearchParams({
      state,
      per_page: String(per_page),
    })
    if (labels) params.set('labels', labels)

    const response = await fetch(`${BASE_URL}/repos/${owner}/${repo}/issues?${params}`, {
      headers: getHeaders(),
    })

    if (!response.ok) {
      return {
        content: [
          {
            type: 'text' as const,
            text: `Error: ${response.status} ${response.statusText}`,
          },
        ],
      }
    }

    const issues = await response.json()
    const formatted = issues.map(
      (issue: Record<string, unknown>) =>
        `#${issue.number} [${issue.state}] ${issue.title}\n  Labels: ${
          (issue.labels as Array<Record<string, string>>).map((l) => l.name).join(', ') || 'none'
        }\n  Created: ${issue.created_at}`
    )

    return {
      content: [
        {
          type: 'text' as const,
          text: formatted.join('\n\n') || 'No issues found',
        },
      ],
    }
  }
)

// Tool: 이슈 생성
server.tool(
  'create_issue',
  'Create a new issue in a GitHub repository',
  {
    owner: z.string().describe('Repository owner'),
    repo: z.string().describe('Repository name'),
    title: z.string().describe('Issue title'),
    body: z.string().optional().describe('Issue body (Markdown)'),
    labels: z.array(z.string()).optional().describe('Labels to add'),
    assignees: z.array(z.string()).optional().describe('Usernames to assign'),
  },
  async ({ owner, repo, title, body, labels, assignees }) => {
    const response = await fetch(`${BASE_URL}/repos/${owner}/${repo}/issues`, {
      method: 'POST',
      headers: getHeaders(),
      body: JSON.stringify({ title, body, labels, assignees }),
    })

    if (!response.ok) {
      const error = await response.text()
      return {
        content: [
          {
            type: 'text' as const,
            text: `Error creating issue: ${response.status} - ${error}`,
          },
        ],
      }
    }

    const issue = await response.json()
    return {
      content: [
        {
          type: 'text' as const,
          text: `Created issue #${issue.number}: ${issue.title}\nURL: ${issue.html_url}`,
        },
      ],
    }
  }
)

// Tool: 이슈 닫기
server.tool(
  'close_issue',
  'Close an issue in a GitHub repository',
  {
    owner: z.string().describe('Repository owner'),
    repo: z.string().describe('Repository name'),
    issue_number: z.number().describe('Issue number'),
    comment: z.string().optional().describe('Optional closing comment'),
  },
  async ({ owner, repo, issue_number, comment }) => {
    // 코멘트가 있으면 먼저 추가
    if (comment) {
      await fetch(`${BASE_URL}/repos/${owner}/${repo}/issues/${issue_number}/comments`, {
        method: 'POST',
        headers: getHeaders(),
        body: JSON.stringify({ body: comment }),
      })
    }

    const response = await fetch(`${BASE_URL}/repos/${owner}/${repo}/issues/${issue_number}`, {
      method: 'PATCH',
      headers: getHeaders(),
      body: JSON.stringify({ state: 'closed' }),
    })

    if (!response.ok) {
      return {
        content: [
          {
            type: 'text' as const,
            text: `Error closing issue: ${response.status}`,
          },
        ],
      }
    }

    return {
      content: [
        {
          type: 'text' as const,
          text: `Closed issue #${issue_number} successfully`,
        },
      ],
    }
  }
)

// Resource: 리포지토리 정보
server.resource('repo-info', 'github://repo-info', async (uri) => {
  return {
    contents: [
      {
        uri: uri.href,
        mimeType: 'text/plain',
        text: 'GitHub Issue MCP Server - Manage issues via AI',
      },
    ],
  }
})

// Prompt: 이슈 트리아지
server.prompt(
  'triage_issues',
  'Generate a prompt for triaging open issues',
  { owner: z.string(), repo: z.string() },
  ({ owner, repo }) => ({
    messages: [
      {
        role: 'user' as const,
        content: {
          type: 'text' as const,
          text: `Please triage the open issues in ${owner}/${repo}. For each issue:
1. Assess priority (critical/high/medium/low)
2. Suggest appropriate labels
3. Recommend if it should be assigned to someone
4. Provide a brief action plan`,
        },
      },
    ],
  })
)

async function main() {
  const transport = new StdioServerTransport()
  await server.connect(transport)
  console.error('GitHub Issue MCP Server running')
}

main().catch(console.error)

package.json:

{
  "name": "github-issue-mcp",
  "version": "1.0.0",
  "type": "module",
  "scripts": {
    "build": "tsc",
    "start": "node dist/index.js"
  },
  "dependencies": {
    "@modelcontextprotocol/sdk": "^1.12.0",
    "zod": "^3.24.0"
  },
  "devDependencies": {
    "typescript": "^5.7.0",
    "@types/node": "^22.0.0"
  }
}

Claude Desktop 연동:

{
  "mcpServers": {
    "github-issues": {
      "command": "node",
      "args": ["/path/to/github-issue-mcp/dist/index.js"],
      "env": {
        "GITHUB_TOKEN": "ghp_your_token_here"
      }
    }
  }
}

4. MCP 서버 테스트와 디버깅

4-1. MCP Inspector

MCP Inspector는 MCP 서버를 로컬에서 테스트할 수 있는 공식 도구입니다.

# npx로 바로 실행
npx @modelcontextprotocol/inspector

# 특정 서버를 지정하여 실행
npx @modelcontextprotocol/inspector python weather_server.py

# FastMCP의 dev 모드 (Inspector 자동 연결)
fastmcp dev weather_server.py

Inspector가 제공하는 기능:

  • 서버의 모든 도구, 리소스, 프롬프트 확인
  • 도구를 직접 호출하고 결과 확인
  • JSON-RPC 메시지 로그 실시간 확인
  • 연결 상태 모니터링

4-2. Claude Desktop에서 연동

macOS에서 Claude Desktop 설정 파일 위치:

# 설정 파일 열기
code ~/Library/Application\ Support/Claude/claude_desktop_config.json

Windows:

%APPDATA%\Claude\claude_desktop_config.json

설정 예시:

{
  "mcpServers": {
    "weather": {
      "command": "python",
      "args": ["/absolute/path/to/weather_server.py"]
    },
    "database": {
      "command": "python",
      "args": ["/absolute/path/to/db_server.py"],
      "env": {
        "DB_PATH": "/data/myapp.db"
      }
    },
    "github": {
      "command": "node",
      "args": ["/absolute/path/to/github-mcp/dist/index.js"],
      "env": {
        "GITHUB_TOKEN": "ghp_xxx"
      }
    }
  }
}

4-3. Claude Code에서 연동

프로젝트 루트에 .mcp.json 파일을 생성합니다:

{
  "mcpServers": {
    "my-server": {
      "command": "python",
      "args": ["./tools/my_mcp_server.py"]
    }
  }
}

또는 글로벌 설정:

claude mcp add my-server python ./tools/my_mcp_server.py

4-4. 로깅 전략

MCP 서버에서 디버그 로그는 stderr로 출력합니다. stdout은 JSON-RPC 메시지 전용이기 때문입니다.

Python:

import sys
import logging

# stderr로 로그 출력 설정
logging.basicConfig(
    level=logging.DEBUG,
    stream=sys.stderr,
    format="%(asctime)s [%(levelname)s] %(message)s",
)

logger = logging.getLogger("my-mcp-server")

@mcp.tool()
def my_tool(param: str) -> str:
    logger.debug(f"Tool called with param: {param}")
    try:
        result = do_something(param)
        logger.info(f"Tool succeeded: {result}")
        return result
    except Exception as e:
        logger.error(f"Tool failed: {e}")
        return f"Error: {str(e)}"

TypeScript:

// stderr로 로그 출력
function log(level: string, message: string) {
  console.error(`[${new Date().toISOString()}] [${level}] ${message}`)
}

server.tool('my_tool', 'Description', { param: z.string() }, async ({ param }) => {
  log('DEBUG', `Tool called with: ${param}`)
  try {
    const result = await doSomething(param)
    log('INFO', `Success: ${result}`)
    return { content: [{ type: 'text', text: result }] }
  } catch (error) {
    log('ERROR', `Failed: ${error}`)
    return { content: [{ type: 'text', text: `Error: ${error}` }] }
  }
})

4-5. 에러 핸들링 패턴

from fastmcp import FastMCP
from fastmcp.exceptions import ToolError

mcp = FastMCP("Robust Server")

@mcp.tool()
def safe_operation(param: str) -> str:
    """A tool with proper error handling."""
    # 입력 검증
    if not param or len(param) > 1000:
        raise ToolError("Parameter must be 1-1000 characters")

    try:
        result = external_api_call(param)
        return result
    except ConnectionError:
        raise ToolError("Failed to connect to external service. Please try again.")
    except TimeoutError:
        raise ToolError("Request timed out. The service may be temporarily unavailable.")
    except Exception as e:
        # 예상치 못한 에러는 상세 정보를 숨기고 일반 메시지만 반환
        logging.error(f"Unexpected error: {e}", exc_info=True)
        raise ToolError("An unexpected error occurred. Check server logs for details.")

5. MCP 서버 배포

5-1. Cloudflare Workers 배포

Cloudflare Workers를 사용하면 서버리스로 MCP 서버를 전 세계에 배포할 수 있습니다.

# 프로젝트 생성
npm create cloudflare@latest -- my-mcp-server
cd my-mcp-server
npm install @modelcontextprotocol/sdk

src/index.ts (Workers용):

import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'

export default {
  async fetch(request: Request): Promise<Response> {
    const server = new McpServer({
      name: 'Cloudflare MCP Server',
      version: '1.0.0',
    })

    // Tools 등록
    server.tool('hello', 'Say hello', { name: z.string() }, async ({ name }) => ({
      content: [{ type: 'text', text: `Hello ${name} from the edge!` }],
    }))

    // Streamable HTTP transport 처리
    // 실제로는 MCP SDK의 HTTP transport handler를 사용
    return new Response('MCP Server Running', { status: 200 })
  },
}
# 배포
npx wrangler deploy

5-2. Docker 컨테이너화

FROM node:22-slim

WORKDIR /app

COPY package*.json ./
RUN npm ci --production

COPY dist/ ./dist/

# MCP 서버는 stdio로 통신
CMD ["node", "dist/index.js"]
docker build -t my-mcp-server .
docker run -i my-mcp-server

Claude Desktop에서 Docker 서버 연결:

{
  "mcpServers": {
    "my-server": {
      "command": "docker",
      "args": ["run", "-i", "--rm", "my-mcp-server"]
    }
  }
}

5-3. npm/PyPI 패키지 배포

npm 패키지로 배포:

{
  "name": "@myorg/mcp-weather",
  "version": "1.0.0",
  "bin": {
    "mcp-weather": "./dist/index.js"
  }
}
npm publish --access public

사용자는 다음과 같이 바로 사용합니다:

{
  "mcpServers": {
    "weather": {
      "command": "npx",
      "args": ["-y", "@myorg/mcp-weather"]
    }
  }
}

PyPI 패키지로 배포:

pip install build twine
python -m build
twine upload dist/*
{
  "mcpServers": {
    "weather": {
      "command": "uvx",
      "args": ["mcp-weather"]
    }
  }
}

5-4. Smithery 마켓플레이스

Smithery(smithery.ai)는 MCP 서버 전용 레지스트리입니다. 등록 과정:

  1. GitHub 리포지토리에 MCP 서버 코드 푸시
  2. smithery.yaml 설정 파일 추가
  3. Smithery 웹사이트에서 리포 연결
  4. 자동 빌드 및 배포
# smithery.yaml
name: my-weather-mcp
description: Weather MCP server for AI assistants
icon: cloud-sun
startCommand:
  type: stdio
  configSchema:
    type: object
    properties:
      API_KEY:
        type: string
        description: Weather API key
    required:
      - API_KEY
  commandFunction:
    - node
    - dist/index.js

5-5. OAuth 2.0 인증 추가

원격 MCP 서버에는 인증이 필수입니다. 2025년 6월 스펙에서 OAuth 2.0 지원이 공식화되었습니다.

import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'

const server = new McpServer({
  name: 'Authenticated Server',
  version: '1.0.0',
})

// OAuth 2.0 미들웨어 (Streamable HTTP transport)
async function validateToken(token: string): Promise<boolean> {
  // OAuth provider에 토큰 검증 요청
  const response = await fetch('https://auth.example.com/introspect', {
    method: 'POST',
    headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
    body: `token=${token}`,
  })
  const data = await response.json()
  return data.active === true
}

// HTTP handler에서 인증 확인
async function handleRequest(req: Request): Promise<Response> {
  const authHeader = req.headers.get('Authorization')
  if (!authHeader || !authHeader.startsWith('Bearer ')) {
    return new Response('Unauthorized', { status: 401 })
  }

  const token = authHeader.slice(7)
  if (!(await validateToken(token))) {
    return new Response('Invalid token', { status: 403 })
  }

  // MCP 요청 처리 계속...
  return new Response('OK')
}

6. MCP 생태계 100+ 서버 총정리

2026년 3월 현재 MCP 생태계는 폭발적으로 성장하고 있습니다. 카테고리별로 주요 서버를 정리합니다.

6-1. 데이터와 분석

서버기능활용
Grafana MCP대시보드 조회, 메트릭 쿼리, 알람 관리AI로 모니터링 자동화
PostgreSQL MCPSQL 쿼리, 스키마 탐색, 데이터 분석자연어를 SQL로 변환
MongoDB MCP컬렉션 CRUD, 집계 파이프라인NoSQL 데이터 탐색
ClickHouse MCP분석 쿼리, 로그 조회로그 기반 문제 해결
BigQuery MCP대규모 데이터 분석데이터 팀 생산성 향상
MySQL MCPSQL 쿼리, 테이블 관리RDBMS 직접 조작
Redis MCP캐시 조회, 키-값 관리캐시 디버깅
Elasticsearch MCP전문 검색, 인덱스 관리로그 분석 자동화
Snowflake MCP데이터 웨어하우스 쿼리비즈니스 인텔리전스
DuckDB MCP로컬 분석 쿼리빠른 데이터 탐색
Prometheus MCP메트릭 쿼리(PromQL)시스템 모니터링
Datadog MCP메트릭, 대시보드, 알림 관리통합 관측성

Grafana MCP 활용 예시:

사용자: "지난 24시간 동안 CPU 사용률이 80%를 넘은 서버를 찾아줘"
Claude: [Grafana MCP의 query_metrics 도구 호출]
        → PromQL: avg by (instance)(cpu_usage_percent > 80)
        → 결과: server-03 (평균 87%), server-07 (평균 92%)

6-2. 개발 도구

서버기능활용
GitHub MCP이슈, PR, Actions, 코드 검색코드 리뷰 자동화
GitLab MCP파이프라인, 머지 리퀘스트CI/CD 관리
Jira MCP이슈 추적, 스프린트 관리PM 업무 자동화
Linear MCP이슈/프로젝트 관리모던 이슈 트래커 연동
Sentry MCP에러 추적, 성능 모니터링디버깅 자동화
CircleCI MCPCI/CD 파이프라인 관리빌드 모니터링
npm MCP패키지 검색, 버전 확인의존성 관리
Snyk MCP보안 취약점 스캔보안 감사 자동화
SonarQube MCP코드 품질 분석코드 리뷰 보조
Postman MCPAPI 테스트, 컬렉션 관리API 개발 효율화

6-3. 문서와 시각화

서버기능활용
Mermaid MCP다이어그램 생성 (flowchart, sequence, ER)문서 자동화
PPT/Slides MCP프레젠테이션 생성/편집보고서 자동화
Notion MCP페이지, 데이터베이스 CRUD지식 관리
Google Docs MCP문서 읽기/편집협업 문서 자동화
Confluence MCPWiki 페이지 관리기술 문서화
Excalidraw MCP화이트보드 다이어그램아키텍처 다이어그램
Google Sheets MCP스프레드시트 읽기/편집데이터 보고서
Obsidian MCP마크다운 노트 관리개인 지식 베이스

Mermaid MCP 활용 예시:

사용자: "현재 프로젝트의 아키텍처를 시퀀스 다이어그램으로 그려줘"
Claude: [Mermaid MCP의 create_diagram 도구 호출]
        → sequenceDiagram 자동 생성
        → SVG/PNG 파일로 저장

6-4. 커뮤니케이션

서버기능활용
Slack MCP채널 메시지, 검색, DM팀 커뮤니케이션 자동화
Discord MCP서버/채널 관리커뮤니티 관리
Email MCP이메일 읽기/전송/검색이메일 자동화
Microsoft Teams MCP채팅, 미팅 관리기업 협업
Telegram MCP봇 메시지, 채널 관리알림 자동화
Twilio MCPSMS, 음성 통화고객 커뮤니케이션

6-5. 클라우드와 인프라

서버기능활용
AWS MCPS3, Lambda, EC2 관리인프라 자동화
Kubernetes MCP클러스터 관리, Pod 조회K8s 운영 자동화
Docker MCP컨테이너/이미지 관리개발 환경 관리
Terraform MCPIaC 실행, 상태 조회인프라 코드 생성
Vercel MCP배포, 환경 변수, 로그프론트엔드 배포
GCP MCPGoogle Cloud 서비스 관리GCP 인프라 자동화
Azure MCPAzure 서비스 관리엔터프라이즈 클라우드
Cloudflare MCPWorkers, DNS, CDN엣지 컴퓨팅 관리
Pulumi MCPIaC (프로그래밍 언어)코드 기반 인프라
Ansible MCP구성 관리, 자동화서버 프로비저닝

6-6. AI와 ML

서버기능활용
HuggingFace MCP모델 검색, 추론ML 워크플로우
Weights and Biases MCP실험 추적, 메트릭ML 실험 관리
LangChain MCP체인/에이전트 실행AI 파이프라인
Ollama MCP로컬 LLM 관리프라이빗 AI 실행
Replicate MCP클라우드 모델 실행ML 모델 배포
Pinecone MCP벡터 DB 관리RAG 구축
Weaviate MCP벡터 검색시맨틱 검색
ChromaDB MCP로컬 벡터 DB임베딩 관리

6-7. 브라우저와 스크래핑

서버기능활용
Playwright MCP브라우저 자동화, 스크린샷웹 테스트
Puppeteer MCPChrome 자동화크롤링
Brave Search MCP웹 검색 APIAI에 실시간 정보 제공
Firecrawl MCP웹사이트 크롤링, 마크다운 변환RAG 데이터 수집
Browserbase MCP클라우드 브라우저대규모 웹 자동화
Exa MCP시맨틱 웹 검색고품질 검색 결과
Tavily MCPAI 최적화 검색리서치 자동화

6-8. 파일과 저장소

서버기능활용
Filesystem MCP로컬 파일 읽기/쓰기파일 관리
Google Drive MCP파일 검색/다운로드클라우드 파일 관리
S3 MCP버킷/객체 관리클라우드 스토리지
Dropbox MCP파일 동기화, 공유파일 협업
OneDrive MCPMicrosoft 파일 관리기업 파일 관리
Box MCP엔터프라이즈 파일 관리기업 콘텐츠 관리

6-9. 디자인과 미디어

서버기능활용
Figma MCP디자인 파일 조회, 컴포넌트 추출디자인-코드 변환
Canva MCP디자인 생성/편집마케팅 자료
FFmpeg MCP오디오/비디오 변환미디어 처리
Sharp MCP이미지 처리이미지 최적화

6-10. 기타 특화 서버

서버기능활용
Stripe MCP결제, 구독 관리결제 시스템
Shopify MCP스토어, 상품 관리이커머스
Twilio MCPSMS, 음성커뮤니케이션
SendGrid MCP이메일 발송마케팅 이메일
Airtable MCP스프레드시트 DB노코드 데이터 관리
Supabase MCPBaaS (DB, Auth, Storage)풀스택 개발
Firebase MCPGoogle BaaS모바일/웹 백엔드
Neon MCP서버리스 PostgreSQL데이터베이스 관리
PlanetScale MCP서버리스 MySQL스케일러블 DB
Turso MCP엣지 SQLite분산 데이터베이스

7. 나만의 MCP 서버 아이디어 10선

MCP 서버를 직접 만들어보면 AI의 활용 범위가 크게 넓어집니다. 다음은 실용적인 MCP 서버 아이디어입니다.

7-1. 개인 블로그 MCP

MDX 파일 기반 블로그를 관리하는 MCP 서버입니다.

from fastmcp import FastMCP
import glob
import yaml

mcp = FastMCP("Blog Manager")

BLOG_DIR = "/path/to/blog/data"

@mcp.tool()
def list_posts(tag: str = "") -> str:
    """List all blog posts, optionally filtered by tag."""
    posts = glob.glob(f"{BLOG_DIR}/**/*.mdx", recursive=True)
    results = []
    for post_path in sorted(posts, reverse=True):
        with open(post_path) as f:
            content = f.read()
            # frontmatter 파싱
            if content.startswith("---"):
                fm_end = content.index("---", 3)
                frontmatter = yaml.safe_load(content[3:fm_end])
                if tag and tag not in frontmatter.get("tags", []):
                    continue
                results.append(
                    f"- {frontmatter['title']} ({frontmatter['date']})"
                )
    return "\n".join(results[:20])

@mcp.tool()
def create_post(title: str, tags: str, content: str) -> str:
    """Create a new blog post."""
    # 구현...
    return "Post created successfully"

7-2. 주식/암호화폐 시세 MCP

실시간 시세를 조회하고 포트폴리오를 분석합니다.

7-3. 캘린더/일정 MCP

Google Calendar나 Apple Calendar와 연동하여 일정을 관리합니다.

7-4. CI/CD 파이프라인 모니터링 MCP

GitHub Actions, Jenkins, CircleCI 파이프라인 상태를 모니터링합니다.

7-5. 쿠버네티스 로그 분석 MCP

kubectl을 래핑하여 Pod 로그를 분석하고 이상 징후를 감지합니다.

7-6. Figma 디자인 추출 MCP

Figma API로 디자인 컴포넌트를 추출하고 코드로 변환합니다.

7-7. 번역 MCP

DeepL이나 Google Translate API로 다국어 번역을 제공합니다.

7-8. 개인 메모/위키 MCP

Obsidian이나 Logseq 볼트를 AI가 탐색하고 편집할 수 있게 합니다.

7-9. 헬스케어 데이터 MCP

Apple Health나 Fitbit 데이터를 분석합니다.

7-10. 스마트홈 IoT 제어 MCP

Home Assistant API로 조명, 온도, 보안 시스템을 제어합니다.

from fastmcp import FastMCP
import httpx

mcp = FastMCP("Smart Home")

HA_URL = "http://homeassistant.local:8123"
HA_TOKEN = "your_long_lived_token"

@mcp.tool()
def control_light(entity_id: str, action: str) -> str:
    """Control a smart light.

    Args:
        entity_id: Home Assistant entity ID (e.g., light.living_room)
        action: 'on' or 'off'
    """
    service = "turn_on" if action == "on" else "turn_off"
    response = httpx.post(
        f"{HA_URL}/api/services/light/{service}",
        headers={"Authorization": f"Bearer {HA_TOKEN}"},
        json={"entity_id": entity_id},
    )
    return f"Light {entity_id}: {action} (status: {response.status_code})"

@mcp.tool()
def get_temperature(entity_id: str) -> str:
    """Get temperature from a sensor."""
    response = httpx.get(
        f"{HA_URL}/api/states/{entity_id}",
        headers={"Authorization": f"Bearer {HA_TOKEN}"},
    )
    data = response.json()
    return f"Temperature: {data['state']}{data['attributes'].get('unit_of_measurement', '')}"

8. MCP 보안 베스트 프랙티스

MCP 서버는 AI에게 시스템 접근 권한을 부여하므로 보안이 매우 중요합니다.

8-1. 입력 검증

Python (Pydantic):

from pydantic import BaseModel, Field, validator

class QueryInput(BaseModel):
    sql: str = Field(..., max_length=5000)
    timeout: int = Field(default=30, ge=1, le=300)

    @validator("sql")
    def validate_sql(cls, v):
        forbidden = ["DROP", "DELETE", "TRUNCATE", "ALTER"]
        upper = v.upper()
        for keyword in forbidden:
            if keyword in upper:
                raise ValueError(f"Forbidden SQL keyword: {keyword}")
        return v

TypeScript (Zod):

const querySchema = z.object({
  sql: z
    .string()
    .max(5000)
    .refine(
      (sql) => {
        const forbidden = ['DROP', 'DELETE', 'TRUNCATE', 'ALTER']
        const upper = sql.toUpperCase()
        return !forbidden.some((kw) => upper.includes(kw))
      },
      { message: 'Forbidden SQL keyword detected' }
    ),
  timeout: z.number().min(1).max(300).default(30),
})

8-2. 최소 권한 원칙

  • DB 서버: 읽기 전용 계정 사용 (SELECT만 허용)
  • 파일 서버: 허용 디렉토리 제한, 심볼릭 링크 해결
  • API 서버: 필요한 스코프만 가진 토큰 사용
  • 네트워크: 내부 네트워크 접근 차단

8-3. Rate Limiting

import time
from collections import defaultdict

class RateLimiter:
    def __init__(self, max_calls: int = 10, window: int = 60):
        self.max_calls = max_calls
        self.window = window
        self.calls = defaultdict(list)

    def check(self, key: str) -> bool:
        now = time.time()
        self.calls[key] = [
            t for t in self.calls[key] if now - t < self.window
        ]
        if len(self.calls[key]) >= self.max_calls:
            return False
        self.calls[key].append(now)
        return True

limiter = RateLimiter(max_calls=20, window=60)

@mcp.tool()
def rate_limited_tool(param: str) -> str:
    if not limiter.check("default"):
        return "Rate limit exceeded. Please wait before trying again."
    return do_work(param)

8-4. 시크릿 관리

import os

# 환경 변수에서 시크릿 로드 (하드코딩 금지)
DB_PASSWORD = os.environ.get("DB_PASSWORD")
API_KEY = os.environ.get("API_KEY")

if not DB_PASSWORD or not API_KEY:
    raise RuntimeError("Required environment variables not set")

Claude Desktop 설정에서 환경 변수 전달:

{
  "mcpServers": {
    "my-server": {
      "command": "python",
      "args": ["server.py"],
      "env": {
        "DB_PASSWORD": "secret_from_keychain",
        "API_KEY": "sk-xxx"
      }
    }
  }
}

8-5. 감사 로깅

import json
import sys
from datetime import datetime

def audit_log(tool_name: str, params: dict, result: str, success: bool):
    log_entry = {
        "timestamp": datetime.utcnow().isoformat(),
        "tool": tool_name,
        "params": params,
        "success": success,
        "result_length": len(result),
    }
    print(json.dumps(log_entry), file=sys.stderr)

8-6. 샌드박싱

Docker나 가상환경을 사용하여 MCP 서버를 격리합니다:

# docker-compose.yml
services:
  mcp-server:
    build: .
    read_only: true
    security_opt:
      - no-new-privileges:true
    tmpfs:
      - /tmp:size=100M
    mem_limit: 512m
    cpus: '0.5'
    networks:
      - mcp-net
    volumes:
      - ./allowed-data:/data:ro

networks:
  mcp-net:
    driver: bridge
    internal: true # 외부 인터넷 접근 차단

9. MCP의 미래: 2026 전망

9-1. Agent-to-Agent MCP 통신

현재 MCP는 주로 사람(사용자)-AI 간의 도구 연결에 초점이 맞춰져 있습니다. 2026년에는 AI 에이전트끼리 MCP를 통해 직접 통신하는 시나리오가 확대될 것입니다.

예를 들어:

  • 코드 리뷰 에이전트가 보안 분석 에이전트에게 MCP를 통해 취약점 검사 요청
  • 데이터 파이프라인 에이전트가 모니터링 에이전트에게 이상 징후 확인 요청
  • 고객 지원 에이전트가 주문 관리 에이전트에게 주문 상태 조회

9-2. 멀티모달 MCP

현재 MCP는 텍스트 중심이지만, 이미지, 오디오, 비디오를 주고받는 멀티모달 확장이 진행 중입니다.

  • 이미지 리소스: 스크린샷, 차트, 디자인 파일을 AI에게 직접 전달
  • 오디오 처리: 회의 녹음을 AI가 분석
  • 비디오 분석: 보안 카메라 영상을 AI가 모니터링

9-3. MCP Registry (중앙 서버 레지스트리)

npm이나 PyPI처럼 MCP 서버를 중앙에서 검색하고 설치할 수 있는 공식 레지스트리가 만들어지고 있습니다. 현재는 Smithery가 이 역할을 하고 있지만, 더 표준화된 레지스트리가 등장할 예정입니다.

9-4. Enterprise MCP Gateway

기업 환경에서 MCP 서버를 중앙에서 관리하는 게이트웨이가 필요합니다:

  • 접근 제어: 누가 어떤 MCP 서버를 사용할 수 있는지
  • 감사 로그: 모든 도구 호출 기록
  • 비용 관리: API 호출 횟수 제한
  • 보안 정책: 민감한 데이터 접근 통제

9-5. Linux Foundation AAIF 거버넌스

2025년 말 MCP가 Linux Foundation의 AI and AI Foundation (AAIF)에 기부되었습니다. 이는 MCP의 거버넌스가 특정 회사(Anthropic)에서 오픈 커뮤니티로 이전됨을 의미합니다.

기대 효과:

  • 더 투명한 스펙 발전 과정
  • 다양한 기업의 참여 촉진
  • 상호운용성 표준 강화
  • 인증 프로그램 도입 가능성

10. 퀴즈

지금까지 배운 내용을 확인해 봅시다.

Q1. MCP의 기반 프로토콜은 무엇이며, 메시지의 세 가지 유형은?

정답: MCP는 JSON-RPC 2.0 기반입니다. 메시지 유형은 다음 세 가지입니다:

  1. Request: id가 있는 요청 (응답 기대)
  2. Response: 요청에 대한 결과 반환
  3. Notification: id가 없는 단방향 알림
Q2. MCP의 두 가지 Transport 방식과 각각의 주요 사용 사례는?

정답:

  1. stdio: 로컬 프로세스의 stdin/stdout으로 통신. Claude Desktop이나 Claude Code에서 로컬 서버를 실행할 때 사용.
  2. Streamable HTTP: HTTP POST와 SSE로 통신. 원격 서버 배포, 멀티 테넌트 환경에서 사용.
Q3. MCP의 세 가지 핵심 프리미티브(Resources, Tools, Prompts)의 차이점은?

정답:

  • Resources: URI 기반의 읽기 전용 데이터. 애플리케이션이 컨텍스트로 활용. (사용자 제어)
  • Tools: AI 모델이 호출하는 실행 가능한 함수. JSON Schema로 파라미터 정의. 부작용 가능. (모델 제어)
  • Prompts: 미리 정의된 재사용 가능한 프롬프트 템플릿. (사용자 제어)
Q4. FastMCP에서 MCP 도구(Tool)를 정의하는 가장 간단한 방법은?

정답: @mcp.tool() 데코레이터를 사용합니다. 함수의 docstring이 도구 설명이 되고, 타입 힌트가 JSON Schema로 자동 변환됩니다.

from fastmcp import FastMCP
mcp = FastMCP("My Server")

@mcp.tool()
def greet(name: str) -> str:
    """Greet someone by name."""
    return f"Hello, {name}!"
Q5. MCP 서버 보안에서 가장 중요한 5가지 원칙은?

정답:

  1. 입력 검증: Zod/Pydantic으로 모든 파라미터 검증
  2. 최소 권한: 필요한 권한만 부여 (읽기 전용 DB 계정 등)
  3. Rate Limiting: API 호출 횟수 제한
  4. 시크릿 관리: 환경 변수 사용, 하드코딩 금지
  5. 감사 로깅: 모든 도구 호출을 기록

추가로 샌드박싱(Docker 격리)과 OAuth 2.0 인증도 중요합니다.


11. 참고 자료

  1. MCP 공식 사양 - Model Context Protocol 전체 스펙
  2. MCP 공식 문서 - 시작 가이드, 튜토리얼
  3. MCP GitHub 리포지토리 - SDK, 레퍼런스 서버
  4. FastMCP 문서 - Python FastMCP 프레임워크
  5. MCP TypeScript SDK - TypeScript SDK
  6. MCP Python SDK - Python SDK
  7. Awesome MCP Servers - 커뮤니티 MCP 서버 목록
  8. Smithery - MCP 서버 레지스트리
  9. MCP Inspector - MCP 서버 테스트 도구
  10. Claude Desktop MCP 설정 가이드 - Claude Desktop 연동
  11. Claude Code MCP 설정 - Claude Code 연동
  12. Grafana MCP Server - Grafana 공식 MCP 서버
  13. GitHub MCP Server - GitHub 공식 MCP 서버
  14. PostgreSQL MCP Server - PostgreSQL MCP
  15. Playwright MCP Server - Microsoft Playwright MCP
  16. Slack MCP Server - Slack MCP
  17. Filesystem MCP Server - 파일시스템 MCP
  18. Cloudflare MCP Workers - Cloudflare 배포 가이드
  19. MCP OAuth 2.0 사양 - 인증 스펙
  20. Linux Foundation AAIF - MCP 거버넌스
  21. Anthropic MCP 블로그 - MCP 발표 블로그
  22. OpenAI MCP 지원 발표 - OpenAI의 MCP 채택
  23. Google MCP 지원 - Google의 MCP 지원