Skip to content

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

✨ Learn with Quiz
|

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

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 지원

Build Your Own MCP Server: Protocol Internals, Hands-On Guide, and 100+ Ecosystem Deep Dive

1. Dissecting MCP Protocol Internals

MCP (Model Context Protocol) is a standard protocol for connecting AI models with external systems. In this post, we go beyond the introductory overview and deeply examine the internal workings of the protocol, then walk through building MCP servers from scratch.

1-1. JSON-RPC 2.0 Message Structure

All MCP communication follows the JSON-RPC 2.0 specification. Messages fall into three types.

Request: Sent when a client or server asks the other side to perform an operation.

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

Response: Returns the result for a request.

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

Notification: A one-way message that expects no response. Note the absence of an id field.

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

Error Response: Returned when request processing fails.

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

Key error codes:

CodeMeaningDescription
-32700Parse ErrorFailed to parse JSON
-32600Invalid RequestMalformed request
-32601Method Not FoundNon-existent method
-32602Invalid ParamsInvalid parameters
-32603Internal ErrorServer internal error

1-2. Connection Lifecycle

An MCP connection goes through clearly defined phases:

Phase 1 - Initialize

The client sends an initialize request, exchanging protocol versions and capabilities with the server.

{
  "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"
    }
  }
}

The server responds:

{
  "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

The client sends a notifications/initialized notification to declare initialization is complete.

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

Phase 3 - Normal Operation

Client and server can now freely exchange messages:

  • Client requests available tools via tools/list
  • Client invokes a specific tool via tools/call
  • Client reads resources via resources/read
  • Server notifies about tool list changes via notifications/tools/list_changed

Phase 4 - Shutdown

When the client terminates the connection, the transport layer cleans up.

1-3. Transport Methods

MCP supports two transport methods.

stdio (Standard Input/Output)

The most common method, running MCP server processes locally.

  • Client spawns the server process
  • Sends requests via stdin, receives responses via stdout
  • Each message is delimited by newlines
  • Debug logs go to stderr
  • Used primarily with Claude Desktop and Claude Code
Client                    Server Process
  |                           |
  |--- spawn process -------->|
  |                           |
  |--- stdin: JSON-RPC ------>|
  |<-- stdout: JSON-RPC -----|
  |                           |
  |--- stdin: JSON-RPC ------>|
  |<-- stdout: JSON-RPC -----|
  |                           |
  |--- kill process --------->|

Streamable HTTP

Used for connecting to remote servers. The March 2025 spec update replaced the older HTTP+SSE approach.

  • Requests sent via HTTP POST
  • Server streams responses via SSE (Server-Sent Events)
  • Suitable for remote deployment and multi-tenant scenarios
  • Uses Mcp-Session-Id header for session management
Client                    HTTP Server
  |                           |
  |--- POST /mcp ----------->|
  |    (initialize)           |
  |<-- SSE: result -----------|
  |<-- Mcp-Session-Id --------|
  |                           |
  |--- POST /mcp ----------->|
  |    (tools/call)           |
  |<-- SSE: result -----------|
  |                           |
  |--- DELETE /mcp ---------->|
  |    (session close)        |

Which Transport to Choose?

CriteriastdioStreamable HTTP
Use CaseLocal dev, desktop appsRemote servers, cloud deployment
Setup DifficultyVery easyModerate
SecurityLocal process isolationRequires OAuth 2.0, TLS
Multi-userNot possiblePossible
NetworkNot requiredRequired
ExampleClaude DesktopTeam-shared MCP server

1-4. Three Core Primitives

MCP servers expose three types of capabilities to AI.

Resources - Read-only Data

Provides data identified by URIs. This includes file contents, DB schemas, API responses, and more.

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

Response:

{
  "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));"
      }
    ]
  }
}

Characteristics:

  • Application uses as context (user-controlled)
  • URI-based identification (file://, db://, api://, etc.)
  • Can subscribe for change notifications
  • Supports text or binary (base64) content

Tools - Executable Functions

Defines functions that AI models can invoke. Input parameters are specified using JSON Schema.

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

Response:

{
  "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"]
        }
      }
    ]
  }
}

Characteristics:

  • Model autonomously invokes (model-controlled)
  • JSON Schema validates parameter types
  • May have side effects
  • May require user confirmation (security)

Prompts - Reusable Prompt Templates

Pre-defines frequently used prompt patterns.

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

Response:

{
  "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 Details

During initialization, client and server exchange their supported features. Servers only need to declare the primitives they intend to provide.

Server Capabilities Example (Tools only):

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

This server only provides Tools and can notify when the tool list changes. It does not provide Resources or Prompts.

Full-featured Server:

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

Client Capabilities Example:

{
  "capabilities": {
    "roots": { "listChanged": true },
    "sampling": {}
  }
}
  • roots: Client provides information about working directories/projects
  • sampling: Server can reverse-invoke the client's LLM

1-6. Sampling: Reverse LLM Invocation from Server

Sampling is a unique MCP feature that allows servers to send requests back to the client's AI model.

{
  "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" }]
    }
  }
}

Use cases:

  • Server asks AI to analyze collected log data
  • Server asks AI to summarize complex data
  • Intermediate judgment requests in agent workflows

Note that Sampling requires user confirmation for security. The client enforces human-in-the-loop control.


2. Building an MCP Server with Python FastMCP (Hands-On 1)

FastMCP is the easiest way to build MCP servers in Python. It is also the official high-level API for the MCP Python SDK.

2-1. Weather MCP Server (Simplest Example)

Installation:

pip install fastmcp

weather_server.py:

from fastmcp import FastMCP

# Create MCP server instance
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)
    """
    # In production, you would call a weather 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()

Running the server:

# Run directly (stdio mode)
python weather_server.py

# Or use FastMCP CLI
fastmcp run weather_server.py

# Test with MCP Inspector
fastmcp dev weather_server.py

Claude Desktop integration (claude_desktop_config.json):

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

Claude Code integration (.mcp.json):

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

2-2. Database MCP Server (Production-Ready)

Let's build a database MCP server, one of the most commonly used patterns in practice.

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
    """
    # Security: Only allow 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()

This server provides:

  • Resources: DB schema information and table listings
  • Tools: SELECT queries, INSERT, UPDATE, DELETE operations

Tell Claude "Show me the 10 most recently registered users" and it will automatically invoke the query_database tool.

2-3. Filesystem MCP Server

import os
from pathlib import Path
from fastmcp import FastMCP

mcp = FastMCP("Filesystem Server")

# Security: Only allow access to specific directories
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()

Key security points:

  • ALLOWED_DIRS restricts accessible directories
  • Path.resolve() prevents symlink bypass attacks
  • Parent directory traversal (../) is blocked

3. Building an MCP Server with TypeScript SDK (Hands-On 2)

TypeScript is the second most popular language for MCP server development. The official @modelcontextprotocol/sdk package is available.

3-1. Basic Structure

Installation:

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 (basic structure):

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

// Create server instance
const server = new McpServer({
  name: 'My MCP Server',
  version: '1.0.0',
})

// Register a 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.`,
        },
      ],
    }
  }
)

// Register a 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',
      },
    ],
  }
})

// Connect transport and run
async function main() {
  const transport = new StdioServerTransport()
  await server.connect(transport)
  console.error('MCP Server running on stdio')
}

main().catch(console.error)

Run:

npx tsc
node dist/index.js

Zod-based parameter validation is the key advantage of the TypeScript SDK. You get both type safety and automatic JSON Schema generation.

3-2. GitHub Issue MCP Server (Production)

Let's build a practical GitHub issue management MCP server.

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'

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

// Tool: List issues
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: Create issue
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: Close issue
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: Repository info
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: Issue triage
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 integration:

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

4. Testing and Debugging MCP Servers

4-1. MCP Inspector

MCP Inspector is the official tool for locally testing MCP servers.

# Run directly with npx
npx @modelcontextprotocol/inspector

# Run with a specific server
npx @modelcontextprotocol/inspector python weather_server.py

# FastMCP dev mode (auto-connects Inspector)
fastmcp dev weather_server.py

Inspector capabilities:

  • Browse all tools, resources, and prompts
  • Invoke tools and inspect results
  • View JSON-RPC message logs in real-time
  • Monitor connection status

4-2. Claude Desktop Integration

Configuration file location on macOS:

# Open config file
code ~/Library/Application\ Support/Claude/claude_desktop_config.json

Windows:

%APPDATA%\Claude\claude_desktop_config.json

Configuration example:

{
  "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 Integration

Create a .mcp.json file at the project root:

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

Or use global settings:

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

4-4. Logging Strategy

Debug logs in MCP servers should go to stderr. Stdout is reserved exclusively for JSON-RPC messages.

Python:

import sys
import logging

# Configure logging to 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:

// Log to 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. Error Handling Patterns

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."""
    # Input validation
    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:
        # Hide details for unexpected errors, return generic message
        logging.error(f"Unexpected error: {e}", exc_info=True)
        raise ToolError("An unexpected error occurred. Check server logs for details.")

5. Deploying MCP Servers

5-1. Cloudflare Workers Deployment

Cloudflare Workers lets you deploy MCP servers serverlessly across the globe.

# Create project
npm create cloudflare@latest -- my-mcp-server
cd my-mcp-server
npm install @modelcontextprotocol/sdk

src/index.ts (Workers version):

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',
    })

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

    // Handle Streamable HTTP transport
    // In practice, use the MCP SDK's HTTP transport handler
    return new Response('MCP Server Running', { status: 200 })
  },
}
# Deploy
npx wrangler deploy

5-2. Docker Containerization

FROM node:22-slim

WORKDIR /app

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

COPY dist/ ./dist/

# MCP server communicates via stdio
CMD ["node", "dist/index.js"]
docker build -t my-mcp-server .
docker run -i my-mcp-server

Connect Docker server from Claude Desktop:

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

5-3. npm/PyPI Package Distribution

npm package:

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

Users can instantly use it:

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

PyPI package:

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

5-4. Smithery Marketplace

Smithery (smithery.ai) is a dedicated MCP server registry. Registration process:

  1. Push MCP server code to a GitHub repository
  2. Add a smithery.yaml configuration file
  3. Connect the repo on the Smithery website
  4. Automatic build and deployment
# 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. Adding OAuth 2.0 Authentication

Authentication is essential for remote MCP servers. OAuth 2.0 support was formalized in the June 2025 spec.

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

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

// OAuth 2.0 middleware (Streamable HTTP transport)
async function validateToken(token: string): Promise<boolean> {
  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
}

// Authenticate in 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 })
  }

  // Continue processing MCP request...
  return new Response('OK')
}

6. 100+ MCP Server Ecosystem Roundup

As of March 2026, the MCP ecosystem is growing explosively. Here is a categorized overview of major servers.

6-1. Data and Analytics

ServerFeaturesUse Case
Grafana MCPDashboard queries, metric queries, alert managementAI-driven monitoring automation
PostgreSQL MCPSQL queries, schema exploration, data analysisNatural language to SQL
MongoDB MCPCollection CRUD, aggregation pipelinesNoSQL data exploration
ClickHouse MCPAnalytical queries, log retrievalLog-based troubleshooting
BigQuery MCPLarge-scale data analysisData team productivity
MySQL MCPSQL queries, table managementRDBMS operations
Redis MCPCache inspection, key-value managementCache debugging
Elasticsearch MCPFull-text search, index managementLog analysis automation
Snowflake MCPData warehouse queriesBusiness intelligence
DuckDB MCPLocal analytical queriesFast data exploration
Prometheus MCPMetric queries (PromQL)System monitoring
Datadog MCPMetrics, dashboards, alertsUnified observability

Grafana MCP Example:

User: "Find servers where CPU usage exceeded 80% in the last 24 hours"
Claude: [Calls Grafana MCP query_metrics tool]
        -> PromQL: avg by (instance)(cpu_usage_percent > 80)
        -> Results: server-03 (avg 87%), server-07 (avg 92%)

6-2. Developer Tools

ServerFeaturesUse Case
GitHub MCPIssues, PRs, Actions, code searchCode review automation
GitLab MCPPipelines, merge requestsCI/CD management
Jira MCPIssue tracking, sprint managementPM workflow automation
Linear MCPIssue/project managementModern issue tracker
Sentry MCPError tracking, performance monitoringDebugging automation
CircleCI MCPCI/CD pipeline managementBuild monitoring
npm MCPPackage search, version checksDependency management
Snyk MCPSecurity vulnerability scanningSecurity audit automation
SonarQube MCPCode quality analysisCode review assistance
Postman MCPAPI testing, collection managementAPI development efficiency

6-3. Documentation and Visualization

ServerFeaturesUse Case
Mermaid MCPDiagram generation (flowchart, sequence, ER)Documentation automation
PPT/Slides MCPPresentation creation/editingReport automation
Notion MCPPages, database CRUDKnowledge management
Google Docs MCPDocument read/editCollaborative documentation
Confluence MCPWiki page managementTechnical documentation
Excalidraw MCPWhiteboard diagramsArchitecture diagrams
Google Sheets MCPSpreadsheet read/editData reports
Obsidian MCPMarkdown note managementPersonal knowledge base

6-4. Communication

ServerFeaturesUse Case
Slack MCPChannel messages, search, DMsTeam communication automation
Discord MCPServer/channel managementCommunity management
Email MCPRead/send/search emailsEmail automation
Microsoft Teams MCPChat, meeting managementEnterprise collaboration
Telegram MCPBot messages, channel managementNotification automation
Twilio MCPSMS, voice callsCustomer communication

6-5. Cloud and Infrastructure

ServerFeaturesUse Case
AWS MCPS3, Lambda, EC2 managementInfrastructure automation
Kubernetes MCPCluster management, Pod inspectionK8s operations
Docker MCPContainer/image managementDev environment management
Terraform MCPIaC execution, state inspectionInfrastructure code generation
Vercel MCPDeployment, env vars, logsFrontend deployment
GCP MCPGoogle Cloud service managementGCP infrastructure automation
Azure MCPAzure service managementEnterprise cloud
Cloudflare MCPWorkers, DNS, CDNEdge computing management
Pulumi MCPIaC (programming languages)Code-based infrastructure
Ansible MCPConfiguration managementServer provisioning

6-6. AI and ML

ServerFeaturesUse Case
HuggingFace MCPModel search, inferenceML workflows
Weights and Biases MCPExperiment tracking, metricsML experiment management
LangChain MCPChain/agent executionAI pipelines
Ollama MCPLocal LLM managementPrivate AI execution
Replicate MCPCloud model executionML model deployment
Pinecone MCPVector DB managementRAG implementation
Weaviate MCPVector searchSemantic search
ChromaDB MCPLocal vector DBEmbedding management

6-7. Browser and Scraping

ServerFeaturesUse Case
Playwright MCPBrowser automation, screenshotsWeb testing
Puppeteer MCPChrome automationCrawling
Brave Search MCPWeb search APIReal-time information for AI
Firecrawl MCPWebsite crawling, markdown conversionRAG data collection
Browserbase MCPCloud browsersLarge-scale web automation
Exa MCPSemantic web searchHigh-quality search results
Tavily MCPAI-optimized searchResearch automation

6-8. Files and Storage

ServerFeaturesUse Case
Filesystem MCPLocal file read/writeFile management
Google Drive MCPFile search/downloadCloud file management
S3 MCPBucket/object managementCloud storage
Dropbox MCPFile sync, sharingFile collaboration
OneDrive MCPMicrosoft file managementEnterprise files
Box MCPEnterprise file managementEnterprise content management

6-9. Design and Media

ServerFeaturesUse Case
Figma MCPDesign file viewing, component extractionDesign-to-code conversion
Canva MCPDesign creation/editingMarketing materials
FFmpeg MCPAudio/video conversionMedia processing
Sharp MCPImage processingImage optimization

6-10. Specialized Servers

ServerFeaturesUse Case
Stripe MCPPayments, subscription managementPayment systems
Shopify MCPStore, product managementE-commerce
SendGrid MCPEmail deliveryMarketing emails
Airtable MCPSpreadsheet DBNo-code data management
Supabase MCPBaaS (DB, Auth, Storage)Full-stack development
Firebase MCPGoogle BaaSMobile/web backends
Neon MCPServerless PostgreSQLDatabase management
PlanetScale MCPServerless MySQLScalable DB
Turso MCPEdge SQLiteDistributed database

7. 10 Ideas for Your Own MCP Server

Building your own MCP server dramatically expands AI's usefulness. Here are practical ideas.

7-1. Personal Blog MCP

An MCP server for managing MDX-file-based blogs.

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()
            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."""
    # Implementation...
    return "Post created successfully"

7-2. Stock/Crypto Price MCP

Query real-time prices and analyze portfolios.

7-3. Calendar/Schedule MCP

Integrate with Google Calendar or Apple Calendar to manage schedules.

7-4. CI/CD Pipeline Monitoring MCP

Monitor GitHub Actions, Jenkins, CircleCI pipeline status.

7-5. Kubernetes Log Analysis MCP

Wrap kubectl to analyze Pod logs and detect anomalies.

7-6. Figma Design Extraction MCP

Extract design components via Figma API and convert to code.

7-7. Translation MCP

Provide multilingual translation via DeepL or Google Translate API.

7-8. Personal Notes/Wiki MCP

Let AI navigate and edit Obsidian or Logseq vaults.

7-9. Healthcare Data MCP

Analyze Apple Health or Fitbit data.

7-10. Smart Home IoT Control MCP

Control lights, temperature, and security systems via 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 Security Best Practices

MCP servers grant AI access to your systems, making security critically important.

8-1. Input Validation

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. Principle of Least Privilege

  • DB servers: Use read-only accounts (SELECT only)
  • File servers: Restrict allowed directories, resolve symlinks
  • API servers: Use tokens with only required scopes
  • Network: Block internal network access

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. Secret Management

import os

# Load secrets from environment variables (never hardcode)
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")

Pass environment variables in Claude Desktop config:

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

8-5. Audit Logging

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. Sandboxing

Use Docker or virtual environments to isolate MCP servers:

# 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 # Block external internet access

9. The Future of MCP: 2026 Outlook

9-1. Agent-to-Agent MCP Communication

Currently MCP focuses mainly on human(user)-AI tool connections. In 2026, scenarios where AI agents communicate directly with each other via MCP will expand.

For example:

  • A code review agent asks a security analysis agent to scan for vulnerabilities via MCP
  • A data pipeline agent asks a monitoring agent to check for anomalies
  • A customer support agent queries an order management agent for order status

9-2. Multimodal MCP

Current MCP is text-centric, but multimodal extensions for images, audio, and video are underway.

  • Image resources: Deliver screenshots, charts, and design files directly to AI
  • Audio processing: AI analyzes meeting recordings
  • Video analysis: AI monitors security camera footage

9-3. MCP Registry (Central Server Registry)

Like npm or PyPI, an official registry for searching and installing MCP servers is being developed. Smithery currently serves this role, but a more standardized registry is forthcoming.

9-4. Enterprise MCP Gateway

Enterprise environments need a central gateway for managing MCP servers:

  • Access control: Who can use which MCP servers
  • Audit logs: Record all tool invocations
  • Cost management: Rate limiting API calls
  • Security policies: Control access to sensitive data

9-5. Linux Foundation AAIF Governance

In late 2025, MCP was donated to the Linux Foundation's AI and AI Foundation (AAIF). This means MCP governance is transitioning from a single company (Anthropic) to an open community.

Expected benefits:

  • More transparent spec development process
  • Increased participation from diverse companies
  • Strengthened interoperability standards
  • Possible certification programs

10. Quiz

Let's test what you have learned.

Q1. What is MCP's underlying protocol, and what are the three message types?

Answer: MCP is based on JSON-RPC 2.0. The three message types are:

  1. Request: Has an id field, expects a response
  2. Response: Returns results for a request
  3. Notification: No id field, one-way message
Q2. What are MCP's two transport methods and their primary use cases?

Answer:

  1. stdio: Communicates via stdin/stdout of local processes. Used when running local servers from Claude Desktop or Claude Code.
  2. Streamable HTTP: Communicates via HTTP POST and SSE. Used for remote server deployment and multi-tenant environments.
Q3. How do the three core primitives (Resources, Tools, Prompts) differ?

Answer:

  • Resources: Read-only data via URIs. Used by applications as context. (User-controlled)
  • Tools: Executable functions invoked by AI models. Parameters defined by JSON Schema. May have side effects. (Model-controlled)
  • Prompts: Pre-defined reusable prompt templates. (User-controlled)
Q4. What is the simplest way to define an MCP tool in FastMCP?

Answer: Use the @mcp.tool() decorator. The function's docstring becomes the tool description, and type hints are automatically converted to 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. What are the 5 most important MCP server security principles?

Answer:

  1. Input validation: Validate all parameters with Zod/Pydantic
  2. Least privilege: Grant only necessary permissions (read-only DB accounts, etc.)
  3. Rate limiting: Limit API call frequency
  4. Secret management: Use environment variables, never hardcode
  5. Audit logging: Record all tool invocations

Additionally, sandboxing (Docker isolation) and OAuth 2.0 authentication are important.


11. References

  1. MCP Official Specification - Full MCP spec
  2. MCP Official Documentation - Getting started guide and tutorials
  3. MCP GitHub Repository - SDKs and reference servers
  4. FastMCP Documentation - Python FastMCP framework
  5. MCP TypeScript SDK - TypeScript SDK
  6. MCP Python SDK - Python SDK
  7. Awesome MCP Servers - Community MCP server list
  8. Smithery - MCP server registry
  9. MCP Inspector - MCP server testing tool
  10. Claude Desktop MCP Setup Guide - Claude Desktop integration
  11. Claude Code MCP Setup - Claude Code integration
  12. Grafana MCP Server - Official Grafana MCP server
  13. GitHub MCP Server - Official GitHub MCP server
  14. PostgreSQL MCP Server - PostgreSQL MCP
  15. Playwright MCP Server - Microsoft Playwright MCP
  16. Slack MCP Server - Slack MCP
  17. Filesystem MCP Server - Filesystem MCP
  18. Cloudflare MCP Workers - Cloudflare deployment guide
  19. MCP OAuth 2.0 Specification - Authentication spec
  20. Linux Foundation AAIF - MCP governance
  21. Anthropic MCP Blog - MCP announcement
  22. OpenAI MCP Support - OpenAI adopts MCP
  23. Google MCP Support - Google supports MCP