Claude Skill

mcp-ops

Model Context Protocol server development, tool design, resource handling, and transport configuration. Use for: mcp, model context protocol, mcp server, mcp tool, mcp resource, fastmcp, mcp transport, stdio, sse, streamable http, mcp inspector, tool handler, mcp prompt.

LLM Mart · 0 points · 0 views 0 listing impressions 0 install-command copies
Virus-scanned Reviewed automatically before listing.

Full trust report

Download 0xdarkmatter-claude-mods-skills_mcp-ops-3dfaf0b.zip · 48 KB
Part of 0xdarkmatter/claude-mods — 94 skills

Install

skills CLI npx skills add https://github.com/0xDarkMatter/claude-mods/tree/main/skills/mcp-ops
Claude Code claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install 0xdarkmatter-claude-mods@llmmart
Git git clone https://github.com/0xDarkMatter/claude-mods.git

The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole 0xdarkmatter/claude-mods collection as a plugin from our marketplace. Git is the plain clone.

Skill manifest

MCP Operations

Comprehensive patterns for building, testing, and deploying Model Context Protocol servers in Python and TypeScript.

Ecosystem facts verified as of 2026-07-05 (standalone FastMCP at major 3).

MCP Architecture Quick Reference

┌─────────────────────────────────────────────────────────┐
│                     MCP Host                            │
│  (Claude Desktop, Claude Code, Custom App)              │
│                                                         │
│  ┌───────────┐   ┌───────────┐   ┌───────────┐        │
│  │  Client A  │   │  Client B  │   │  Client C  │       │
│  └─────┬─────┘   └─────┬─────┘   └─────┬─────┘        │
└────────┼───────────────┼───────────────┼────────────────┘
         │               │               │
    ┌────┴────┐     ┌────┴────┐     ┌────┴────┐
    │Transport│     │Transport│     │Transport│
    │ (stdio) │     │  (SSE)  │     │ (HTTP)  │
    └────┬────┘     └────┬────┘     └────┬────┘
         │               │               │
┌────────┴──┐     ┌──────┴────┐   ┌──────┴────┐
│  Server A  │     │  Server B  │   │  Server C  │
│            │     │            │   │            │
│ ┌────────┐ │     │ ┌────────┐ │   │ ┌────────┐ │
│ │ Tools  │ │     │ │Resources│ │   │ │Prompts │ │
│ └────────┘ │     │ └────────┘ │   │ └────────┘ │
│ ┌────────┐ │     │ ┌────────┐ │   │ ┌────────┐ │
│ │Resources│ │     │ │Prompts │ │   │ │ Tools  │ │
│ └────────┘ │     │ └────────┘ │   │ └────────┘ │
└────────────┘     └────────────┘   └────────────┘

Protocol: JSON-RPC 2.0 over chosen transport
Flow:     Client → request → Server → response → Client

Server Type Decision Tree

What transport does your MCP server need?
│
├─ Local CLI tool / single-user desktop integration?
│  └─ stdio
│     - Simplest setup, no networking
│     - Claude Desktop, Claude Code native support
│     - Process lifecycle managed by host
│
├─ Web dashboard / browser-based client?
│  └─ SSE (Server-Sent Events)
│     - HTTP-based, works through firewalls
│     - Persistent connection for server→client events
│     - Good for development and internal tools
│
└─ Production API / multi-tenant / cloud deployment?
   └─ Streamable HTTP
      - HTTP POST for requests, SSE for streaming responses
      - Supports stateless and stateful modes
      - Full auth support, load balancer friendly
      - Recommended for production deployments

Tool vs Resource vs Prompt Decision Tree

What does the LLM need to do?
│
├─ Perform an action or computation?
│  └─ TOOL
│     - Has side effects (API calls, file writes, DB mutations)
│     - Accepts structured input, returns results
│     - Examples: run_query, create_issue, send_email
│
├─ Read data or context?
│  └─ RESOURCE
│     - Read-only data retrieval
│     - Identified by URI (file://, db://, api://)
│     - Examples: config://app, schema://users, file://readme.md
│
└─ Guide the LLM's behavior or workflow?
   └─ PROMPT
      - Templated instructions with arguments
      - Suggests conversation starters or workflows
      - Examples: code_review(language, file), summarize(topic)

Python SDK Quick Start

from mcp.server.fastmcp import FastMCP

mcp = FastMCP("my-server")

@mcp.tool()
def search_docs(query: str) -> str:
    """Search documentation by keyword."""
    results = perform_search(query)
    return "\n".join(f"- {r.title}: {r.snippet}" for r in results)

@mcp.tool()
def create_ticket(title: str, body: str, priority: str = "medium") -> str:
    """Create a support ticket."""
    ticket = api.create(title=title, body=body, priority=priority)
    return f"Created ticket #{ticket.id}: {ticket.url}"

@mcp.resource("config://app")
def get_config() -> str:
    """Return current application configuration."""
    return json.dumps(load_config(), indent=2)

@mcp.resource("schema://db/{table}")
def get_table_schema(table: str) -> str:
    """Return the schema for a database table."""
    return json.dumps(get_schema(table), indent=2)

@mcp.prompt()
def code_review(language: str, filepath: str) -> str:
    """Generate a code review prompt for the given file."""
    return f"Review this {language} code in {filepath} for bugs, style issues, and performance."

if __name__ == "__main__":
    mcp.run()  # Defaults to stdio transport

Install and run:

uv init my-mcp-server && cd my-mcp-server
uv add mcp[cli]
# Run with: uv run python server.py
# Or:       uv run mcp run server.py

Two Python FastMCPs — know which you're on. The official mcp SDK bundles a frozen 1.x-era FastMCP (from mcp.server.fastmcp import FastMCP, used in the samples above — stable, minimal). The standalone fastmcp package (gofastmcp.com) is where active development happens and is at major 3: same decorator surface, plus auth, proxying, OpenAPI generation, and a test client. To use it:

uv add fastmcp
from fastmcp import FastMCP   # standalone FastMCP 3 — not mcp.server.fastmcp

mcp = FastMCP("my-server")    # v3: constructor is identity/behaviour only;
                              # transport config moved to run()/serve time

FastMCP 3 breaking changes (from 2.x): 16 deprecated constructor kwargs removed (transport settings now passed at serve time), ui= replaced by app=, ctx.set_state()/ctx.get_state() are now async with session-scoped persistence, and the metadata namespace changed from _fastmcp to fastmcp.

TypeScript SDK Quick Start

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-server",
  version: "1.0.0",
});

// Register a tool
server.tool(
  "search_docs",
  "Search documentation by keyword",
  { query: z.string().describe("Search query") },
  async ({ query }) => {
    const results = await performSearch(query);
    return {
      content: [{ type: "text", text: results.join("\n") }],
    };
  }
);

// Register a resource
server.resource(
  "config",
  "config://app",
  { description: "Current application configuration" },
  async (uri) => ({
    contents: [{
      uri: uri.href,
      mimeType: "application/json",
      text: JSON.stringify(loadConfig(), null, 2),
    }],
  })
);

// Register a prompt
server.prompt(
  "code_review",
  "Generate a code review prompt",
  { language: z.string(), filepath: z.string() },
  async ({ language, filepath }) => ({
    messages: [{
      role: "user",
      content: {
        type: "text",
        text: `Review this ${language} code in ${filepath} for bugs and style issues.`,
      },
    }],
  })
);

async function main() {
  const transport = new StdioServerTransport();
  await server.connect(transport);
}
main().catch(console.error);

Install and run:

npm init -y
npm install @modelcontextprotocol/sdk zod
npx tsx server.ts

Transport Selection Matrix

Feature stdio SSE Streamable HTTP
Use case Local CLI tools, desktop Web dashboards, dev Production APIs
Protocol stdin/stdout pipes HTTP + EventSource HTTP POST + SSE
Auth support Env vars only Bearer tokens Full OAuth2/PKCE
Deployment Local process Single server Load balanced
Reconnection Process restart Auto-reconnect Stateless resilient
Multi-client 1:1 only Multiple clients Horizontally scalable
Firewall N/A (local) HTTP-friendly HTTP-friendly
State Process lifetime Connection lifetime Session or stateless
Best for Claude Desktop/Code Internal tools Cloud/enterprise

Authentication Patterns Quick Reference

# Pattern 1: API keys from environment
import os
from mcp.server.fastmcp import FastMCP

mcp = FastMCP("api-server")

@mcp.tool()
def call_api(endpoint: str) -> str:
    """Call external API with configured credentials."""
    api_key = os.environ["MY_API_KEY"]  # Set in client config
    resp = httpx.get(f"https://api.example.com/{endpoint}",
                     headers={"Authorization": f"Bearer {api_key}"})
    return resp.text
# Pattern 2: OAuth2 token refresh (in-memory cache)
import time

_token_cache: dict = {}

async def get_valid_token() -> str:
    if _token_cache.get("expires_at", 0) > time.time() + 60:
        return _token_cache["access_token"]
    resp = await httpx.AsyncClient().post("https://auth.example.com/token", data={
        "grant_type": "refresh_token",
        "refresh_token": os.environ["REFRESH_TOKEN"],
        "client_id": os.environ["CLIENT_ID"],
    })
    data = resp.json()
    _token_cache.update({
        "access_token": data["access_token"],
        "expires_at": time.time() + data["expires_in"],
    })
    return data["access_token"]
// Claude Desktop config with env vars
{
  "mcpServers": {
    "my-server": {
      "command": "uv",
      "args": ["run", "--directory", "/path/to/server", "python", "server.py"],
      "env": {
        "MY_API_KEY": "sk-...",
        "DATABASE_URL": "postgresql://..."
      }
    }
  }
}

Common Gotchas

Gotcha Why Fix
Tool not appearing in client inputSchema has invalid JSON Schema Validate schema with jsonschema library; use Pydantic/Zod to generate
Tool returns raw object Results must be content list with typed items Always return {"content": [{"type": "text", "text": "..."}]}
Timeout on long operations Default client timeout is often 30-60s Add progress notifications; break into smaller operations
Concurrent requests fail Tool handler uses shared mutable state Use asyncio locks, or make handlers stateless
Large response crashes client MCP messages have practical size limits Paginate results; return summaries with detail-fetch tools
Error swallowed silently Exception in handler returns generic error Set isError: true in response; include error message in content
SSE connection drops No keep-alive or reconnection logic Implement heartbeat; client auto-reconnects on SSE
Client ignores new tools Capabilities not updated after tool change Call server.request_context.session.send_resource_list_changed()
Tool name collision Two servers register same tool name Namespace tools: myserver_search not just search
Resource URI too generic data://info is ambiguous Use specific schemes: db://myapp/users, config://myapp/settings
async def missing on handler FastMCP tools can be sync or async, but I/O should be async Use async def for any handler doing network/file I/O
Server works locally, fails in Claude Desktop Different working directory or PATH Use absolute paths; log os.getcwd() on startup

Reference Files

File Lines Content
references/server-architecture.md ~700 Server lifecycle, FastMCP/TS SDK setup, capabilities, middleware, error handling
references/tool-handlers.md ~650 Schema design, validation, return types, composition, side effects, examples
references/resources-prompts.md ~550 Resource URIs, static/dynamic resources, templates, prompts, subscriptions
references/transport-auth.md ~550 stdio/SSE/HTTP transports, session management, OAuth2, rate limiting, TLS
references/testing-debugging.md ~550 MCP Inspector, unit/integration testing, protocol debugging, CI, performance

Staleness verifier

This skill encodes fast-moving facts (the MCP SDK package names + spec URL). scripts/check-mcp-facts.py guards them against silent drift:

# Structural (PR CI, no network): every catalogued package's prose_token is
# still named in this skill's prose, the spec URL is still cited, and the
# currency note still carries a year.
python scripts/check-mcp-facts.py --offline        # exit 0 consistent, 10 drift

# Live (freshness job, never blocks a PR): each SDK still resolves on
# npm/PyPI, no tracked major has moved off the sampled major, spec URL 200.
python scripts/check-mcp-facts.py --live            # exit 10 drift, 7 registries unreachable

The canonical fact set lives in assets/mcp-facts.json; when you add or drop a package, update it to match or --offline fails CI.

See Also

Files (claude-mods)
  • assets
    • .gitkeep 0 B · in bundle
    • mcp-facts.json 2.3 KB
      {
        "_comment": "Canonical fast-moving facts the mcp-ops skill encodes. scripts/check-mcp-facts.py asserts the skill prose still names these SDK packages + spec URL and still carries a dated currency note (--offline), and probes the npm/PyPI registries + the spec URL for drift (--live). Edit deliberately: a change here is a skill-content decision, not housekeeping.",
        "schema": "claude-mods.mcp-ops.facts/v1",
        "as_of": "2026-07-05",
        "spec": {
          "url": "https://modelcontextprotocol.io/specification/latest",
          "prose_token": "modelcontextprotocol.io/specification",
          "_comment": "The MCP specification URL cited in See Also (moved off the spec. subdomain, which stopped resolving by 2026-07). --offline asserts the token is named in the skill prose; --live expects the URL to answer HTTP 200 (after redirects)."
        },
        "packages": {
          "@modelcontextprotocol/sdk": {
            "registry": "npm",
            "role": "TypeScript SDK",
            "sampled_major": 1,
            "track_major": true,
            "prose_token": "@modelcontextprotocol/sdk",
            "_comment": "The official TS SDK. --live flags drift if the latest dist-tag leaves major 1 (a 2.x would be an API-break release warranting a skill review)."
          },
          "@modelcontextprotocol/inspector": {
            "registry": "npm",
            "role": "MCP Inspector (dev/debug UI)",
            "sampled_major": 0,
            "track_major": false,
            "prose_token": "@modelcontextprotocol/inspector",
            "_comment": "The interactive inspector invoked via `npx`. Only resolution is checked live (0.x versioning is chatty); a 404 means it was renamed/removed."
          },
          "mcp": {
            "registry": "pypi",
            "role": "Python SDK (FastMCP ships inside as mcp.server.fastmcp)",
            "sampled_major": 1,
            "track_major": true,
            "prose_token": "mcp[cli]",
            "_comment": "The official Python SDK on PyPI, installed via `uv add mcp[cli]`. --live flags drift if the latest release leaves major 1."
          },
          "fastmcp": {
            "registry": "pypi",
            "role": "FastMCP high-level framework (gofastmcp.com)",
            "sampled_major": 3,
            "track_major": true,
            "prose_token": "fastmcp",
            "_comment": "The standalone FastMCP package, documented at major 3 (constructor identity-only, async ctx state, fastmcp meta namespace). Named in prose as FastMCP and via gofastmcp.com; matched case-insensitively."
          }
        }
      }
      
  • references
    • resources-prompts.md 16.5 KB
      # Resources and Prompts
      
      ## Resource Overview
      
      Resources provide **read-only data** to the LLM. They are identified by URIs and can be static or dynamic.
      
      ```
      Resource URI format:  scheme://authority/path
      Examples:
        file:///workspace/readme.md
        db://myapp/users
        config://settings
        api://github/repos/user/repo
      ```
      
      ## Resource URIs
      
      ### URI Scheme Design
      
      | Scheme | Use Case | Example |
      |--------|----------|---------|
      | `file://` | Local filesystem | `file:///workspace/src/main.py` |
      | `db://` | Database objects | `db://myapp/tables/users` |
      | `config://` | Configuration | `config://app/settings` |
      | `api://` | External API data | `api://github/repos` |
      | `schema://` | Schema definitions | `schema://db/users` |
      | `log://` | Log files/entries | `log://app/errors/today` |
      | `docs://` | Documentation | `docs://api/endpoints` |
      
      ### URI Templates
      
      URI templates allow parameterized resources:
      
      ```python
      from mcp.server.fastmcp import FastMCP
      
      mcp = FastMCP("resource-server")
      
      # Static URI - single resource
      @mcp.resource("config://app/settings")
      def get_settings() -> str:
          """Return application settings."""
          return json.dumps(load_settings(), indent=2)
      
      # URI template - parameterized resource
      @mcp.resource("db://app/tables/{table_name}")
      def get_table_data(table_name: str) -> str:
          """Return data from a database table."""
          if table_name not in ALLOWED_TABLES:
              raise ValueError(f"Table {table_name} not accessible")
          rows = db.query(f"SELECT * FROM {table_name} LIMIT 100")
          return json.dumps(rows, default=str, indent=2)
      
      # Template with multiple parameters
      @mcp.resource("api://github/{owner}/{repo}/info")
      def get_repo_info(owner: str, repo: str) -> str:
          """Return GitHub repository information."""
          resp = httpx.get(f"https://api.github.com/repos/{owner}/{repo}")
          return resp.text
      ```
      
      ### TypeScript Resources
      
      ```typescript
      // Static resource
      server.resource("settings", "config://app/settings", async (uri) => ({
        contents: [{
          uri: uri.href,
          mimeType: "application/json",
          text: JSON.stringify(loadSettings(), null, 2),
        }],
      }));
      
      // Resource template
      server.resource(
        "table_data",
        new ResourceTemplate("db://app/tables/{table_name}", { list: undefined }),
        async (uri, { table_name }) => ({
          contents: [{
            uri: uri.href,
            mimeType: "application/json",
            text: JSON.stringify(await db.query(`SELECT * FROM ${table_name} LIMIT 100`)),
          }],
        })
      );
      ```
      
      ## Static Resources
      
      Resources backed by files, configs, or constant data:
      
      ```python
      import os
      import json
      
      @mcp.resource("config://app/environment")
      def get_environment() -> str:
          """Return current environment configuration."""
          return json.dumps({
              "node_env": os.environ.get("NODE_ENV", "development"),
              "debug": os.environ.get("DEBUG", "false"),
              "version": "1.0.0",
          }, indent=2)
      
      @mcp.resource("docs://api/openapi")
      def get_openapi_spec() -> str:
          """Return the OpenAPI specification."""
          with open("openapi.yaml") as f:
              return f.read()
      
      @mcp.resource("schema://database/migrations")
      def get_migration_status() -> str:
          """Return database migration status."""
          migrations = get_applied_migrations()
          pending = get_pending_migrations()
          return json.dumps({
              "applied": [m.name for m in migrations],
              "pending": [m.name for m in pending],
              "current_version": migrations[-1].version if migrations else "none",
          }, indent=2)
      ```
      
      ## Dynamic Resources
      
      Resources that fetch data on-demand from external sources:
      
      ```python
      import httpx
      
      @mcp.resource("api://weather/{city}")
      async def get_weather(city: str) -> str:
          """Return current weather for a city."""
          async with httpx.AsyncClient() as client:
              resp = await client.get(
                  "https://api.weather.example.com/current",
                  params={"city": city},
                  timeout=10.0,
              )
              return resp.text
      
      @mcp.resource("db://app/stats")
      def get_app_stats() -> str:
          """Return application statistics."""
          stats = {
              "total_users": db.count("users"),
              "active_sessions": db.count("sessions", {"active": True}),
              "requests_today": db.count("requests", {"date": today()}),
              "error_rate": db.query_scalar(
                  "SELECT COUNT(*) FILTER (WHERE status >= 500)::float / COUNT(*) FROM requests WHERE date = $1",
                  today(),
              ),
          }
          return json.dumps(stats, indent=2)
      
      @mcp.resource("log://app/errors/recent")
      def get_recent_errors() -> str:
          """Return the most recent application errors."""
          errors = db.query(
              "SELECT timestamp, level, message, stack_trace FROM logs "
              "WHERE level = 'ERROR' ORDER BY timestamp DESC LIMIT 20"
          )
          return json.dumps(errors, default=str, indent=2)
      ```
      
      ## MIME Types
      
      | MIME Type | Use For | Example |
      |-----------|---------|---------|
      | `text/plain` | Plain text, logs | Default if unspecified |
      | `application/json` | Structured data | API responses, configs |
      | `text/markdown` | Formatted docs | README, documentation |
      | `text/html` | Web content | Rendered pages |
      | `image/png` | PNG images | Charts, screenshots (base64) |
      | `image/jpeg` | JPEG images | Photos (base64) |
      | `application/pdf` | PDF documents | Reports (base64) |
      
      ### Setting MIME Types
      
      ```python
      # Python - FastMCP infers from return type, or specify explicitly
      @mcp.resource("docs://readme", mime_type="text/markdown")
      def get_readme() -> str:
          """Return the project README."""
          with open("README.md") as f:
              return f.read()
      
      # Binary content with base64
      @mcp.resource("images://logo")
      def get_logo() -> bytes:
          """Return the application logo."""
          with open("logo.png", "rb") as f:
              return f.read()  # FastMCP handles base64 encoding
      ```
      
      ```typescript
      // TypeScript - specify mimeType in contents
      server.resource("readme", "docs://readme", async (uri) => ({
        contents: [{
          uri: uri.href,
          mimeType: "text/markdown",
          text: await fs.readFile("README.md", "utf-8"),
        }],
      }));
      
      // Binary content
      server.resource("logo", "images://logo", async (uri) => ({
        contents: [{
          uri: uri.href,
          mimeType: "image/png",
          blob: (await fs.readFile("logo.png")).toString("base64"),
        }],
      }));
      ```
      
      ## Resource Subscriptions
      
      Clients can subscribe to resource changes and receive notifications:
      
      ```python
      from mcp.server.fastmcp import FastMCP, Context
      
      mcp = FastMCP("subscription-demo")
      
      # Track subscriptions
      _config_version = 0
      
      @mcp.resource("config://app/settings")
      def get_settings() -> str:
          return json.dumps(load_settings(), indent=2)
      
      @mcp.tool()
      async def update_setting(key: str, value: str, ctx: Context) -> str:
          """Update a configuration setting."""
          global _config_version
          save_setting(key, value)
          _config_version += 1
      
          # Notify subscribed clients that the resource changed
          await ctx.request_context.session.send_resource_updated("config://app/settings")
          return f"Updated {key} = {value}"
      ```
      
      ## Resource Listing
      
      Servers expose available resources via `resources/list`:
      
      ```python
      # FastMCP handles listing automatically for registered resources.
      # For dynamic resources, implement custom listing:
      
      @mcp.resource("db://app/tables/{table_name}")
      def get_table(table_name: str) -> str:
          """Read data from a database table."""
          return json.dumps(db.query(f"SELECT * FROM {table_name} LIMIT 50"), default=str)
      
      # Override resource listing to show available tables
      # (FastMCP's resource template handles this automatically when
      #  the template is registered with a list callback)
      ```
      
      ### Pagination for Large Resource Lists
      
      When you have many resources, consider chunking or metadata:
      
      ```python
      @mcp.resource("db://app/tables/{table}/page/{page}")
      def get_table_page(table: str, page: str) -> str:
          """Read a page of data from a database table.
      
          Args:
              table: Table name
              page: Page number (1-based)
          """
          page_num = int(page)
          offset = (page_num - 1) * 50
          rows = db.query(f"SELECT * FROM {table} LIMIT 50 OFFSET {offset}")
          total = db.count(table)
          return json.dumps({
              "rows": rows,
              "page": page_num,
              "total_pages": (total + 49) // 50,
              "total_rows": total,
          }, default=str, indent=2)
      ```
      
      ---
      
      ## Prompt Templates
      
      Prompts are pre-written templates that suggest how the LLM should approach a task.
      
      ### Basic Prompts
      
      ```python
      from mcp.server.fastmcp import FastMCP
      
      mcp = FastMCP("prompt-server")
      
      @mcp.prompt()
      def code_review(language: str, filepath: str) -> str:
          """Generate a code review prompt for the given file."""
          return (
              f"Please review the following {language} code from {filepath}. "
              f"Focus on:\n"
              f"1. Bug risks and edge cases\n"
              f"2. Performance issues\n"
              f"3. Code style and readability\n"
              f"4. Security concerns\n"
              f"5. Suggestions for improvement\n"
          )
      
      @mcp.prompt()
      def explain_error(error_message: str, context: str = "") -> str:
          """Generate a prompt to explain an error message."""
          prompt = f"Explain this error message and suggest how to fix it:\n\n```\n{error_message}\n```"
          if context:
              prompt += f"\n\nContext:\n{context}"
          return prompt
      ```
      
      ### TypeScript Prompts
      
      ```typescript
      server.prompt(
        "code_review",
        "Generate a code review prompt",
        {
          language: z.string().describe("Programming language"),
          filepath: z.string().describe("Path to the file to review"),
        },
        async ({ language, filepath }) => ({
          messages: [{
            role: "user",
            content: {
              type: "text",
              text: `Review this ${language} code from ${filepath}. Check for bugs, performance, style, and security.`,
            },
          }],
        })
      );
      ```
      
      ### Prompt Arguments
      
      ```python
      from typing import Optional
      
      @mcp.prompt()
      def sql_query_help(
          task: str,
          database_type: str = "postgresql",
          tables: Optional[str] = None,
      ) -> str:
          """Help write a SQL query for the given task.
      
          Args:
              task: What the query should do
              database_type: Target database (postgresql, mysql, sqlite)
              tables: Comma-separated list of relevant tables
          """
          prompt = f"Write a {database_type} SQL query to: {task}\n"
          if tables:
              prompt += f"\nRelevant tables: {tables}"
              prompt += "\nPlease query the table schemas first if you need to understand the structure."
          prompt += "\n\nRequirements:\n"
          prompt += "- Use parameterized queries (no string interpolation)\n"
          prompt += "- Include appropriate indexes if suggesting schema changes\n"
          prompt += "- Add comments explaining complex joins or subqueries\n"
          return prompt
      ```
      
      ### Multi-Turn Prompts
      
      Prompts can include multiple messages for conversation setup:
      
      ```python
      @mcp.prompt()
      def debug_session(error_type: str, language: str) -> list[dict]:
          """Start a debugging session for a specific error type."""
          return [
              {
                  "role": "system",
                  "content": f"You are a {language} debugging expert. Help the user systematically debug their {error_type} error.",
              },
              {
                  "role": "user",
                  "content": (
                      f"I'm encountering a {error_type} in my {language} code. "
                      "Please help me debug it step by step. "
                      "Start by asking me for the error message and relevant code."
                  ),
              },
          ]
      ```
      
      ```typescript
      server.prompt(
        "debug_session",
        "Start a debugging session",
        {
          error_type: z.string().describe("Type of error (e.g., TypeError, ConnectionError)"),
          language: z.string().describe("Programming language"),
        },
        async ({ error_type, language }) => ({
          messages: [
            {
              role: "assistant",
              content: {
                type: "text",
                text: `I'll help you debug your ${error_type} in ${language}. Let's work through this systematically.\n\nFirst, can you share:\n1. The full error message and stack trace\n2. The relevant code section\n3. What you've already tried`,
              },
            },
          ],
        })
      );
      ```
      
      ### Prompts that Reference Resources
      
      Combine prompts with resources for context-aware interactions:
      
      ```python
      @mcp.resource("schema://db/{table}")
      def get_table_schema(table: str) -> str:
          """Return the schema for a database table."""
          schema = db.get_schema(table)
          return json.dumps(schema, indent=2)
      
      @mcp.prompt()
      def optimize_query(table: str, slow_query: str) -> list[dict]:
          """Help optimize a slow SQL query with schema context."""
          return [
              {
                  "role": "user",
                  "content": [
                      {
                          "type": "resource",
                          "resource": {
                              "uri": f"schema://db/{table}",
                              "text": get_table_schema(table),
                              "mimeType": "application/json",
                          },
                      },
                      {
                          "type": "text",
                          "text": (
                              f"This query against the `{table}` table is slow:\n\n"
                              f"```sql\n{slow_query}\n```\n\n"
                              "Please suggest optimizations, including index recommendations."
                          ),
                      },
                  ],
              },
          ]
      ```
      
      ### Guided Workflows
      
      Use prompts to define multi-step workflows:
      
      ```python
      @mcp.prompt()
      def migration_workflow(source_db: str, target_db: str) -> list[dict]:
          """Guide through a database migration workflow."""
          return [
              {
                  "role": "user",
                  "content": (
                      f"I need to migrate data from {source_db} to {target_db}. "
                      "Please guide me through these steps:\n\n"
                      "1. Analyze the source schema\n"
                      "2. Create the target schema (with any needed transformations)\n"
                      "3. Write the migration script\n"
                      "4. Create validation queries to verify the migration\n"
                      "5. Suggest a rollback plan\n\n"
                      "Let's start with step 1."
                  ),
              },
          ]
      
      @mcp.prompt()
      def api_design(service_name: str, endpoints: str = "") -> str:
          """Help design a REST API."""
          prompt = f"Help me design a REST API for the {service_name} service.\n"
          if endpoints:
              prompt += f"\nPlanned endpoints:\n{endpoints}\n"
          prompt += (
              "\nFor each endpoint, specify:\n"
              "- HTTP method and path\n"
              "- Request/response schemas\n"
              "- Authentication requirements\n"
              "- Rate limiting considerations\n"
              "- Error responses\n"
          )
          return prompt
      ```
      
      ## Prompt Best Practices
      
      | Practice | Why |
      |----------|-----|
      | Use descriptive argument names | LLM and client UIs show argument names |
      | Provide defaults for optional args | Reduces friction for common cases |
      | Include structured instructions | Numbered lists guide the LLM's approach |
      | Reference resources when relevant | Gives the LLM concrete data to work with |
      | Keep prompts focused | One task per prompt, not multi-purpose |
      | Test with real LLM conversations | Prompts that read well may not work well |
      
      ## Combining Resources and Tools
      
      A common pattern: resources provide context, tools perform actions:
      
      ```python
      # Resource: provides data for the LLM to understand
      @mcp.resource("db://app/tables/{table}/schema")
      def get_schema(table: str) -> str:
          """Return the schema for a database table."""
          return json.dumps(db.get_schema(table), indent=2)
      
      @mcp.resource("db://app/tables/{table}/stats")
      def get_stats(table: str) -> str:
          """Return statistics for a database table."""
          return json.dumps({
              "row_count": db.count(table),
              "size_bytes": db.table_size(table),
              "last_modified": db.last_modified(table).isoformat(),
          }, indent=2)
      
      # Tool: performs actions using the context from resources
      @mcp.tool()
      def optimize_table(table: str, strategy: str = "auto") -> str:
          """Optimize a database table.
      
          Args:
              table: Table name to optimize
              strategy: Optimization strategy: auto, vacuum, reindex, analyze
          """
          if strategy == "auto":
              stats = json.loads(get_stats(table))
              if stats["row_count"] > 1_000_000:
                  strategy = "vacuum"
              else:
                  strategy = "analyze"
      
          result = db.optimize(table, strategy)
          return f"Optimized {table} using {strategy}: {result}"
      
      # Prompt: guides the LLM to use resources and tools together
      @mcp.prompt()
      def db_health_check() -> str:
          """Run a database health check."""
          return (
              "Please check the health of the database:\n"
              "1. Read the schema for each table\n"
              "2. Check the stats for each table\n"
              "3. Identify any tables that need optimization\n"
              "4. Run optimize_table on any that need it\n"
              "5. Summarize the results\n"
          )
      ```
      
    • server-architecture.md 20.1 KB
      # MCP Server Architecture
      
      ## Protocol Overview
      
      MCP uses **JSON-RPC 2.0** as its wire protocol. Every message is a JSON object with:
      
      - **Requests**: `{"jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": {...}}`
      - **Responses**: `{"jsonrpc": "2.0", "id": 1, "result": {...}}`
      - **Notifications**: `{"jsonrpc": "2.0", "method": "notifications/progress", "params": {...}}` (no `id`, no response expected)
      
      The protocol is transport-agnostic. The same JSON-RPC messages flow over stdio pipes, SSE streams, or HTTP requests.
      
      ## Server Lifecycle
      
      ```
      ┌──────────────┐     ┌──────────────────────┐     ┌─────────┐
      │ Uninitialized │────→│    Initializing       │────→│  Ready  │
      └──────────────┘     │                      │     └────┬────┘
                           │ Client sends          │          │
                           │ initialize request    │     ┌────┴────┐
                           │ Server responds with  │     │ Serving │
                           │ capabilities          │     │ requests│
                           │ Client sends          │     └────┬────┘
                           │ initialized notif     │          │
                           └──────────────────────┘     ┌────┴────┐
                                                        │Shutdown │
                                                        └─────────┘
      ```
      
      ### Phase 1: Initialization
      
      Client sends `initialize` with its capabilities and protocol version. Server responds with its own capabilities.
      
      ```json
      // Client → Server
      {
        "jsonrpc": "2.0",
        "id": 1,
        "method": "initialize",
        "params": {
          "protocolVersion": "2025-03-26",
          "capabilities": {},
          "clientInfo": { "name": "claude-code", "version": "1.0.0" }
        }
      }
      
      // Server → Client
      {
        "jsonrpc": "2.0",
        "id": 1,
        "result": {
          "protocolVersion": "2025-03-26",
          "capabilities": {
            "tools": { "listChanged": true },
            "resources": { "subscribe": true, "listChanged": true },
            "prompts": { "listChanged": true },
            "logging": {}
          },
          "serverInfo": { "name": "my-server", "version": "1.0.0" }
        }
      }
      ```
      
      ### Phase 2: Initialized Notification
      
      Client sends `notifications/initialized` to confirm. Server transitions to ready state.
      
      ### Phase 3: Serving
      
      Server handles requests: `tools/list`, `tools/call`, `resources/list`, `resources/read`, `prompts/list`, `prompts/get`, `completion/complete`.
      
      ### Phase 4: Shutdown
      
      Transport closes (process exit for stdio, connection close for HTTP/SSE). Server cleans up resources.
      
      ## Capability Negotiation
      
      Servers declare what they support during initialization:
      
      | Capability | Meaning | Sub-capabilities |
      |-----------|---------|------------------|
      | `tools` | Server offers tools | `listChanged` - notify on tool list changes |
      | `resources` | Server offers resources | `subscribe` - clients can subscribe to changes; `listChanged` |
      | `prompts` | Server offers prompts | `listChanged` - notify on prompt list changes |
      | `logging` | Server can emit log messages | (none) |
      | `experimental` | Experimental features | Varies by implementation |
      
      ## FastMCP Server Setup (Python)
      
      FastMCP is the recommended high-level API for Python MCP servers. Two variants exist:
      the frozen 1.x-era version bundled in the official SDK (`from mcp.server.fastmcp import
      FastMCP`, used below) and the actively developed standalone `fastmcp` package at major 3
      (`from fastmcp import FastMCP`). The decorator surface shown here works on both; on
      standalone FastMCP 3, transport configuration moves out of the constructor to serve time,
      and `ctx.set_state()`/`ctx.get_state()` are async.
      
      ### Basic Server
      
      ```python
      from mcp.server.fastmcp import FastMCP
      
      # Create server with metadata
      mcp = FastMCP(
          "my-server",
          version="1.0.0",
          description="A server that does useful things",
      )
      
      @mcp.tool()
      def greet(name: str) -> str:
          """Greet a user by name."""
          return f"Hello, {name}!"
      
      if __name__ == "__main__":
          mcp.run()  # stdio by default
      ```
      
      ### Server with Dependencies
      
      ```python
      from mcp.server.fastmcp import FastMCP
      import httpx
      
      mcp = FastMCP("api-server")
      
      # Dependencies are injected per-request via FastMCP's Context
      @mcp.tool()
      async def fetch_data(url: str) -> str:
          """Fetch data from a URL."""
          async with httpx.AsyncClient() as client:
              resp = await client.get(url, timeout=30.0)
              resp.raise_for_status()
              return resp.text
      ```
      
      ### Lifespan Handlers
      
      Use lifespan to manage resources that live for the server's entire lifetime:
      
      ```python
      from contextlib import asynccontextmanager
      from mcp.server.fastmcp import FastMCP
      
      @asynccontextmanager
      async def lifespan(server: FastMCP):
          """Initialize and cleanup server resources."""
          # Startup: create connection pools, load config
          db = await create_db_pool()
          server.state["db"] = db
          try:
              yield
          finally:
              # Shutdown: cleanup
              await db.close()
      
      mcp = FastMCP("db-server", lifespan=lifespan)
      
      @mcp.tool()
      async def query_db(sql: str, ctx: Context) -> str:
          """Run a read-only SQL query."""
          db = ctx.server.state["db"]
          results = await db.fetch(sql)
          return json.dumps(results, default=str)
      ```
      
      ### FastMCP Context Object
      
      The `Context` parameter gives tools access to server internals:
      
      ```python
      from mcp.server.fastmcp import FastMCP, Context
      
      mcp = FastMCP("context-demo")
      
      @mcp.tool()
      async def long_operation(items: list[str], ctx: Context) -> str:
          """Process items with progress reporting."""
          results = []
          for i, item in enumerate(items):
              await ctx.report_progress(i, len(items))
              result = await process_item(item)
              results.append(result)
              await ctx.info(f"Processed {item}")  # Log to client
          return json.dumps(results)
      ```
      
      Context provides:
      - `ctx.report_progress(current, total)` - send progress notifications
      - `ctx.info(message)`, `ctx.debug(message)`, `ctx.warning(message)`, `ctx.error(message)` - logging
      - `ctx.read_resource(uri)` - read another resource from within a tool
      - `ctx.server` - access the FastMCP server instance and its state
      - `ctx.request_context` - access the low-level request context and session
      
      ### Running with Different Transports
      
      ```python
      # stdio (default) - for Claude Desktop / Claude Code
      mcp.run()
      mcp.run(transport="stdio")
      
      # SSE - for web clients
      mcp.run(transport="sse", host="0.0.0.0", port=8000)
      
      # Streamable HTTP - for production
      mcp.run(transport="streamable-http", host="0.0.0.0", port=8000)
      ```
      
      ## TypeScript SDK Server Setup
      
      ### Basic Server with McpServer
      
      ```typescript
      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-server",
        version: "1.0.0",
      });
      
      // Tools, resources, and prompts registered via server methods
      server.tool("greet", "Greet a user", { name: z.string() }, async ({ name }) => ({
        content: [{ type: "text", text: `Hello, ${name}!` }],
      }));
      
      const transport = new StdioServerTransport();
      await server.connect(transport);
      ```
      
      ### Low-Level Server API
      
      For maximum control, use the `Server` class directly:
      
      ```typescript
      import { Server } from "@modelcontextprotocol/sdk/server/index.js";
      import {
        CallToolRequestSchema,
        ListToolsRequestSchema,
      } from "@modelcontextprotocol/sdk/types.js";
      
      const server = new Server(
        { name: "low-level-server", version: "1.0.0" },
        { capabilities: { tools: { listChanged: true } } }
      );
      
      server.setRequestHandler(ListToolsRequestSchema, async () => ({
        tools: [
          {
            name: "greet",
            description: "Greet a user",
            inputSchema: {
              type: "object" as const,
              properties: {
                name: { type: "string", description: "User name" },
              },
              required: ["name"],
            },
          },
        ],
      }));
      
      server.setRequestHandler(CallToolRequestSchema, async (request) => {
        if (request.params.name === "greet") {
          const { name } = request.params.arguments as { name: string };
          return {
            content: [{ type: "text", text: `Hello, ${name}!` }],
          };
        }
        throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${request.params.name}`);
      });
      ```
      
      ### McpError for Typed Errors
      
      ```typescript
      import { McpError, ErrorCode } from "@modelcontextprotocol/sdk/types.js";
      
      // Standard JSON-RPC error codes
      throw new McpError(ErrorCode.InvalidParams, "Missing required field: query");
      throw new McpError(ErrorCode.MethodNotFound, "Unknown tool: foo");
      throw new McpError(ErrorCode.InternalError, "Database connection failed");
      
      // Custom error codes (use negative numbers per JSON-RPC spec)
      throw new McpError(-32001, "Rate limit exceeded");
      ```
      
      ## Multi-Tool Server Organization
      
      ### By Domain (Recommended)
      
      ```python
      # tools/files.py
      from mcp.server.fastmcp import FastMCP
      
      def register_file_tools(mcp: FastMCP):
          @mcp.tool()
          def read_file(path: str) -> str:
              """Read contents of a file."""
              with open(path) as f:
                  return f.read()
      
          @mcp.tool()
          def write_file(path: str, content: str) -> str:
              """Write content to a file."""
              with open(path, "w") as f:
                  f.write(content)
              return f"Wrote {len(content)} bytes to {path}"
      
          @mcp.tool()
          def list_files(directory: str) -> str:
              """List files in a directory."""
              import os
              entries = os.listdir(directory)
              return "\n".join(entries)
      
      # tools/database.py
      def register_db_tools(mcp: FastMCP):
          @mcp.tool()
          def query(sql: str) -> str:
              """Execute a read-only SQL query."""
              ...
      
          @mcp.tool()
          def list_tables() -> str:
              """List all database tables."""
              ...
      
      # server.py
      from mcp.server.fastmcp import FastMCP
      from tools.files import register_file_tools
      from tools.database import register_db_tools
      
      mcp = FastMCP("multi-tool-server")
      register_file_tools(mcp)
      register_db_tools(mcp)
      
      if __name__ == "__main__":
          mcp.run()
      ```
      
      ### Tool Namespacing
      
      Prefix tool names to avoid collisions when multiple servers are active:
      
      ```python
      @mcp.tool(name="myapp_search")  # Not just "search"
      def search(query: str) -> str:
          ...
      
      @mcp.tool(name="myapp_create_item")  # Not just "create"
      def create_item(title: str) -> str:
          ...
      ```
      
      ## Middleware Patterns
      
      ### Request Logging
      
      ```python
      from mcp.server.fastmcp import FastMCP
      import logging
      
      logger = logging.getLogger("mcp-server")
      mcp = FastMCP("logged-server")
      
      # Use lifespan for server-level middleware
      @asynccontextmanager
      async def lifespan(server: FastMCP):
          logger.info("Server starting up")
          yield
          logger.info("Server shutting down")
      
      # For per-tool logging, use a decorator
      def logged_tool(func):
          async def wrapper(*args, **kwargs):
              logger.info(f"Tool called: {func.__name__} with {kwargs}")
              try:
                  result = await func(*args, **kwargs) if asyncio.iscoroutinefunction(func) else func(*args, **kwargs)
                  logger.info(f"Tool {func.__name__} succeeded")
                  return result
              except Exception as e:
                  logger.error(f"Tool {func.__name__} failed: {e}")
                  raise
          wrapper.__name__ = func.__name__
          wrapper.__doc__ = func.__doc__
          wrapper.__annotations__ = func.__annotations__
          return wrapper
      ```
      
      ### Rate Limiting
      
      ```python
      import time
      from collections import defaultdict
      
      class RateLimiter:
          def __init__(self, max_calls: int, window_seconds: int):
              self.max_calls = max_calls
              self.window = window_seconds
              self.calls: dict[str, list[float]] = 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
      
      rate_limiter = RateLimiter(max_calls=60, window_seconds=60)
      
      @mcp.tool()
      async def rate_limited_api_call(endpoint: str, ctx: Context) -> str:
          """Call an API with rate limiting."""
          if not rate_limiter.check("api"):
              return "Rate limit exceeded. Please wait before making more requests."
          async with httpx.AsyncClient() as client:
              resp = await client.get(f"https://api.example.com/{endpoint}")
              return resp.text
      ```
      
      ### Caching Middleware
      
      ```python
      import time
      from functools import wraps
      
      def cached(ttl_seconds: int = 300):
          """Cache tool results for the given TTL."""
          cache: dict[str, tuple[float, str]] = {}
      
          def decorator(func):
              @wraps(func)
              async def wrapper(*args, **kwargs):
                  key = f"{func.__name__}:{args}:{kwargs}"
                  if key in cache:
                      cached_at, result = cache[key]
                      if time.time() - cached_at < ttl_seconds:
                          return result
                  result = await func(*args, **kwargs) if asyncio.iscoroutinefunction(func) else func(*args, **kwargs)
                  cache[key] = (time.time(), result)
                  return result
              return wrapper
          return decorator
      
      @mcp.tool()
      @cached(ttl_seconds=60)
      async def get_weather(city: str) -> str:
          """Get current weather for a city (cached for 60s)."""
          async with httpx.AsyncClient() as client:
              resp = await client.get(f"https://weather.api/current?city={city}")
              return resp.text
      ```
      
      ## Error Handling
      
      ### Python Error Handling
      
      ```python
      from mcp.server.fastmcp import FastMCP
      
      mcp = FastMCP("robust-server")
      
      @mcp.tool()
      async def safe_operation(input: str) -> str:
          """Perform an operation with proper error handling."""
          try:
              result = await do_something(input)
              return json.dumps({"status": "success", "data": result})
          except ValueError as e:
              # User-facing error: return as tool result with error flag
              # FastMCP handles this by returning content with isError=True
              raise ValueError(f"Invalid input: {e}")
          except httpx.HTTPError as e:
              raise RuntimeError(f"API request failed: {e}")
          except Exception as e:
              # Log internally, return safe message
              logger.exception("Unexpected error in safe_operation")
              raise RuntimeError("An unexpected error occurred. Check server logs.")
      ```
      
      ### TypeScript Error Handling
      
      ```typescript
      server.tool("safe_operation", "Do something safely", { input: z.string() }, async ({ input }) => {
        try {
          const result = await doSomething(input);
          return { content: [{ type: "text", text: JSON.stringify(result) }] };
        } catch (error) {
          // Return error as tool result (visible to LLM)
          return {
            content: [{ type: "text", text: `Error: ${error.message}` }],
            isError: true,
          };
        }
      });
      ```
      
      ### Error Categories
      
      | Error Type | Handling | Example |
      |-----------|----------|---------|
      | Invalid input | Return clear message, `isError: true` | "Missing required field: query" |
      | Auth failure | Return message suggesting config check | "API key invalid. Check MY_API_KEY env var" |
      | External API error | Return status + retry suggestion | "GitHub API returned 503. Try again in 30s" |
      | Internal error | Log details, return safe message | "Internal error. Check server logs" |
      | Timeout | Return partial results if available | "Operation timed out. Partial results: ..." |
      
      ## Logging
      
      ### Python Logging
      
      ```python
      from mcp.server.fastmcp import FastMCP, Context
      
      mcp = FastMCP("logged-server")
      
      @mcp.tool()
      async def debug_tool(data: str, ctx: Context) -> str:
          """A tool with comprehensive logging."""
          await ctx.debug(f"Received data: {data[:100]}")
          await ctx.info("Processing started")
      
          try:
              result = process(data)
              await ctx.info(f"Processing complete: {len(result)} items")
              return json.dumps(result)
          except Exception as e:
              await ctx.error(f"Processing failed: {e}")
              raise
      ```
      
      ### Log Levels
      
      | Level | Use For |
      |-------|---------|
      | `debug` | Detailed diagnostic info, request/response bodies |
      | `info` | Normal operations, progress updates |
      | `warning` | Recoverable issues, deprecation notices |
      | `error` | Failures that affect the current operation |
      
      ## Graceful Shutdown
      
      ```python
      import signal
      from contextlib import asynccontextmanager
      from mcp.server.fastmcp import FastMCP
      
      @asynccontextmanager
      async def lifespan(server: FastMCP):
          # Startup
          db_pool = await create_pool()
          http_client = httpx.AsyncClient()
          server.state["db"] = db_pool
          server.state["http"] = http_client
      
          try:
              yield
          finally:
              # Cleanup: close connections, flush buffers
              await http_client.aclose()
              await db_pool.close()
              logger.info("Server shutdown complete")
      
      mcp = FastMCP("graceful-server", lifespan=lifespan)
      ```
      
      ## Connection and Session Management
      
      ### Session Isolation
      
      Each client connection gets its own session. Do not share mutable state between sessions without synchronization:
      
      ```python
      # BAD: Shared mutable state without locks
      results_cache = {}  # All sessions share this dict unsafely
      
      # GOOD: Per-session state via context
      @mcp.tool()
      async def track_calls(ctx: Context) -> str:
          """Track how many times this session called this tool."""
          session = ctx.request_context.session
          if not hasattr(session, "call_count"):
              session.call_count = 0
          session.call_count += 1
          return f"This session has made {session.call_count} calls"
      
      # GOOD: Shared state with proper locking
      import asyncio
      _lock = asyncio.Lock()
      _shared_cache: dict = {}
      
      @mcp.tool()
      async def cached_lookup(key: str) -> str:
          async with _lock:
              if key not in _shared_cache:
                  _shared_cache[key] = await expensive_lookup(key)
              return _shared_cache[key]
      ```
      
      ### Notifying Clients of Changes
      
      When your server's available tools, resources, or prompts change at runtime:
      
      ```python
      @mcp.tool()
      async def enable_advanced_tools(ctx: Context) -> str:
          """Dynamically register new tools and notify the client."""
          register_advanced_tools(ctx.server)
          # Notify client that tool list has changed
          await ctx.request_context.session.send_resource_list_changed()
          return "Advanced tools enabled"
      ```
      
      ## Project Structure
      
      ### Python (Recommended Layout)
      
      ```
      my-mcp-server/
      ├── pyproject.toml
      ├── src/
      │   └── my_server/
      │       ├── __init__.py
      │       ├── server.py          # FastMCP instance + main()
      │       ├── tools/
      │       │   ├── __init__.py
      │       │   ├── files.py       # File operation tools
      │       │   └── api.py         # API wrapper tools
      │       ├── resources/
      │       │   ├── __init__.py
      │       │   └── config.py      # Configuration resources
      │       └── prompts/
      │           ├── __init__.py
      │           └── workflows.py   # Prompt templates
      └── tests/
          ├── test_tools.py
          └── test_resources.py
      ```
      
      ### TypeScript (Recommended Layout)
      
      ```
      my-mcp-server/
      ├── package.json
      ├── tsconfig.json
      ├── src/
      │   ├── index.ts               # Server setup + transport
      │   ├── tools/
      │   │   ├── files.ts
      │   │   └── api.ts
      │   ├── resources/
      │   │   └── config.ts
      │   └── prompts/
      │       └── workflows.ts
      └── tests/
          ├── tools.test.ts
          └── resources.test.ts
      ```
      
      ## Claude Desktop Configuration
      
      ```json
      {
        "mcpServers": {
          "my-python-server": {
            "command": "uv",
            "args": ["run", "--directory", "/path/to/my-server", "python", "-m", "my_server"],
            "env": {
              "API_KEY": "your-key",
              "DATABASE_URL": "postgresql://localhost/mydb"
            }
          },
          "my-ts-server": {
            "command": "npx",
            "args": ["tsx", "/path/to/my-server/src/index.ts"],
            "env": {
              "API_KEY": "your-key"
            }
          }
        }
      }
      ```
      
      ## Claude Code Configuration
      
      In `.claude/settings.json` or project settings:
      
      ```json
      {
        "mcpServers": {
          "my-server": {
            "command": "uv",
            "args": ["run", "--directory", "/path/to/server", "python", "server.py"],
            "env": {
              "API_KEY": "your-key"
            }
          }
        }
      }
      ```
      
      Or via CLI:
      
      ```bash
      claude mcp add my-server -- uv run --directory /path/to/server python server.py
      ```
      
    • testing-debugging.md 24.6 KB
      # Testing and Debugging
      
      ## MCP Inspector
      
      The MCP Inspector is an interactive debugging tool for testing MCP servers without a full client setup.
      
      ### Installation and Usage
      
      ```bash
      # Run directly with npx (no install needed)
      npx @modelcontextprotocol/inspector
      
      # Connect to a stdio server
      npx @modelcontextprotocol/inspector -- uv run python server.py
      
      # Connect to a stdio server with env vars
      npx @modelcontextprotocol/inspector -e API_KEY=sk-key -- uv run python server.py
      
      # Connect to an SSE server
      npx @modelcontextprotocol/inspector --url http://localhost:8000/sse
      
      # Connect to a Streamable HTTP server
      npx @modelcontextprotocol/inspector --url http://localhost:8000/mcp
      ```
      
      ### What You Can Do with Inspector
      
      | Feature | How |
      |---------|-----|
      | List tools | Click "Tools" tab to see all registered tools |
      | Call tools | Fill in arguments and click "Call" |
      | List resources | Click "Resources" tab |
      | Read resources | Click any resource to see its contents |
      | List prompts | Click "Prompts" tab |
      | Get prompts | Fill in arguments and see rendered prompt |
      | View messages | "Messages" tab shows raw JSON-RPC traffic |
      | Test notifications | See server notifications in real-time |
      
      ### Inspecting Protocol Messages
      
      The Inspector shows raw JSON-RPC messages. Use this to verify:
      
      - Request format matches the MCP specification
      - Response content is properly structured
      - Error codes and messages are correct
      - Notifications are sent at the right times
      
      ```
      Example Inspector message log:
      
      → {"jsonrpc":"2.0","id":1,"method":"initialize","params":{...}}
      ← {"jsonrpc":"2.0","id":1,"result":{"capabilities":{"tools":{}},...}}
      → {"jsonrpc":"2.0","method":"notifications/initialized"}
      → {"jsonrpc":"2.0","id":2,"method":"tools/list"}
      ← {"jsonrpc":"2.0","id":2,"result":{"tools":[{"name":"search",...}]}}
      → {"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"search","arguments":{"query":"test"}}}
      ← {"jsonrpc":"2.0","id":3,"result":{"content":[{"type":"text","text":"..."}]}}
      ```
      
      ## Unit Testing Tools (Python)
      
      ### Testing with pytest
      
      ```python
      # test_tools.py
      import pytest
      import json
      from unittest.mock import AsyncMock, patch, MagicMock
      
      # Test tool functions directly (they're just functions)
      from my_server.server import search_docs, create_ticket
      
      class TestSearchDocs:
          def test_basic_search(self):
              """Test search returns formatted results."""
              with patch("my_server.server.perform_search") as mock_search:
                  mock_search.return_value = [
                      MagicMock(title="Doc 1", snippet="First result"),
                      MagicMock(title="Doc 2", snippet="Second result"),
                  ]
                  result = search_docs("test query")
                  assert "Doc 1" in result
                  assert "Doc 2" in result
                  mock_search.assert_called_once_with("test query")
      
          def test_empty_search(self):
              """Test search with no results."""
              with patch("my_server.server.perform_search") as mock_search:
                  mock_search.return_value = []
                  result = search_docs("nonexistent")
                  assert result == ""
      
          def test_search_error(self):
              """Test search handles errors gracefully."""
              with patch("my_server.server.perform_search") as mock_search:
                  mock_search.side_effect = ConnectionError("API down")
                  with pytest.raises(ConnectionError):
                      search_docs("test")
      
      
      class TestCreateTicket:
          def test_create_ticket(self):
              """Test ticket creation returns confirmation."""
              with patch("my_server.server.api") as mock_api:
                  mock_api.create.return_value = MagicMock(id=42, url="https://example.com/42")
                  result = create_ticket("Bug report", "Something broke", "high")
                  assert "#42" in result
                  assert "https://example.com/42" in result
      
          def test_create_ticket_validation(self):
              """Test ticket creation validates required fields."""
              # FastMCP validates types before calling the function
              # Test the function's own validation
              with pytest.raises(ValueError):
                  create_ticket("", "body", "medium")  # Empty title
      ```
      
      ### Testing Async Tools
      
      ```python
      import pytest
      import asyncio
      
      @pytest.mark.asyncio
      async def test_async_tool():
          """Test an async tool handler."""
          with patch("my_server.server.httpx.AsyncClient") as mock_client:
              mock_response = AsyncMock()
              mock_response.text = '{"data": "test"}'
              mock_response.raise_for_status = MagicMock()
              mock_client.return_value.__aenter__ = AsyncMock(return_value=mock_response)
              mock_client.return_value.__aexit__ = AsyncMock(return_value=False)
      
              # Better: use a real mock client
              mock_instance = AsyncMock()
              mock_instance.get.return_value = mock_response
      
              with patch("my_server.server.httpx.AsyncClient") as MockClient:
                  MockClient.return_value.__aenter__.return_value = mock_instance
                  MockClient.return_value.__aexit__.return_value = False
      
                  result = await fetch_data("https://example.com/api")
                  assert "test" in result
      ```
      
      ### Testing with FastMCP Test Client
      
      ```python
      import pytest
      from mcp.server.fastmcp import FastMCP
      
      # Create a test server
      mcp = FastMCP("test-server")
      
      @mcp.tool()
      def add(a: int, b: int) -> str:
          """Add two numbers."""
          return str(a + b)
      
      @mcp.resource("config://test")
      def get_config() -> str:
          return '{"key": "value"}'
      
      @pytest.mark.asyncio
      async def test_tool_via_mcp():
          """Test tools through the MCP protocol layer."""
          async with mcp.test_client() as client:
              # List tools
              tools = await client.list_tools()
              assert any(t.name == "add" for t in tools)
      
              # Call a tool
              result = await client.call_tool("add", {"a": 2, "b": 3})
              assert result[0].text == "5"
      
      @pytest.mark.asyncio
      async def test_resource_via_mcp():
          """Test resources through the MCP protocol layer."""
          async with mcp.test_client() as client:
              resources = await client.list_resources()
              assert any(r.uri == "config://test" for r in resources)
      
              content = await client.read_resource("config://test")
              assert '"key"' in content[0].text
      ```
      
      ## Unit Testing Tools (TypeScript)
      
      ### Testing with vitest
      
      ```typescript
      // tools.test.ts
      import { describe, it, expect, vi, beforeEach } from "vitest";
      import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
      import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js";
      import { Client } from "@modelcontextprotocol/sdk/client/index.js";
      import { z } from "zod";
      
      describe("search tool", () => {
        let server: McpServer;
        let client: Client;
      
        beforeEach(async () => {
          server = new McpServer({ name: "test", version: "1.0.0" });
      
          server.tool("search", "Search docs", { query: z.string() }, async ({ query }) => {
            // In real code, this calls an external service
            const results = await mockSearch(query);
            return {
              content: [{ type: "text", text: results.join("\n") }],
            };
          });
      
          // Connect via in-memory transport
          const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
          await server.connect(serverTransport);
      
          client = new Client({ name: "test-client", version: "1.0.0" });
          await client.connect(clientTransport);
        });
      
        it("should return search results", async () => {
          const result = await client.callTool({ name: "search", arguments: { query: "test" } });
          expect(result.content).toHaveLength(1);
          expect(result.content[0].type).toBe("text");
        });
      
        it("should list tools", async () => {
          const tools = await client.listTools();
          expect(tools.tools).toHaveLength(1);
          expect(tools.tools[0].name).toBe("search");
        });
      });
      ```
      
      ### Testing Error Handling
      
      ```typescript
      describe("error handling", () => {
        it("should return isError for invalid input", async () => {
          const result = await client.callTool({
            name: "query_db",
            arguments: { sql: "DELETE FROM users" },
          });
          expect(result.isError).toBe(true);
          expect(result.content[0].text).toContain("Only SELECT queries");
        });
      
        it("should handle tool not found", async () => {
          await expect(
            client.callTool({ name: "nonexistent", arguments: {} })
          ).rejects.toThrow();
        });
      });
      ```
      
      ## Integration Testing
      
      ### Full Server Integration Test (Python)
      
      ```python
      import pytest
      import asyncio
      from mcp import ClientSession
      from mcp.client.stdio import stdio_client
      
      @pytest.mark.asyncio
      async def test_server_integration():
          """Test the full server by spawning it as a subprocess."""
          async with stdio_client(
              command="uv",
              args=["run", "python", "server.py"],
              env={"API_KEY": "test-key"},
          ) as (read, write):
              async with ClientSession(read, write) as session:
                  # Initialize
                  await session.initialize()
      
                  # List tools
                  tools = await session.list_tools()
                  tool_names = [t.name for t in tools.tools]
                  assert "search" in tool_names
      
                  # Call a tool
                  result = await session.call_tool("search", {"query": "test"})
                  assert len(result.content) > 0
                  assert result.content[0].type == "text"
      
                  # List resources
                  resources = await session.list_resources()
                  assert len(resources.resources) > 0
      
                  # Read a resource
                  content = await session.read_resource("config://app")
                  assert len(content.contents) > 0
      ```
      
      ### Integration Test with HTTP Transport
      
      ```python
      import pytest
      import httpx
      import subprocess
      import time
      
      @pytest.fixture(scope="module")
      def server_process():
          """Start the MCP server as a subprocess."""
          proc = subprocess.Popen(
              ["uv", "run", "python", "server.py", "--transport", "streamable-http", "--port", "8765"],
              env={**os.environ, "API_KEY": "test-key"},
          )
          time.sleep(2)  # Wait for server to start
          yield proc
          proc.terminate()
          proc.wait()
      
      @pytest.mark.asyncio
      async def test_http_server(server_process):
          """Test the server via HTTP transport."""
          async with httpx.AsyncClient(base_url="http://localhost:8765") as client:
              # Initialize
              resp = await client.post("/mcp", json={
                  "jsonrpc": "2.0",
                  "id": 1,
                  "method": "initialize",
                  "params": {
                      "protocolVersion": "2025-03-26",
                      "capabilities": {},
                      "clientInfo": {"name": "test", "version": "1.0.0"},
                  },
              })
              assert resp.status_code == 200
              result = resp.json()["result"]
              assert "capabilities" in result
      
              # List tools
              resp = await client.post("/mcp", json={
                  "jsonrpc": "2.0",
                  "id": 2,
                  "method": "tools/list",
              })
              tools = resp.json()["result"]["tools"]
              assert len(tools) > 0
      ```
      
      ## Mock Clients
      
      ### Python Mock Client
      
      ```python
      from unittest.mock import AsyncMock
      from mcp.types import TextContent, CallToolResult
      
      def create_mock_session():
          """Create a mock MCP session for testing."""
          session = AsyncMock()
      
          # Mock tool listing
          session.list_tools.return_value = MockToolList(tools=[
              MockTool(name="search", description="Search docs", inputSchema={
                  "type": "object",
                  "properties": {"query": {"type": "string"}},
                  "required": ["query"],
              }),
          ])
      
          # Mock tool calls
          async def mock_call_tool(name, arguments):
              if name == "search":
                  return CallToolResult(
                      content=[TextContent(type="text", text="Mock result for: " + arguments["query"])]
                  )
              raise ValueError(f"Unknown tool: {name}")
      
          session.call_tool = AsyncMock(side_effect=mock_call_tool)
          return session
      ```
      
      ## Protocol Debugging
      
      ### Logging JSON-RPC Messages
      
      ```python
      import logging
      import json
      
      # Enable protocol-level logging
      logging.basicConfig(level=logging.DEBUG)
      logger = logging.getLogger("mcp.protocol")
      
      # Custom message logger
      class MessageLogger:
          def log_request(self, method: str, params: dict, id: int):
              logger.debug(f"→ [{id}] {method}: {json.dumps(params, indent=2)}")
      
          def log_response(self, id: int, result: dict):
              logger.debug(f"← [{id}] Result: {json.dumps(result, indent=2, default=str)[:500]}")
      
          def log_notification(self, method: str, params: dict):
              logger.debug(f"→ (notif) {method}: {json.dumps(params, indent=2)}")
      
          def log_error(self, id: int, error: dict):
              logger.error(f"← [{id}] Error: {json.dumps(error, indent=2)}")
      ```
      
      ### Capturing Messages for Replay
      
      ```python
      import json
      from datetime import datetime
      
      class MessageCapture:
          """Capture MCP messages for debugging and replay."""
      
          def __init__(self, output_file: str = "mcp_capture.jsonl"):
              self.output_file = output_file
              self.messages = []
      
          def capture(self, direction: str, message: dict):
              entry = {
                  "timestamp": datetime.utcnow().isoformat(),
                  "direction": direction,  # "sent" or "received"
                  "message": message,
              }
              self.messages.append(entry)
              with open(self.output_file, "a") as f:
                  f.write(json.dumps(entry) + "\n")
      
          def replay(self) -> list[dict]:
              """Load captured messages for analysis."""
              with open(self.output_file) as f:
                  return [json.loads(line) for line in f]
      ```
      
      ### Common Protocol Issues
      
      | Issue | Symptom | Diagnosis |
      |-------|---------|-----------|
      | Missing `jsonrpc` field | Server returns parse error | Check all messages include `"jsonrpc": "2.0"` |
      | Wrong method name | Method not found error | Verify against spec: `tools/call` not `tool/call` |
      | Missing `id` on request | No response received | All requests need unique `id`; notifications don't |
      | `params` vs `arguments` | Tool gets empty args | `tools/call` uses `params.arguments` for tool args |
      | Content format wrong | Client shows raw object | Must be `[{"type": "text", "text": "..."}]` |
      | Protocol version mismatch | Initialize fails | Use `"2025-03-26"` (check spec for latest) |
      
      ## Common Issues and Solutions
      
      ### Server Not Starting
      
      ```bash
      # Check 1: Can you run the server directly?
      uv run python server.py
      # If this fails, fix the Python/dependency issues first
      
      # Check 2: Does the command path exist?
      which uv    # or: which python, which npx
      # Ensure the command is on PATH
      
      # Check 3: Are dependencies installed?
      cd /path/to/server && uv pip list | grep mcp
      
      # Check 4: Check stderr for errors (stdio servers)
      # Add to server.py:
      import sys
      print("Server starting...", file=sys.stderr)
      ```
      
      ### Tool Not Appearing in Client
      
      ```python
      # Check 1: Does list_tools work?
      # Use MCP Inspector to verify
      npx @modelcontextprotocol/inspector -- uv run python server.py
      
      # Check 2: Is the tool registered correctly?
      # Verify with a simple test:
      @mcp.tool()
      def test_tool() -> str:
          """A test tool that always works."""
          return "Tool is working!"
      
      # Check 3: Invalid inputSchema
      # Ensure schema is valid JSON Schema
      # Common mistake: using Python types instead of JSON Schema types
      # BAD: {"type": "str"}
      # GOOD: {"type": "string"}
      ```
      
      ### Auth Failures
      
      ```python
      # Check 1: Are env vars set in the CLIENT config, not just your shell?
      # Claude Desktop reads env from claude_desktop_config.json, NOT your shell profile
      
      # Check 2: Verify env vars are accessible
      @mcp.tool()
      def debug_env() -> str:
          """Show environment variables (for debugging only)."""
          import os
          return json.dumps({
              k: v[:5] + "..." if len(v) > 5 else v
              for k, v in os.environ.items()
              if k.startswith("MY_")  # Only show your app's vars
          })
      
      # Check 3: Token expiration
      # Add logging to token refresh:
      import sys
      print(f"Token expires at: {expires_at}", file=sys.stderr)
      ```
      
      ### Timeout Errors
      
      ```python
      # Check 1: Add timeouts to all external calls
      async with httpx.AsyncClient(timeout=30.0) as client:
          resp = await client.get(url)
      
      # Check 2: Break long operations into steps
      @mcp.tool()
      async def process_large_dataset(dataset_id: str, ctx: Context) -> str:
          """Process a large dataset in chunks with progress."""
          chunks = get_chunks(dataset_id)
          results = []
          for i, chunk in enumerate(chunks):
              await ctx.report_progress(i, len(chunks))
              results.append(await process_chunk(chunk))
          return json.dumps({"processed": len(results)})
      
      # Check 3: Use streaming for large responses
      # Return a summary instead of full data
      @mcp.tool()
      def query_large_table(table: str) -> str:
          """Query a table, returning summary + sample."""
          count = db.count(table)
          sample = db.query(f"SELECT * FROM {table} LIMIT 10")
          return json.dumps({
              "total_rows": count,
              "sample": sample,
              "message": f"Showing 10 of {count} rows. Use pagination for more.",
          }, default=str)
      ```
      
      ### JSON Parse Errors
      
      ```python
      # Check 1: Don't print to stdout in stdio servers!
      # stdout IS the protocol channel. Use stderr for logging.
      import sys
      print("Debug info", file=sys.stderr)  # Correct
      # print("Debug info")  # WRONG - corrupts protocol stream
      
      # Check 2: Ensure tool results are serializable
      @mcp.tool()
      def get_data() -> str:
          result = db.query("SELECT * FROM users")
          # BAD: datetime objects aren't JSON serializable by default
          # return json.dumps(result)
      
          # GOOD: handle non-serializable types
          return json.dumps(result, default=str)
      ```
      
      ## CI Testing
      
      ### GitHub Actions
      
      ```yaml
      # .github/workflows/test-mcp.yml
      name: Test MCP Server
      on: [push, pull_request]
      
      jobs:
        test:
          runs-on: ubuntu-latest
          steps:
            - uses: actions/checkout@v4
      
            - name: Install uv
              uses: astral-sh/setup-uv@v4
      
            - name: Install dependencies
              run: uv sync
      
            - name: Run unit tests
              run: uv run pytest tests/ -v
      
            - name: Run integration tests
              run: uv run pytest tests/integration/ -v
              env:
                API_KEY: ${{ secrets.TEST_API_KEY }}
      
            - name: Test server starts
              run: |
                uv run python server.py &
                SERVER_PID=$!
                sleep 3
                # Verify server is running
                kill -0 $SERVER_PID 2>/dev/null && echo "Server started successfully"
                kill $SERVER_PID
      
            - name: Test with MCP Inspector
              run: |
                npx @modelcontextprotocol/inspector --test -- uv run python server.py
      ```
      
      ### Docker-Based Testing
      
      ```dockerfile
      # Dockerfile.test
      FROM python:3.12-slim
      WORKDIR /app
      COPY . .
      RUN pip install uv && uv sync
      RUN uv run pytest tests/ -v
      ```
      
      ```yaml
      # docker-compose.test.yml
      services:
        test:
          build:
            context: .
            dockerfile: Dockerfile.test
          environment:
            - API_KEY=test-key
            - DATABASE_URL=postgresql://postgres:test@db:5432/testdb
          depends_on:
            - db
        db:
          image: postgres:16
          environment:
            POSTGRES_PASSWORD: test
            POSTGRES_DB: testdb
      ```
      
      ```bash
      docker compose -f docker-compose.test.yml up --build --abort-on-container-exit
      ```
      
      ## Performance Testing
      
      ### Concurrent Tool Calls
      
      ```python
      import pytest
      import asyncio
      import time
      
      @pytest.mark.asyncio
      async def test_concurrent_tool_calls():
          """Test server handles concurrent requests correctly."""
          async with mcp.test_client() as client:
              start = time.time()
      
              # Fire 20 concurrent tool calls
              tasks = [
                  client.call_tool("search", {"query": f"test-{i}"})
                  for i in range(20)
              ]
              results = await asyncio.gather(*tasks)
              elapsed = time.time() - start
      
              assert len(results) == 20
              assert all(len(r) > 0 for r in results)
              print(f"20 concurrent calls completed in {elapsed:.2f}s")
      ```
      
      ### Large Payload Handling
      
      ```python
      @pytest.mark.asyncio
      async def test_large_response():
          """Test server handles large responses without issues."""
          async with mcp.test_client() as client:
              result = await client.call_tool("generate_report", {
                  "size": "large",  # Generates a multi-KB response
              })
              assert len(result[0].text) > 10000
              # Verify it's valid JSON
              data = json.loads(result[0].text)
              assert "report" in data
      
      @pytest.mark.asyncio
      async def test_response_size_limit():
          """Verify server truncates oversized responses."""
          async with mcp.test_client() as client:
              result = await client.call_tool("get_all_data", {})
              text = result[0].text
              # Server should paginate or truncate
              assert len(text) < 1_000_000  # Under 1MB
      ```
      
      ### Memory Usage
      
      ```python
      import tracemalloc
      
      @pytest.mark.asyncio
      async def test_memory_usage():
          """Test that tool calls don't leak memory."""
          tracemalloc.start()
      
          async with mcp.test_client() as client:
              snapshot1 = tracemalloc.take_snapshot()
      
              # Run 100 tool calls
              for i in range(100):
                  await client.call_tool("search", {"query": f"test-{i}"})
      
              snapshot2 = tracemalloc.take_snapshot()
              stats = snapshot2.compare_to(snapshot1, "lineno")
      
              # Check no single allocation grew more than 10MB
              for stat in stats[:5]:
                  assert stat.size_diff < 10 * 1024 * 1024, f"Memory leak detected: {stat}"
      
          tracemalloc.stop()
      ```
      
      ## Debugging Tools
      
      ### Claude Desktop Logs
      
      ```bash
      # macOS
      tail -f ~/Library/Logs/Claude/mcp-server-*.log
      
      # Windows
      Get-Content "$env:APPDATA\Claude\Logs\mcp-server-*.log" -Wait
      
      # Look for:
      # - Server startup errors
      # - Tool call failures
      # - Transport disconnections
      ```
      
      ### Custom Debug Server
      
      Add a debug tool to your server for troubleshooting:
      
      ```python
      import sys
      import os
      import platform
      
      @mcp.tool()
      def server_debug_info() -> str:
          """Return server diagnostic information (remove in production)."""
          return json.dumps({
              "python_version": sys.version,
              "platform": platform.platform(),
              "cwd": os.getcwd(),
              "env_vars": {k: "***" for k in os.environ if k.startswith("MY_")},
              "pid": os.getpid(),
              "argv": sys.argv,
          }, indent=2)
      ```
      
      ### Stderr Logging for stdio Servers
      
      Since stdout is the protocol channel, use stderr for all debugging:
      
      ```python
      import sys
      import logging
      
      # Configure logging to stderr
      logging.basicConfig(
          stream=sys.stderr,
          level=logging.DEBUG,
          format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
      )
      logger = logging.getLogger("my-server")
      
      @mcp.tool()
      def my_tool(query: str) -> str:
          logger.debug(f"my_tool called with query={query!r}")
          try:
              result = process(query)
              logger.info(f"my_tool succeeded: {len(result)} chars")
              return result
          except Exception:
              logger.exception("my_tool failed")
              raise
      ```
      
      ## Error Reproduction
      
      ### Capturing and Replaying Protocol Messages
      
      ```python
      import json
      
      class ProtocolRecorder:
          """Record MCP protocol messages for reproduction."""
      
          def __init__(self, output_path: str = "mcp_recording.jsonl"):
              self.output_path = output_path
              self._file = open(output_path, "w")
      
          def record(self, direction: str, message: dict):
              self._file.write(json.dumps({
                  "direction": direction,
                  "message": message,
                  "timestamp": time.time(),
              }) + "\n")
              self._file.flush()
      
          def close(self):
              self._file.close()
      
      
      class ProtocolReplayer:
          """Replay recorded messages against a server."""
      
          def __init__(self, recording_path: str):
              with open(recording_path) as f:
                  self.entries = [json.loads(line) for line in f]
      
          async def replay(self, session):
              """Replay recorded messages and compare responses."""
              sent = [e for e in self.entries if e["direction"] == "sent"]
              received = [e for e in self.entries if e["direction"] == "received"]
      
              for i, entry in enumerate(sent):
                  msg = entry["message"]
                  if "id" in msg:
                      # It's a request
                      method = msg["method"]
                      params = msg.get("params", {})
                      if method == "tools/call":
                          result = await session.call_tool(
                              params["name"],
                              params.get("arguments", {}),
                          )
                          # Compare with recorded response
                          expected = received[i] if i < len(received) else None
                          if expected:
                              print(f"[{method}] Match: {result == expected['message']['result']}")
      ```
      
      ### Minimal Reproduction Script
      
      When filing bug reports, create a minimal reproduction:
      
      ```python
      #!/usr/bin/env python3
      """Minimal reproduction for MCP issue #XXX.
      
      Run: uv run python repro.py
      Test: npx @modelcontextprotocol/inspector -- uv run python repro.py
      """
      from mcp.server.fastmcp import FastMCP
      
      mcp = FastMCP("repro-server")
      
      @mcp.tool()
      def trigger_bug(input: str) -> str:
          """This tool demonstrates the bug."""
          # Minimal code that triggers the issue
          result = problematic_operation(input)
          return result
      
      if __name__ == "__main__":
          mcp.run()
      ```
      
    • tool-handlers.md 23.7 KB
      # Tool Handlers
      
      ## Tool Schema Design
      
      Every MCP tool has an `inputSchema` that follows JSON Schema. The schema tells the LLM what arguments the tool accepts.
      
      ### Basic Schema
      
      ```python
      from mcp.server.fastmcp import FastMCP
      
      mcp = FastMCP("tools-demo")
      
      # FastMCP generates the schema from type hints and docstring
      @mcp.tool()
      def search(query: str, max_results: int = 10) -> str:
          """Search for documents matching a query.
      
          Args:
              query: The search query string
              max_results: Maximum number of results to return (default: 10)
          """
          results = perform_search(query, limit=max_results)
          return "\n".join(f"- {r.title}: {r.snippet}" for r in results)
      ```
      
      Generated schema:
      
      ```json
      {
        "name": "search",
        "description": "Search for documents matching a query.",
        "inputSchema": {
          "type": "object",
          "properties": {
            "query": {
              "type": "string",
              "description": "The search query string"
            },
            "max_results": {
              "type": "integer",
              "description": "Maximum number of results to return (default: 10)",
              "default": 10
            }
          },
          "required": ["query"]
        }
      }
      ```
      
      ### Complex Schemas with Pydantic
      
      ```python
      from pydantic import BaseModel, Field
      from enum import Enum
      from typing import Optional
      
      class Priority(str, Enum):
          LOW = "low"
          MEDIUM = "medium"
          HIGH = "high"
          CRITICAL = "critical"
      
      class CreateTicket(BaseModel):
          title: str = Field(description="Short summary of the issue")
          body: str = Field(description="Detailed description")
          priority: Priority = Field(default=Priority.MEDIUM, description="Ticket priority level")
          labels: list[str] = Field(default_factory=list, description="Labels to apply")
          assignee: Optional[str] = Field(default=None, description="GitHub username to assign")
      
      @mcp.tool()
      def create_ticket(ticket: CreateTicket) -> str:
          """Create a new support ticket."""
          result = api.create_ticket(
              title=ticket.title,
              body=ticket.body,
              priority=ticket.priority.value,
              labels=ticket.labels,
              assignee=ticket.assignee,
          )
          return f"Created ticket #{result.id}: {result.url}"
      ```
      
      ### TypeScript Schema with Zod
      
      ```typescript
      import { z } from "zod";
      
      const PriorityEnum = z.enum(["low", "medium", "high", "critical"]);
      
      server.tool(
        "create_ticket",
        "Create a new support ticket",
        {
          title: z.string().describe("Short summary of the issue"),
          body: z.string().describe("Detailed description"),
          priority: PriorityEnum.default("medium").describe("Ticket priority level"),
          labels: z.array(z.string()).default([]).describe("Labels to apply"),
          assignee: z.string().optional().describe("GitHub username to assign"),
        },
        async ({ title, body, priority, labels, assignee }) => {
          const result = await api.createTicket({ title, body, priority, labels, assignee });
          return {
            content: [{ type: "text", text: `Created ticket #${result.id}: ${result.url}` }],
          };
        }
      );
      ```
      
      ### Schema Best Practices
      
      | Practice | Why | Example |
      |----------|-----|---------|
      | Always add `description` to every field | LLM uses descriptions to decide what values to pass | `"description": "SQL query to execute"` |
      | Use `enum` for fixed choices | Constrains LLM to valid values | `"enum": ["asc", "desc"]` |
      | Set sensible `default` values | Reduces required arguments | `"default": 10` |
      | Mark truly required fields only | Optional fields with defaults reduce friction | Only `query` required, not `max_results` |
      | Use nested objects sparingly | Flat schemas are easier for LLMs | Prefer `title, body` over `ticket: {title, body}` |
      | Keep descriptions under 100 chars | Long descriptions waste context | "Search query" not "The string to use for searching..." |
      
      ## Input Validation
      
      ### Python with Pydantic (FastMCP)
      
      FastMCP automatically validates inputs against type hints:
      
      ```python
      from pydantic import Field, field_validator
      
      @mcp.tool()
      def query_database(
          sql: str,
          limit: int = Field(default=100, ge=1, le=1000),
      ) -> str:
          """Execute a read-only SQL query.
      
          Args:
              sql: SQL SELECT query to execute
              limit: Maximum rows to return (1-1000)
          """
          if not sql.strip().upper().startswith("SELECT"):
              raise ValueError("Only SELECT queries are allowed")
          results = db.execute(f"{sql} LIMIT {limit}")
          return json.dumps(results, default=str)
      ```
      
      ### Custom Validators
      
      ```python
      from pydantic import BaseModel, field_validator
      
      class FileReadArgs(BaseModel):
          path: str
          encoding: str = "utf-8"
      
          @field_validator("path")
          @classmethod
          def validate_path(cls, v: str) -> str:
              import os
              # Prevent directory traversal
              resolved = os.path.realpath(v)
              allowed_root = os.path.realpath("/workspace")
              if not resolved.startswith(allowed_root):
                  raise ValueError(f"Path must be within /workspace, got: {v}")
              return resolved
      
          @field_validator("encoding")
          @classmethod
          def validate_encoding(cls, v: str) -> str:
              allowed = {"utf-8", "ascii", "latin-1", "utf-16"}
              if v not in allowed:
                  raise ValueError(f"Encoding must be one of: {allowed}")
              return v
      
      @mcp.tool()
      def read_file(args: FileReadArgs) -> str:
          """Read a file from the workspace."""
          with open(args.path, encoding=args.encoding) as f:
              return f.read()
      ```
      
      ### TypeScript with Zod
      
      ```typescript
      server.tool(
        "query_database",
        "Execute a read-only SQL query",
        {
          sql: z.string()
            .refine((s) => s.trim().toUpperCase().startsWith("SELECT"), {
              message: "Only SELECT queries are allowed",
            }),
          limit: z.number().int().min(1).max(1000).default(100),
        },
        async ({ sql, limit }) => {
          const results = await db.query(`${sql} LIMIT ${limit}`);
          return {
            content: [{ type: "text", text: JSON.stringify(results) }],
          };
        }
      );
      ```
      
      ## Return Types
      
      ### Text Content (Most Common)
      
      ```python
      @mcp.tool()
      def get_user(user_id: str) -> str:
          """Look up a user by ID."""
          user = db.get_user(user_id)
          return json.dumps({
              "id": user.id,
              "name": user.name,
              "email": user.email,
              "created_at": user.created_at.isoformat(),
          }, indent=2)
      ```
      
      ### Image Content
      
      ```python
      import base64
      
      @mcp.tool()
      def generate_chart(data: str, chart_type: str = "bar") -> list:
          """Generate a chart image from data."""
          # Generate chart with matplotlib
          import matplotlib.pyplot as plt
          import io
      
          fig, ax = plt.subplots()
          parsed = json.loads(data)
          ax.bar(parsed["labels"], parsed["values"])
          ax.set_title(parsed.get("title", "Chart"))
      
          buf = io.BytesIO()
          fig.savefig(buf, format="png")
          buf.seek(0)
          plt.close(fig)
      
          image_b64 = base64.b64encode(buf.read()).decode("utf-8")
      
          # Return both image and text description
          return [
              {"type": "image", "data": image_b64, "mimeType": "image/png"},
              {"type": "text", "text": f"Generated {chart_type} chart with {len(parsed['labels'])} data points."},
          ]
      ```
      
      ### Embedded Resources
      
      ```python
      @mcp.tool()
      def analyze_file(path: str) -> list:
          """Analyze a file and return results with the file content as an embedded resource."""
          content = open(path).read()
          analysis = perform_analysis(content)
      
          return [
              {
                  "type": "resource",
                  "resource": {
                      "uri": f"file://{path}",
                      "mimeType": "text/plain",
                      "text": content,
                  },
              },
              {"type": "text", "text": f"Analysis:\n{analysis}"},
          ]
      ```
      
      ### Multiple Content Items
      
      ```python
      @mcp.tool()
      def compare_files(path_a: str, path_b: str) -> list:
          """Compare two files and show differences."""
          content_a = open(path_a).read()
          content_b = open(path_b).read()
          diff = compute_diff(content_a, content_b)
      
          return [
              {"type": "text", "text": f"## File A: {path_a}\n```\n{content_a}\n```"},
              {"type": "text", "text": f"## File B: {path_b}\n```\n{content_b}\n```"},
              {"type": "text", "text": f"## Differences\n```diff\n{diff}\n```"},
          ]
      ```
      
      ## Progress Notifications
      
      For long-running operations, report progress so the client can display updates:
      
      ```python
      @mcp.tool()
      async def batch_process(items: list[str], ctx: Context) -> str:
          """Process a batch of items with progress reporting."""
          results = []
          total = len(items)
      
          for i, item in enumerate(items):
              await ctx.report_progress(i, total)
              await ctx.info(f"Processing item {i+1}/{total}: {item}")
      
              try:
                  result = await process_single_item(item)
                  results.append({"item": item, "status": "success", "result": result})
              except Exception as e:
                  results.append({"item": item, "status": "error", "error": str(e)})
      
          await ctx.report_progress(total, total)
      
          succeeded = sum(1 for r in results if r["status"] == "success")
          return json.dumps({
              "summary": f"{succeeded}/{total} items processed successfully",
              "results": results,
          }, indent=2)
      ```
      
      ## Error Responses
      
      ### Returning Errors to the LLM
      
      ```python
      @mcp.tool()
      def delete_item(item_id: str) -> str:
          """Delete an item by ID."""
          try:
              item = db.get(item_id)
              if item is None:
                  # Raise to signal error - FastMCP sets isError=True
                  raise ValueError(f"Item {item_id} not found")
              if item.protected:
                  raise PermissionError(f"Item {item_id} is protected and cannot be deleted")
              db.delete(item_id)
              return f"Deleted item {item_id}"
          except (ValueError, PermissionError):
              raise  # Re-raise known errors for the LLM
          except Exception as e:
              raise RuntimeError(f"Failed to delete item: {e}")
      ```
      
      ### TypeScript Error Responses
      
      ```typescript
      server.tool("delete_item", "Delete an item", { item_id: z.string() }, async ({ item_id }) => {
        try {
          const item = await db.get(item_id);
          if (!item) {
            return {
              content: [{ type: "text", text: `Item ${item_id} not found` }],
              isError: true,
            };
          }
          await db.delete(item_id);
          return {
            content: [{ type: "text", text: `Deleted item ${item_id}` }],
          };
        } catch (error) {
          return {
            content: [{ type: "text", text: `Failed to delete: ${error.message}` }],
            isError: true,
          };
        }
      });
      ```
      
      ### Error Best Practices
      
      | Practice | Why |
      |----------|-----|
      | Always include actionable message | LLM can report to user or retry differently |
      | Distinguish user errors from system errors | User errors: "Invalid SQL syntax"; system: "Database unavailable" |
      | Never expose stack traces | Security risk; use structured error messages |
      | Return `isError: true` for failures | Clients and LLMs can distinguish success from failure |
      | Log internal details server-side | Use `ctx.error()` or logging for debugging |
      
      ## Tool Composition
      
      ### Tools Calling Other Tools
      
      ```python
      @mcp.tool()
      async def analyze_and_fix(filepath: str, ctx: Context) -> str:
          """Analyze code for issues and apply fixes."""
          # Read the file using a resource
          content = await ctx.read_resource(f"file://{filepath}")
      
          # Analyze
          issues = analyze_code(content)
          if not issues:
              return "No issues found"
      
          # Apply fixes
          fixed = apply_fixes(content, issues)
      
          # Write back (side effect)
          with open(filepath, "w") as f:
              f.write(fixed)
      
          return f"Fixed {len(issues)} issues in {filepath}:\n" + "\n".join(
              f"- {issue.description}" for issue in issues
          )
      ```
      
      ### Dependency Injection
      
      ```python
      from dataclasses import dataclass
      
      @dataclass
      class AppDependencies:
          db: DatabasePool
          http: httpx.AsyncClient
          cache: dict
      
      @asynccontextmanager
      async def lifespan(server: FastMCP):
          deps = AppDependencies(
              db=await create_pool(),
              http=httpx.AsyncClient(),
              cache={},
          )
          server.state["deps"] = deps
          try:
              yield
          finally:
              await deps.http.aclose()
              await deps.db.close()
      
      mcp = FastMCP("di-server", lifespan=lifespan)
      
      @mcp.tool()
      async def search_and_cache(query: str, ctx: Context) -> str:
          """Search with caching."""
          deps: AppDependencies = ctx.server.state["deps"]
      
          if query in deps.cache:
              return deps.cache[query]
      
          results = await deps.db.fetch("SELECT * FROM docs WHERE content LIKE $1", f"%{query}%")
          formatted = json.dumps(results, default=str)
          deps.cache[query] = formatted
          return formatted
      ```
      
      ## Batch Operations
      
      ```python
      @mcp.tool()
      async def bulk_update(
          updates: list[dict],
          continue_on_error: bool = True,
          ctx: Context = None,
      ) -> str:
          """Apply multiple updates, optionally continuing past errors.
      
          Args:
              updates: List of {id, field, value} objects
              continue_on_error: If true, skip failed items and continue
          """
          results = {"succeeded": [], "failed": []}
      
          for i, update in enumerate(updates):
              if ctx:
                  await ctx.report_progress(i, len(updates))
              try:
                  db.update(update["id"], {update["field"]: update["value"]})
                  results["succeeded"].append(update["id"])
              except Exception as e:
                  if continue_on_error:
                      results["failed"].append({"id": update["id"], "error": str(e)})
                  else:
                      return json.dumps({
                          "error": f"Failed on item {update['id']}: {e}",
                          "completed": results["succeeded"],
                      })
      
          return json.dumps({
              "summary": f"{len(results['succeeded'])} succeeded, {len(results['failed'])} failed",
              **results,
          }, indent=2)
      ```
      
      ## Idempotency
      
      Design tools that are safe to retry:
      
      ```python
      @mcp.tool()
      def upsert_config(key: str, value: str) -> str:
          """Set a configuration value (idempotent - safe to retry).
      
          Args:
              key: Configuration key
              value: Configuration value
          """
          # Use upsert instead of insert to make retries safe
          db.execute(
              "INSERT INTO config (key, value) VALUES ($1, $2) "
              "ON CONFLICT (key) DO UPDATE SET value = $2",
              key, value,
          )
          return f"Config {key} = {value}"
      
      @mcp.tool()
      def create_item_idempotent(idempotency_key: str, title: str, body: str) -> str:
          """Create an item with an idempotency key (safe to retry).
      
          Args:
              idempotency_key: Unique key for this operation (e.g., UUID)
              title: Item title
              body: Item body
          """
          existing = db.get_by_idempotency_key(idempotency_key)
          if existing:
              return f"Item already exists: #{existing.id} (idempotent match)"
          item = db.create(title=title, body=body, idempotency_key=idempotency_key)
          return f"Created item #{item.id}"
      ```
      
      ## Side Effects and Confirmation
      
      ### Read-Only vs Mutating Tools
      
      ```python
      # Read-only: no confirmation needed
      @mcp.tool()
      def list_users(role: str = "all") -> str:
          """List users, optionally filtered by role."""
          users = db.list_users(role=role if role != "all" else None)
          return json.dumps(users, default=str)
      
      # Mutating: include clear description of what will change
      @mcp.tool()
      def delete_user(user_id: str, confirm: bool = False) -> str:
          """Permanently delete a user and all their data.
      
          WARNING: This action is irreversible. Set confirm=true to proceed.
      
          Args:
              user_id: The user ID to delete
              confirm: Must be true to actually perform the deletion
          """
          if not confirm:
              user = db.get_user(user_id)
              return (
                  f"This will permanently delete user {user.name} ({user.email}) "
                  f"and {user.data_count} associated records. "
                  f"Call again with confirm=true to proceed."
              )
          db.delete_user(user_id)
          return f"User {user_id} has been permanently deleted."
      ```
      
      ### Dry Run Pattern
      
      ```python
      @mcp.tool()
      def refactor_imports(directory: str, dry_run: bool = True) -> str:
          """Reorganize import statements in Python files.
      
          Args:
              directory: Directory to process
              dry_run: If true, show what would change without modifying files
          """
          changes = analyze_imports(directory)
      
          if dry_run:
              summary = "\n".join(f"  {c.file}: {c.description}" for c in changes)
              return f"Dry run - {len(changes)} files would be modified:\n{summary}\n\nRun with dry_run=false to apply."
      
          for change in changes:
              apply_change(change)
          return f"Applied {len(changes)} import reorganizations."
      ```
      
      ## Schema Evolution
      
      ### Backwards-Compatible Changes
      
      ```python
      # v1: Original tool
      @mcp.tool()
      def search_v1(query: str) -> str:
          """Search documents."""
          ...
      
      # v2: Added optional fields (backwards compatible)
      @mcp.tool()
      def search(
          query: str,
          max_results: int = 10,        # New in v2
          include_archived: bool = False, # New in v2
      ) -> str:
          """Search documents with optional filters.
      
          Args:
              query: Search query
              max_results: Maximum results to return
              include_archived: Include archived documents in results
          """
          ...
      
      # v3: Deprecated field (still accepted, but ignored)
      @mcp.tool()
      def search(
          query: str,
          max_results: int = 10,
          include_archived: bool = False,
          sort_by: str = "relevance",  # New in v3
          # Deprecated: use sort_by instead
          sort_order: str | None = None,  # Deprecated in v3
      ) -> str:
          """Search documents.
      
          Args:
              query: Search query
              max_results: Maximum results to return
              include_archived: Include archived documents
              sort_by: Sort results by: relevance, date, title
              sort_order: DEPRECATED - use sort_by instead
          """
          if sort_order is not None:
              # Handle deprecated parameter gracefully
              sort_by = sort_order
          ...
      ```
      
      ## Real-World Tool Examples
      
      ### File System Tools
      
      ```python
      import os
      import stat
      
      @mcp.tool()
      def read_file(path: str, encoding: str = "utf-8") -> str:
          """Read the contents of a file.
      
          Args:
              path: Absolute path to the file
              encoding: File encoding (default: utf-8)
          """
          path = os.path.realpath(path)
          if not os.path.isfile(path):
              raise FileNotFoundError(f"File not found: {path}")
      
          file_size = os.path.getsize(path)
          if file_size > 10 * 1024 * 1024:  # 10MB limit
              raise ValueError(f"File too large ({file_size} bytes). Maximum is 10MB.")
      
          with open(path, encoding=encoding) as f:
              return f.read()
      
      @mcp.tool()
      def write_file(path: str, content: str, create_dirs: bool = False) -> str:
          """Write content to a file.
      
          Args:
              path: Absolute path to write to
              content: Content to write
              create_dirs: Create parent directories if they don't exist
          """
          path = os.path.realpath(path)
          if create_dirs:
              os.makedirs(os.path.dirname(path), exist_ok=True)
          with open(path, "w") as f:
              f.write(content)
          return f"Wrote {len(content)} bytes to {path}"
      
      @mcp.tool()
      def list_directory(path: str, show_hidden: bool = False) -> str:
          """List files and directories at the given path.
      
          Args:
              path: Directory path to list
              show_hidden: Include hidden files (starting with .)
          """
          path = os.path.realpath(path)
          entries = []
          for entry in sorted(os.listdir(path)):
              if not show_hidden and entry.startswith("."):
                  continue
              full_path = os.path.join(path, entry)
              info = os.stat(full_path)
              entry_type = "dir" if stat.S_ISDIR(info.st_mode) else "file"
              size = info.st_size if entry_type == "file" else ""
              entries.append(f"{'[D]' if entry_type == 'dir' else '[F]'} {entry} {size}")
          return "\n".join(entries) if entries else "(empty directory)"
      ```
      
      ### Database Query Tool
      
      ```python
      import sqlite3
      import json
      
      @mcp.tool()
      def query_sqlite(
          db_path: str,
          sql: str,
          params: list = None,
      ) -> str:
          """Execute a read-only SQL query against a SQLite database.
      
          Args:
              db_path: Path to the SQLite database file
              sql: SQL query (SELECT only)
              params: Optional query parameters for parameterized queries
          """
          sql_stripped = sql.strip().upper()
          if not sql_stripped.startswith("SELECT") and not sql_stripped.startswith("WITH"):
              raise ValueError("Only SELECT and WITH (CTE) queries are allowed")
      
          conn = sqlite3.connect(db_path)
          conn.row_factory = sqlite3.Row
          try:
              cursor = conn.execute(sql, params or [])
              rows = [dict(row) for row in cursor.fetchall()]
              columns = [desc[0] for desc in cursor.description] if cursor.description else []
              return json.dumps({
                  "columns": columns,
                  "rows": rows,
                  "row_count": len(rows),
              }, indent=2, default=str)
          finally:
              conn.close()
      ```
      
      ### API Wrapper Tool
      
      ```python
      import httpx
      import os
      
      @mcp.tool()
      async def github_search(
          query: str,
          search_type: str = "repositories",
          per_page: int = 10,
      ) -> str:
          """Search GitHub for repositories, code, or issues.
      
          Args:
              query: GitHub search query
              search_type: Type of search: repositories, code, issues
              per_page: Results per page (1-100)
          """
          if search_type not in ("repositories", "code", "issues"):
              raise ValueError(f"Invalid search type: {search_type}")
          if not 1 <= per_page <= 100:
              raise ValueError("per_page must be between 1 and 100")
      
          token = os.environ.get("GITHUB_TOKEN")
          headers = {"Accept": "application/vnd.github.v3+json"}
          if token:
              headers["Authorization"] = f"Bearer {token}"
      
          async with httpx.AsyncClient() as client:
              resp = await client.get(
                  f"https://api.github.com/search/{search_type}",
                  params={"q": query, "per_page": per_page},
                  headers=headers,
                  timeout=30.0,
              )
              resp.raise_for_status()
              data = resp.json()
      
          items = data.get("items", [])
          if search_type == "repositories":
              results = [
                  f"- [{item['full_name']}]({item['html_url']}) "
                  f"({item['stargazers_count']} stars): {item.get('description', 'No description')}"
                  for item in items
              ]
          elif search_type == "issues":
              results = [
                  f"- [{item['title']}]({item['html_url']}) "
                  f"({item['state']}): {item['repository_url'].split('/')[-1]}"
                  for item in items
              ]
          else:
              results = [f"- {item['path']} in {item['repository']['full_name']}" for item in items]
      
          return f"Found {data['total_count']} results:\n" + "\n".join(results)
      ```
      
      ### Web Scraping Tool
      
      ```python
      import httpx
      from html.parser import HTMLParser
      
      class TextExtractor(HTMLParser):
          def __init__(self):
              super().__init__()
              self.text_parts = []
              self._skip = False
              self._skip_tags = {"script", "style", "noscript"}
      
          def handle_starttag(self, tag, attrs):
              if tag in self._skip_tags:
                  self._skip = True
      
          def handle_endtag(self, tag):
              if tag in self._skip_tags:
                  self._skip = False
      
          def handle_data(self, data):
              if not self._skip:
                  text = data.strip()
                  if text:
                      self.text_parts.append(text)
      
      @mcp.tool()
      async def fetch_webpage(url: str, extract_text: bool = True) -> str:
          """Fetch a webpage and optionally extract its text content.
      
          Args:
              url: URL to fetch
              extract_text: If true, extract text only; if false, return raw HTML
          """
          if not url.startswith(("http://", "https://")):
              raise ValueError("URL must start with http:// or https://")
      
          async with httpx.AsyncClient(follow_redirects=True) as client:
              resp = await client.get(url, timeout=30.0, headers={
                  "User-Agent": "MCP-Server/1.0 (compatible; tool-fetch)"
              })
              resp.raise_for_status()
      
          if not extract_text:
              return resp.text[:50000]  # Limit raw HTML size
      
          extractor = TextExtractor()
          extractor.feed(resp.text)
          text = "\n".join(extractor.text_parts)
      
          # Truncate if too long
          if len(text) > 30000:
              text = text[:30000] + "\n\n[Truncated - content exceeds 30KB]"
      
          return text
      ```
      
    • transport-auth.md 20.8 KB
      # Transport and Authentication
      
      ## stdio Transport
      
      ### How It Works
      
      The stdio transport communicates via **stdin** (client-to-server) and **stdout** (server-to-client). Each message is a JSON-RPC 2.0 object, one per line (newline-delimited).
      
      ```
      Host Process                    MCP Server Process
      ┌──────────┐                    ┌──────────┐
      │          │ ─── stdin ──────→  │          │
      │  Client  │                    │  Server  │
      │          │ ←── stdout ─────── │          │
      └──────────┘                    └──────────┘
                                      stderr → logs
      ```
      
      **Key characteristics:**
      - One client per server process (1:1 mapping)
      - Host manages process lifecycle (spawn, restart, kill)
      - No networking - everything is local
      - stderr is used for logging (not protocol messages)
      - Simplest transport, best for CLI tools and desktop integrations
      
      ### Python stdio Server
      
      ```python
      from mcp.server.fastmcp import FastMCP
      
      mcp = FastMCP("my-server")
      
      @mcp.tool()
      def hello(name: str) -> str:
          """Say hello."""
          return f"Hello, {name}!"
      
      if __name__ == "__main__":
          mcp.run()  # stdio is the default transport
      ```
      
      ### TypeScript stdio Server
      
      ```typescript
      import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
      import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
      
      const server = new McpServer({ name: "my-server", version: "1.0.0" });
      // ... register tools ...
      
      const transport = new StdioServerTransport();
      await server.connect(transport);
      ```
      
      ### Claude Desktop Configuration
      
      Location: `~/Library/Application Support/Claude/claude_desktop_config.json` (macOS) or `%APPDATA%/Claude/claude_desktop_config.json` (Windows)
      
      ```json
      {
        "mcpServers": {
          "my-python-server": {
            "command": "uv",
            "args": ["run", "--directory", "/absolute/path/to/server", "python", "server.py"],
            "env": {
              "API_KEY": "sk-your-key",
              "LOG_LEVEL": "INFO"
            }
          },
          "my-node-server": {
            "command": "npx",
            "args": ["tsx", "/absolute/path/to/server/index.ts"],
            "env": {
              "API_KEY": "sk-your-key"
            }
          },
          "published-server": {
            "command": "uvx",
            "args": ["my-published-mcp-server"],
            "env": {}
          }
        }
      }
      ```
      
      ### Claude Code Configuration
      
      ```json
      // .claude/settings.json (project-level)
      {
        "mcpServers": {
          "my-server": {
            "command": "uv",
            "args": ["run", "--directory", "/path/to/server", "python", "server.py"],
            "env": {
              "API_KEY": "sk-your-key"
            }
          }
        }
      }
      ```
      
      CLI shortcuts:
      
      ```bash
      # Add a server
      claude mcp add my-server -- uv run --directory /path/to/server python server.py
      
      # Add with environment variables
      claude mcp add my-server -e API_KEY=sk-key -- uv run python server.py
      
      # List configured servers
      claude mcp list
      
      # Remove a server
      claude mcp remove my-server
      ```
      
      ## SSE Transport
      
      ### How It Works
      
      SSE (Server-Sent Events) uses HTTP for client-to-server requests and an EventSource stream for server-to-client messages.
      
      ```
      Client                              Server
      ┌──────────┐                        ┌──────────┐
      │          │ ── HTTP POST /sse ───→  │          │
      │          │ ←── SSE event stream ── │          │
      │          │                         │          │
      │          │ ── HTTP POST /msg ───→  │          │
      │          │ ←── (via SSE stream) ── │          │
      └──────────┘                        └──────────┘
      ```
      
      **Key characteristics:**
      - HTTP-based, works through firewalls and proxies
      - Server pushes events to client via EventSource
      - Client sends requests via HTTP POST
      - Multiple clients can connect simultaneously
      - Good for development servers and internal tools
      
      ### Python SSE Server
      
      ```python
      from mcp.server.fastmcp import FastMCP
      
      mcp = FastMCP("sse-server")
      
      @mcp.tool()
      def hello(name: str) -> str:
          return f"Hello, {name}!"
      
      if __name__ == "__main__":
          mcp.run(transport="sse", host="0.0.0.0", port=8000)
      ```
      
      ### TypeScript SSE Server
      
      ```typescript
      import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
      import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
      import express from "express";
      
      const app = express();
      const server = new McpServer({ name: "sse-server", version: "1.0.0" });
      
      // ... register tools ...
      
      app.get("/sse", async (req, res) => {
        const transport = new SSEServerTransport("/messages", res);
        await server.connect(transport);
      });
      
      app.post("/messages", async (req, res) => {
        // Handle incoming messages from client
        await transport.handlePostMessage(req, res);
      });
      
      app.listen(8000, () => console.log("SSE server running on port 8000"));
      ```
      
      ### SSE Reconnection
      
      SSE connections can drop. The EventSource API handles automatic reconnection:
      
      ```typescript
      // Client-side: EventSource handles reconnection automatically
      const eventSource = new EventSource("http://localhost:8000/sse");
      eventSource.onopen = () => console.log("Connected");
      eventSource.onerror = (e) => console.log("Connection lost, reconnecting...");
      ```
      
      Server-side, send keepalive comments to prevent connection timeout:
      
      ```python
      # FastMCP handles this internally when using SSE transport
      # For custom implementations, send periodic comments:
      # : keepalive\n\n
      ```
      
      ## Streamable HTTP Transport
      
      ### How It Works
      
      Streamable HTTP uses standard HTTP POST for requests and SSE for streaming responses. It supports both stateful (session-based) and stateless modes.
      
      ```
      Client                              Server
      ┌──────────┐                        ┌──────────┐
      │          │ ── POST /mcp ────────→ │          │
      │          │    (JSON-RPC request)   │          │
      │          │                         │          │
      │          │ ←── SSE stream ──────── │          │
      │          │    (JSON-RPC response)  │          │
      │          │    (+ notifications)    │          │
      └──────────┘                        └──────────┘
      
      Session management via Mcp-Session-Id header
      ```
      
      **Key characteristics:**
      - Single HTTP endpoint for all communication
      - Responses can be regular HTTP or SSE streams
      - Session IDs for stateful operation, or fully stateless
      - Load balancer and CDN friendly
      - Full authentication support
      - Recommended for production deployments
      
      ### Python Streamable HTTP Server
      
      ```python
      from mcp.server.fastmcp import FastMCP
      
      mcp = FastMCP("http-server")
      
      @mcp.tool()
      def hello(name: str) -> str:
          return f"Hello, {name}!"
      
      if __name__ == "__main__":
          mcp.run(transport="streamable-http", host="0.0.0.0", port=8000)
      ```
      
      ### TypeScript Streamable HTTP Server
      
      ```typescript
      import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
      import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
      import express from "express";
      
      const app = express();
      app.use(express.json());
      
      const server = new McpServer({ name: "http-server", version: "1.0.0" });
      // ... register tools ...
      
      app.post("/mcp", async (req, res) => {
        const transport = new StreamableHTTPServerTransport({
          sessionIdGenerator: () => crypto.randomUUID(),
        });
        await server.connect(transport);
        await transport.handleRequest(req, res);
      });
      
      app.listen(8000);
      ```
      
      ### Stateless Mode
      
      For serverless or horizontally scaled deployments:
      
      ```typescript
      const transport = new StreamableHTTPServerTransport({
        sessionIdGenerator: undefined,  // No session tracking
      });
      ```
      
      In stateless mode:
      - No `Mcp-Session-Id` header
      - Each request is independent
      - Server reconstructs state from request context
      - Works with any load balancer without sticky sessions
      
      ## Transport Selection Guide
      
      | Scenario | Transport | Why |
      |----------|-----------|-----|
      | Claude Desktop integration | stdio | Native support, simplest setup |
      | Claude Code tool | stdio | Direct process management |
      | VS Code extension backend | stdio | Process managed by extension |
      | Internal dashboard | SSE | Browser-friendly, real-time updates |
      | Development/testing | SSE | Easy to inspect with browser |
      | Production API | Streamable HTTP | Auth, scaling, load balancing |
      | Serverless (Lambda, Workers) | Streamable HTTP (stateless) | No persistent connections needed |
      | Multi-tenant SaaS | Streamable HTTP | Session isolation, auth per tenant |
      | Mobile app backend | Streamable HTTP | Standard HTTP, auth support |
      
      ## Session Management
      
      ### Session IDs
      
      For stateful HTTP transports, each client gets a unique session:
      
      ```typescript
      const transport = new StreamableHTTPServerTransport({
        sessionIdGenerator: () => crypto.randomUUID(),
        // Optional: validate session IDs
        sessionValidator: async (sessionId) => {
          return await sessionStore.exists(sessionId);
        },
      });
      ```
      
      ### Session State
      
      ```python
      # Store per-session state
      class SessionState:
          def __init__(self):
              self.user_id: str | None = None
              self.preferences: dict = {}
              self.history: list = []
      
      # In FastMCP, access via context
      @mcp.tool()
      async def set_preference(key: str, value: str, ctx: Context) -> str:
          session = ctx.request_context.session
          if not hasattr(session, "state"):
              session.state = SessionState()
          session.state.preferences[key] = value
          return f"Set {key} = {value}"
      ```
      
      ### Session Cleanup
      
      ```python
      from contextlib import asynccontextmanager
      import asyncio
      
      @asynccontextmanager
      async def lifespan(server: FastMCP):
          # Start cleanup task
          cleanup_task = asyncio.create_task(cleanup_expired_sessions())
          try:
              yield
          finally:
              cleanup_task.cancel()
      
      async def cleanup_expired_sessions():
          while True:
              await asyncio.sleep(300)  # Every 5 minutes
              expired = session_store.get_expired(max_age_seconds=3600)
              for session_id in expired:
                  session_store.remove(session_id)
      ```
      
      ## Authentication
      
      ### API Keys in Environment Variables
      
      The simplest auth pattern - pass credentials via environment configuration:
      
      ```python
      import os
      
      @mcp.tool()
      async def call_api(endpoint: str) -> str:
          """Call an authenticated API."""
          api_key = os.environ.get("API_KEY")
          if not api_key:
              raise RuntimeError("API_KEY environment variable not set")
      
          async with httpx.AsyncClient() as client:
              resp = await client.get(
                  f"https://api.example.com/{endpoint}",
                  headers={"Authorization": f"Bearer {api_key}"},
                  timeout=30.0,
              )
              resp.raise_for_status()
              return resp.text
      ```
      
      Client config passes the key:
      
      ```json
      {
        "mcpServers": {
          "api-server": {
            "command": "python",
            "args": ["server.py"],
            "env": {
              "API_KEY": "sk-your-api-key-here"
            }
          }
        }
      }
      ```
      
      ### Bearer Token Authentication (HTTP transports)
      
      For SSE and Streamable HTTP, authenticate incoming requests:
      
      ```python
      from starlette.middleware.base import BaseHTTPMiddleware
      from starlette.requests import Request
      from starlette.responses import JSONResponse
      
      class AuthMiddleware(BaseHTTPMiddleware):
          async def dispatch(self, request: Request, call_next):
              # Skip auth for health checks
              if request.url.path == "/health":
                  return await call_next(request)
      
              auth_header = request.headers.get("Authorization")
              if not auth_header or not auth_header.startswith("Bearer "):
                  return JSONResponse({"error": "Missing or invalid Authorization header"}, status_code=401)
      
              token = auth_header[7:]
              if not await validate_token(token):
                  return JSONResponse({"error": "Invalid token"}, status_code=403)
      
              # Attach user info to request state
              request.state.user = await get_user_from_token(token)
              return await call_next(request)
      ```
      
      ### OAuth2 PKCE Flow
      
      For MCP servers that need user-level authentication:
      
      ```python
      import secrets
      import hashlib
      import base64
      from urllib.parse import urlencode
      
      class OAuth2PKCEFlow:
          def __init__(self, client_id: str, auth_url: str, token_url: str, redirect_uri: str):
              self.client_id = client_id
              self.auth_url = auth_url
              self.token_url = token_url
              self.redirect_uri = redirect_uri
      
          def generate_auth_url(self) -> tuple[str, str]:
              """Generate authorization URL with PKCE challenge."""
              code_verifier = secrets.token_urlsafe(64)
              code_challenge = base64.urlsafe_b64encode(
                  hashlib.sha256(code_verifier.encode()).digest()
              ).rstrip(b"=").decode()
      
              params = urlencode({
                  "response_type": "code",
                  "client_id": self.client_id,
                  "redirect_uri": self.redirect_uri,
                  "code_challenge": code_challenge,
                  "code_challenge_method": "S256",
                  "scope": "read write",
              })
              return f"{self.auth_url}?{params}", code_verifier
      
          async def exchange_code(self, code: str, code_verifier: str) -> dict:
              """Exchange authorization code for tokens."""
              async with httpx.AsyncClient() as client:
                  resp = await client.post(self.token_url, data={
                      "grant_type": "authorization_code",
                      "client_id": self.client_id,
                      "code": code,
                      "redirect_uri": self.redirect_uri,
                      "code_verifier": code_verifier,
                  })
                  resp.raise_for_status()
                  return resp.json()
      
          async def refresh_token(self, refresh_token: str) -> dict:
              """Refresh an expired access token."""
              async with httpx.AsyncClient() as client:
                  resp = await client.post(self.token_url, data={
                      "grant_type": "refresh_token",
                      "client_id": self.client_id,
                      "refresh_token": refresh_token,
                  })
                  resp.raise_for_status()
                  return resp.json()
      ```
      
      ### Token Management
      
      ```python
      import time
      import asyncio
      
      class TokenManager:
          """Manage OAuth2 tokens with automatic refresh."""
      
          def __init__(self, oauth: OAuth2PKCEFlow):
              self.oauth = oauth
              self._tokens: dict = {}
              self._lock = asyncio.Lock()
      
          async def get_token(self) -> str:
              """Get a valid access token, refreshing if needed."""
              async with self._lock:
                  if self._is_valid():
                      return self._tokens["access_token"]
      
                  if "refresh_token" in self._tokens:
                      self._tokens = await self.oauth.refresh_token(self._tokens["refresh_token"])
                      self._tokens["obtained_at"] = time.time()
                      return self._tokens["access_token"]
      
                  raise RuntimeError("No valid token available. User must re-authenticate.")
      
          def _is_valid(self) -> bool:
              if "access_token" not in self._tokens:
                  return False
              expires_at = self._tokens.get("obtained_at", 0) + self._tokens.get("expires_in", 0)
              return time.time() < expires_at - 60  # 60s buffer
      
          def set_tokens(self, tokens: dict):
              """Store tokens after initial authorization."""
              self._tokens = {**tokens, "obtained_at": time.time()}
      ```
      
      ## Rate Limiting
      
      ### Per-Client Rate Limiting
      
      ```python
      import time
      from collections import defaultdict
      
      class SlidingWindowRateLimiter:
          def __init__(self, max_requests: int, window_seconds: int):
              self.max_requests = max_requests
              self.window = window_seconds
              self.requests: dict[str, list[float]] = defaultdict(list)
      
          def allow(self, client_id: str) -> bool:
              now = time.time()
              # Remove expired entries
              self.requests[client_id] = [
                  t for t in self.requests[client_id]
                  if now - t < self.window
              ]
              if len(self.requests[client_id]) >= self.max_requests:
                  return False
              self.requests[client_id].append(now)
              return True
      
          def remaining(self, client_id: str) -> int:
              now = time.time()
              active = [t for t in self.requests[client_id] if now - t < self.window]
              return max(0, self.max_requests - len(active))
      
      # Usage in tools
      rate_limiter = SlidingWindowRateLimiter(max_requests=100, window_seconds=60)
      
      @mcp.tool()
      async def rate_limited_tool(query: str, ctx: Context) -> str:
          """A rate-limited tool."""
          client_id = str(id(ctx.request_context.session))
          if not rate_limiter.allow(client_id):
              remaining_wait = rate_limiter.window
              raise RuntimeError(f"Rate limit exceeded. Try again in {remaining_wait}s.")
          return await process(query)
      ```
      
      ### Per-Tool Rate Limiting
      
      ```python
      from functools import wraps
      
      def rate_limit(max_calls: int, window: int):
          """Decorator to rate-limit individual tools."""
          limiter = SlidingWindowRateLimiter(max_calls, window)
      
          def decorator(func):
              @wraps(func)
              async def wrapper(*args, **kwargs):
                  tool_name = func.__name__
                  if not limiter.allow(tool_name):
                      raise RuntimeError(f"Tool {tool_name} rate limit exceeded ({max_calls}/{window}s)")
                  return await func(*args, **kwargs) if asyncio.iscoroutinefunction(func) else func(*args, **kwargs)
              return wrapper
          return decorator
      
      @mcp.tool()
      @rate_limit(max_calls=10, window=60)
      async def expensive_api_call(query: str) -> str:
          """Call an expensive API (limited to 10 calls/minute)."""
          ...
      ```
      
      ## CORS Configuration
      
      For web-based clients connecting to SSE or HTTP servers:
      
      ```python
      from starlette.middleware.cors import CORSMiddleware
      
      # If using Starlette/FastAPI alongside FastMCP
      app.add_middleware(
          CORSMiddleware,
          allow_origins=["http://localhost:3000", "https://myapp.example.com"],
          allow_credentials=True,
          allow_methods=["GET", "POST", "OPTIONS"],
          allow_headers=["Authorization", "Content-Type", "Mcp-Session-Id"],
      )
      ```
      
      ```typescript
      // Express CORS
      import cors from "cors";
      app.use(cors({
        origin: ["http://localhost:3000", "https://myapp.example.com"],
        credentials: true,
        allowedHeaders: ["Authorization", "Content-Type", "Mcp-Session-Id"],
      }));
      ```
      
      ## TLS/HTTPS for Production
      
      Always use HTTPS for non-stdio transports in production:
      
      ```python
      # Using uvicorn with TLS
      import uvicorn
      
      if __name__ == "__main__":
          uvicorn.run(
              app,
              host="0.0.0.0",
              port=443,
              ssl_keyfile="/path/to/key.pem",
              ssl_certfile="/path/to/cert.pem",
          )
      ```
      
      Or terminate TLS at a reverse proxy (recommended):
      
      ```nginx
      # nginx reverse proxy for MCP server
      server {
          listen 443 ssl;
          server_name mcp.example.com;
      
          ssl_certificate /etc/letsencrypt/live/mcp.example.com/fullchain.pem;
          ssl_certificate_key /etc/letsencrypt/live/mcp.example.com/privkey.pem;
      
          location / {
              proxy_pass http://localhost:8000;
              proxy_http_version 1.1;
              proxy_set_header Upgrade $http_upgrade;
              proxy_set_header Connection "upgrade";
              proxy_set_header Host $host;
              proxy_set_header X-Real-IP $remote_addr;
              proxy_buffering off;  # Important for SSE
              proxy_cache off;      # Don't cache SSE streams
          }
      }
      ```
      
      ## Proxy Patterns
      
      ### Reverse Proxy for Multiple MCP Servers
      
      Route requests to different MCP servers based on path:
      
      ```nginx
      # Serve multiple MCP servers from one domain
      server {
          listen 443 ssl;
          server_name mcp.example.com;
      
          # Server A: database tools
          location /db/ {
              proxy_pass http://localhost:8001/;
              proxy_buffering off;
          }
      
          # Server B: file system tools
          location /files/ {
              proxy_pass http://localhost:8002/;
              proxy_buffering off;
          }
      
          # Server C: API integration tools
          location /api/ {
              proxy_pass http://localhost:8003/;
              proxy_buffering off;
          }
      }
      ```
      
      ### Gateway Pattern
      
      A single MCP server that routes to backend services:
      
      ```python
      from mcp.server.fastmcp import FastMCP
      import httpx
      
      mcp = FastMCP("gateway")
      
      # Route tool calls to backend MCP servers
      BACKENDS = {
          "db_": "http://localhost:8001",
          "file_": "http://localhost:8002",
          "api_": "http://localhost:8003",
      }
      
      @mcp.tool()
      async def gateway_call(tool_name: str, arguments: dict) -> str:
          """Route a tool call to the appropriate backend.
      
          Args:
              tool_name: Full tool name (e.g., db_query, file_read)
              arguments: Tool arguments as a JSON object
          """
          for prefix, backend_url in BACKENDS.items():
              if tool_name.startswith(prefix):
                  async with httpx.AsyncClient() as client:
                      resp = await client.post(
                          f"{backend_url}/mcp",
                          json={
                              "jsonrpc": "2.0",
                              "id": 1,
                              "method": "tools/call",
                              "params": {"name": tool_name, "arguments": arguments},
                          },
                      )
                      return resp.json()["result"]["content"][0]["text"]
          raise ValueError(f"No backend found for tool: {tool_name}")
      ```
      
  • scripts
    • .gitkeep 0 B · in bundle
    • check-mcp-facts.py 11.7 KB
      #!/usr/bin/env python3
      """Staleness verifier for mcp-ops: the MCP SDK packages + spec URL the skill
      names must stay real, named, and current.
      
      mcp-ops tells the reader to install `@modelcontextprotocol/sdk` (npm),
      `@modelcontextprotocol/inspector` (npm), `mcp[cli]` / `fastmcp` (PyPI), and
      cites the spec at spec.modelcontextprotocol.io. Those are exactly the facts
      that drift silently (SKILL-RESOURCE-PROTOCOL.md §7): an SDK is renamed, a
      package is unpublished, the prose stops mentioning one the catalog still
      lists, or the spec URL goes dead — and nobody notices for months. Two modes:
      
        --offline (default, safe for PR CI): structural consistency, no network.
          * assets/mcp-facts.json parses and carries the schema + an as_of date
          * every catalogued package's prose_token is still named somewhere in the
            skill prose (SKILL.md + references/*.md) — the catalog can't drift from
            the docs
          * the spec URL token is still cited in the prose
          * SKILL.md still carries a dated "verified as of <year>" currency note
        --live (scheduled freshness job, never a PR gate): does each package still
          resolve on its registry (npm latest / PyPI JSON), and has any tracked
          SDK's major moved off the sampled major? Does the spec URL answer 200?
      
      Usage:   check-mcp-facts.py [--offline | --live] [--catalog FILE] [--skill DIR] [--json] [--timeout S]
      Input:   argv flags only (no stdin).
      Output:  stdout = findings (plain rows, or a --json envelope). Data only.
      Stderr:  the verdict line, notices, errors.
      Exit:    0 ok, 2 usage, 3 catalog/skill missing, 4 catalog unparseable,
               7 registries/spec unreachable (live, advisory — never a real failure),
               10 drift found (offline: unnamed/currency-note gone; live: package
                  gone, SDK major moved, or spec URL dead)
      
      Examples:
        check-mcp-facts.py --offline                 # PR CI: catalog ⇆ prose consistency
        check-mcp-facts.py --live                    # weekly: SDKs still resolve + spec 200
        check-mcp-facts.py --offline --json | jq '.data[]'
      """
      from __future__ import annotations
      
      import argparse
      import json
      import re
      import sys
      import urllib.error
      import urllib.parse
      import urllib.request
      from pathlib import Path
      
      EX_OK = 0
      EX_USAGE = 2
      EX_NOTFOUND = 3
      EX_UNPARSEABLE = 4
      EX_UNAVAILABLE = 7
      EX_DRIFT = 10
      
      SCHEMA = "claude-mods.mcp-ops.facts/v1"
      
      HERE = Path(__file__).resolve().parent
      DEFAULT_CATALOG = HERE.parent / "assets" / "mcp-facts.json"
      DEFAULT_SKILL = HERE.parent
      
      NPM_REGISTRY = "https://registry.npmjs.org"
      PYPI_REGISTRY = "https://pypi.org/pypi"
      
      CURRENCY_RE = re.compile(r"verified as of\s+(\d{4})", re.IGNORECASE)
      AS_OF_RE = re.compile(r"^\d{4}-\d{2}-\d{2}$")
      
      
      class Finding:
          __slots__ = ("check", "status", "detail")
      
          def __init__(self, check: str, status: str, detail: str) -> None:
              self.check = check
              self.status = status  # ok | drift | unavailable
              self.detail = detail
      
          def as_dict(self) -> dict:
              return {"check": self.check, "status": self.status, "detail": self.detail}
      
      
      def load_catalog(path: Path) -> dict:
          if not path.is_file():
              print(f"error: facts catalog not found: {path}", file=sys.stderr)
              raise SystemExit(EX_NOTFOUND)
          try:
              data = json.loads(path.read_text(encoding="utf-8"))
              if not isinstance(data, dict) or data.get("schema") != SCHEMA:
                  raise ValueError(f"schema must be {SCHEMA!r}")
              if not AS_OF_RE.match(str(data.get("as_of", ""))):
                  raise ValueError(f"as_of must be YYYY-MM-DD, got {data.get('as_of')!r}")
              if not isinstance(data.get("packages"), dict) or not data["packages"]:
                  raise ValueError("'packages' must be a non-empty object")
              return data
          except (json.JSONDecodeError, KeyError, TypeError, ValueError) as exc:
              print(f"error: could not parse catalog {path}: {exc}", file=sys.stderr)
              raise SystemExit(EX_UNPARSEABLE)
      
      
      def read_corpus(skill_dir: Path) -> tuple[str, str]:
          """Returns (skill_md_text, all_prose_text) across SKILL.md + references/*.md."""
          doc = skill_dir / "SKILL.md"
          if not doc.is_file():
              print(f"error: SKILL.md not found under {skill_dir}", file=sys.stderr)
              raise SystemExit(EX_NOTFOUND)
          skill_md = doc.read_text(encoding="utf-8", errors="replace")
          parts = [skill_md]
          ref_dir = skill_dir / "references"
          if ref_dir.is_dir():
              for ref in sorted(ref_dir.glob("*.md")):
                  parts.append(ref.read_text(encoding="utf-8", errors="replace"))
          return skill_md, "\n".join(parts)
      
      
      def check_offline(catalog: dict, skill_dir: Path) -> list[Finding]:
          skill_md, corpus = read_corpus(skill_dir)
          findings: list[Finding] = []
          lower_corpus = corpus.lower()
      
          # Currency note still dated.
          if CURRENCY_RE.search(skill_md):
              m = CURRENCY_RE.search(skill_md)
              findings.append(Finding("currency-note", "ok", f"currency note dated {m.group(1)}"))
          else:
              findings.append(Finding("currency-note", "drift",
                                      "no dated 'verified as of <year>' currency note in SKILL.md"))
      
          # Spec URL token still cited.
          spec = catalog.get("spec", {})
          spec_token = str(spec.get("prose_token", ""))
          if spec_token and spec_token.lower() in lower_corpus:
              findings.append(Finding("spec-cited", "ok", f"{spec_token} named in prose"))
          else:
              findings.append(Finding("spec-cited", "drift",
                                      f"spec token {spec_token!r} no longer named in skill prose"))
      
          # Every catalogued package still named in the prose.
          for name, meta in catalog.get("packages", {}).items():
              token = str(meta.get("prose_token", name))
              if token.lower() in lower_corpus:
                  findings.append(Finding(f"pkg-named:{name}", "ok", "named in skill prose"))
              else:
                  findings.append(Finding(f"pkg-named:{name}", "drift",
                                          f"prose_token {token!r} not found in skill prose"))
          return findings
      
      
      def _fetch(url: str, timeout: float, accept: str = "application/json") -> tuple[str, object]:
          """Return (ok|notfound|unavailable, payload-or-status)."""
          req = urllib.request.Request(url, headers={"User-Agent": "claude-mods-mcp-ops-check/1",
                                                     "Accept": accept})
          try:
              with urllib.request.urlopen(req, timeout=timeout) as resp:
                  body = resp.read().decode("utf-8", errors="replace")
                  return "ok", body
          except urllib.error.HTTPError as exc:
              if exc.code in (404, 410):
                  return "notfound", exc.code
              return "unavailable", exc.code
          except (urllib.error.URLError, TimeoutError, OSError):
              return "unavailable", None
      
      
      def _npm_latest(pkg: str, timeout: float) -> tuple[str, str]:
          """Return (status, version-or-status-detail). status in ok|notfound|unavailable."""
          url = f"{NPM_REGISTRY}/{urllib.parse.quote(pkg, safe='')}/latest"
          status, payload = _fetch(url, timeout)
          if status != "ok":
              return status, str(payload)
          try:
              ver = json.loads(payload).get("version", "")
              return "ok", ver
          except json.JSONDecodeError:
              return "unavailable", "bad-json"
      
      
      def _pypi_latest(pkg: str, timeout: float) -> tuple[str, str]:
          url = f"{PYPI_REGISTRY}/{urllib.parse.quote(pkg, safe='')}/json"
          status, payload = _fetch(url, timeout)
          if status != "ok":
              return status, str(payload)
          try:
              ver = json.loads(payload).get("info", {}).get("version", "")
              return "ok", ver
          except json.JSONDecodeError:
              return "unavailable", "bad-json"
      
      
      def _major(ver: str) -> str:
          """Leading integer component of a version string ('1.2.3' -> '1')."""
          m = re.match(r"\s*(\d+)", ver)
          return m.group(1) if m else ""
      
      
      def check_live(catalog: dict, timeout: float) -> list[Finding]:
          findings: list[Finding] = []
      
          for name, meta in catalog.get("packages", {}).items():
              registry = meta.get("registry", "npm")
              if registry == "npm":
                  status, info = _npm_latest(name, timeout)
              else:
                  status, info = _pypi_latest(name, timeout)
              if status == "notfound":
                  findings.append(Finding(f"npm:{name}", "drift",
                                          "package gone from registry — renamed/removed, review skill"))
                  continue
              if status == "unavailable":
                  findings.append(Finding(f"npm:{name}", "unavailable", "registry unreachable"))
                  continue
              ver = str(info)
              if meta.get("track_major"):
                  sampled = str(meta.get("sampled_major", ""))
                  latest_major = _major(ver)
                  if latest_major and latest_major != sampled:
                      findings.append(Finding(f"npm:{name}", "drift",
                                              f"{name}@{ver} major {latest_major} != sampled {sampled} — review skill"))
                  else:
                      findings.append(Finding(f"npm:{name}", "ok", f"latest {ver} (major {latest_major})"))
              else:
                  findings.append(Finding(f"npm:{name}", "ok", f"latest {ver}"))
      
          # Spec URL must answer 200 (follows redirects).
          spec_url = str(catalog.get("spec", {}).get("url", ""))
          if spec_url:
              status, info = _fetch(spec_url, timeout, accept="text/html,application/json")
              if status == "ok":
                  findings.append(Finding("spec-url", "ok", f"{spec_url} answered 200"))
              elif status == "notfound":
                  findings.append(Finding("spec-url", "drift", f"{spec_url} returned 4xx — spec URL dead"))
              else:
                  findings.append(Finding("spec-url", "unavailable", f"{spec_url} unreachable"))
          return findings
      
      
      def main(argv: list[str]) -> int:
          p = argparse.ArgumentParser(
              prog="check-mcp-facts.py",
              description="Verify mcp-ops' SDK packages + spec URL stay named (offline) and live (live).",
          )
          mode = p.add_mutually_exclusive_group()
          mode.add_argument("--offline", action="store_true", help="structural consistency, no network (default)")
          mode.add_argument("--live", action="store_true", help="probe npm/PyPI registries + the spec URL")
          p.add_argument("--catalog", default=str(DEFAULT_CATALOG), help="facts catalog JSON")
          p.add_argument("--skill", default=str(DEFAULT_SKILL), help="skill directory (SKILL.md + references/)")
          p.add_argument("--timeout", type=float, default=10.0, help="per-request timeout seconds (live)")
          p.add_argument("--json", action="store_true", help="emit a JSON envelope")
          p.add_argument("-q", "--quiet", action="store_true", help="suppress stderr progress/summary")
          try:
              args = p.parse_args(argv)
          except SystemExit as exc:
              return EX_USAGE if exc.code not in (0, None) else (exc.code or EX_OK)
      
          catalog = load_catalog(Path(args.catalog))
          live = args.live and not args.offline
          mode_name = "live" if live else "offline"
          findings = check_live(catalog, args.timeout) if live else check_offline(catalog, Path(args.skill))
      
          n_drift = sum(1 for f in findings if f.status == "drift")
          n_unavail = sum(1 for f in findings if f.status == "unavailable")
      
          if args.json:
              print(json.dumps({
                  "data": [f.as_dict() for f in findings],
                  "meta": {"mode": mode_name, "count": len(findings),
                           "drift": n_drift, "unavailable": n_unavail, "schema": SCHEMA},
              }, indent=2))
          else:
              for f in findings:
                  print(f"{f.check}\t{f.status}\t{f.detail}")
      
          for f in findings:
              if f.status != "ok":
                  print(f"  [{f.status.upper()}] {f.check}: {f.detail}", file=sys.stderr)
          if not args.quiet:
              print(f"-- {len(findings)} checks: {n_drift} drift, {n_unavail} unavailable", file=sys.stderr)
      
          if n_drift:
              return EX_DRIFT
          if n_unavail:
              return EX_UNAVAILABLE
          return EX_OK
      
      
      if __name__ == "__main__":
          sys.exit(main(sys.argv[1:]))
      
  • tests
    • run.sh 5.2 KB
      #!/usr/bin/env bash
      # Offline self-test for the mcp-ops skill — structure, frontmatter, and the
      # staleness-verifier contract (SKILL-RESOURCE-PROTOCOL §7, §10).
      #
      # Usage:   tests/run.sh
      # Input:   none (self-contained; no network, no MCP server required)
      # Output:  TAP-ish progress on stderr; final PASS/FAIL line.
      # Exit:    0 all pass (or skipped on unsupported platform), 1 any failure.
      #
      # Examples:
      #   tests/run.sh
      #   bash skills/mcp-ops/tests/run.sh
      set -uo pipefail
      
      here="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
      fail=0
      pass=0
      note() { printf '  %s %s\n' "$1" "$2" >&2; }
      ok()   { pass=$((pass+1)); note "ok  " "$1"; }
      bad()  { fail=$((fail+1)); note "FAIL" "$1"; }
      
      # Resolve a *working* python (python3, else python). The bare `command -v` is not
      # enough on Windows, where `python3` is a Microsoft Store stub that exits nonzero.
      PY=""
      for cand in python3 python; do
        if command -v "$cand" >/dev/null 2>&1 && "$cand" --version >/dev/null 2>&1; then
          PY="$cand"; break
        fi
      done
      if [ -z "$PY" ]; then
        echo "SKIP: no working python interpreter on this platform" >&2
        exit 0
      fi
      
      # 1. Required directories exist
      for d in scripts references assets tests; do
        [ -d "$here/$d" ] && ok "dir $d/ exists" || bad "missing dir $d/"
      done
      
      # 2. SKILL.md frontmatter house rules
      skill="$here/SKILL.md"
      if [ -f "$skill" ]; then
        ok "SKILL.md present"
        grep -q '^name: mcp-ops$' "$skill" && ok "name matches directory" || bad "name != mcp-ops"
        grep -q '^license: MIT$' "$skill" && ok "license: MIT" || bad "missing license: MIT"
        grep -q '^  author: claude-mods$' "$skill" && ok "metadata.author" || bad "missing metadata.author"
      else
        bad "SKILL.md missing"
      fi
      
      # 3. Every reference on disk is cited from SKILL.md (no dead weight)
      for ref in "$here"/references/*.md; do
        base="references/$(basename "$ref")"
        grep -qF "$base" "$skill" && ok "cited: $base" || bad "uncited reference: $base"
      done
      
      # 4. Every SKILL.md-cited bundled resource exists on disk
      for res in assets/mcp-facts.json scripts/check-mcp-facts.py; do
        [ -f "$here/$res" ] && ok "resource present: $res" || bad "missing resource: $res"
      done
      
      # 5. check-mcp-facts.py — staleness verifier contract (§7, §10), offline-safe
      verifier="$here/scripts/check-mcp-facts.py"
      catalog="$here/assets/mcp-facts.json"
      ec() { local want="$1" lbl="$2"; shift 2; "$@" >/dev/null 2>&1; local got=$?
             [ "$got" = "$want" ] && ok "$lbl (exit $got)" || bad "$lbl (want $want got $got)"; }
      if [ -f "$verifier" ]; then
        "$PY" -m py_compile "$verifier" && ok "verifier: py_compile clean" || bad "verifier: py_compile failed"
        grep -qE '^Examples:$' "$verifier" && ok "verifier: has Examples block" || bad "verifier: no Examples block (docstring)"
        "$PY" "$verifier" --help >/dev/null 2>&1 && ok "verifier: --help exits 0" || bad "verifier: --help nonzero"
        # Offline mode must pass on the skill's own content (internal consistency).
        ec 0 "verifier: --offline consistent"  "$PY" "$verifier" --offline
        # Bad flag → USAGE (exit 2); conflicting modes → USAGE.
        ec 2 "verifier: bad flag → exit 2"     "$PY" "$verifier" --bogus
        ec 2 "verifier: --offline --live → 2"  "$PY" "$verifier" --offline --live
        # stdout is data-only: --offline --json must emit parseable JSON.
        "$PY" "$verifier" --offline --json -q 2>/dev/null \
          | "$PY" -c 'import json,sys; d=json.load(sys.stdin); assert d["meta"]["schema"]=="claude-mods.mcp-ops.facts/v1"' \
          && ok "verifier: --json envelope parses (stdout clean)" || bad "verifier: --json envelope broken"
        # Error paths: missing catalog → 3, malformed catalog → 4, drift catalog → 10.
        TMP="$(mktemp -d)"; trap 'rm -rf "$TMP"' EXIT
        ec 3 "verifier: missing catalog -> 3"  "$PY" "$verifier" --offline --catalog "$TMP/nope.json"
        printf '{"packages":"x"}' > "$TMP/bad.json"
        ec 4 "verifier: malformed catalog -> 4" "$PY" "$verifier" --offline --catalog "$TMP/bad.json"
        # Minimal drift catalog (argv path so MSYS translates it): a package whose
        # prose_token is not in the real skill prose -> drift -> exit 10.
        printf '%s\n' '{"schema":"claude-mods.mcp-ops.facts/v1","as_of":"2026-07-05","spec":{"url":"https://spec.example","prose_token":"zzz.no.such.spec"},"packages":{"@no-such/sdk":{"registry":"npm","prose_token":"@no-such/sdk","sampled_major":1,"track_major":true}}}' > "$TMP/drift.json"
        ec 10 "verifier: unnamed package -> 10" "$PY" "$verifier" --offline --catalog "$TMP/drift.json"
        # cited from SKILL.md
        grep -qF "scripts/check-mcp-facts.py" "$skill" && ok "verifier: cited from SKILL.md" || bad "verifier: uncited"
      else
        bad "check-mcp-facts.py missing"
      fi
      
      # 6. mcp-facts.json — parses, carries schema the verifier depends on
      "$PY" -c "
      import json, sys
      d = json.load(open(sys.argv[1], encoding='utf-8'))
      assert d['schema'] == 'claude-mods.mcp-ops.facts/v1'
      assert d['packages'], 'no packages committed'
      assert '@modelcontextprotocol/sdk' in d['packages']
      assert d['spec']['url'].startswith('https://')
      " "$catalog" && ok "mcp-facts.json schema + packages" || bad "mcp-facts.json invalid"
      
      # 7. Currency note present near the top of the body
      grep -qE 'verified as of [0-9]{4}' "$skill" && ok "currency note present" || bad "no dated currency note"
      
      echo "mcp-ops self-test: $pass passed, $fail failed" >&2
      [ "$fail" -eq 0 ]
      
  • SKILL.md 14.4 KB
    ---
    name: mcp-ops
    description: "Model Context Protocol server development, tool design, resource handling, and transport configuration. Use for: mcp, model context protocol, mcp server, mcp tool, mcp resource, fastmcp, mcp transport, stdio, sse, streamable http, mcp inspector, tool handler, mcp prompt."
    license: MIT
    allowed-tools: "Read Write Bash"
    metadata:
      author: claude-mods
      related-skills: claude-code-ops, typescript-ops, python-fastapi-ops
    ---
    
    # MCP Operations
    
    Comprehensive patterns for building, testing, and deploying Model Context Protocol servers in Python and TypeScript.
    
    > Ecosystem facts verified as of 2026-07-05 (standalone FastMCP at major 3).
    
    ## MCP Architecture Quick Reference
    
    ```
    ┌─────────────────────────────────────────────────────────┐
    │                     MCP Host                            │
    │  (Claude Desktop, Claude Code, Custom App)              │
    │                                                         │
    │  ┌───────────┐   ┌───────────┐   ┌───────────┐        │
    │  │  Client A  │   │  Client B  │   │  Client C  │       │
    │  └─────┬─────┘   └─────┬─────┘   └─────┬─────┘        │
    └────────┼───────────────┼───────────────┼────────────────┘
             │               │               │
        ┌────┴────┐     ┌────┴────┐     ┌────┴────┐
        │Transport│     │Transport│     │Transport│
        │ (stdio) │     │  (SSE)  │     │ (HTTP)  │
        └────┬────┘     └────┬────┘     └────┬────┘
             │               │               │
    ┌────────┴──┐     ┌──────┴────┐   ┌──────┴────┐
    │  Server A  │     │  Server B  │   │  Server C  │
    │            │     │            │   │            │
    │ ┌────────┐ │     │ ┌────────┐ │   │ ┌────────┐ │
    │ │ Tools  │ │     │ │Resources│ │   │ │Prompts │ │
    │ └────────┘ │     │ └────────┘ │   │ └────────┘ │
    │ ┌────────┐ │     │ ┌────────┐ │   │ ┌────────┐ │
    │ │Resources│ │     │ │Prompts │ │   │ │ Tools  │ │
    │ └────────┘ │     │ └────────┘ │   │ └────────┘ │
    └────────────┘     └────────────┘   └────────────┘
    
    Protocol: JSON-RPC 2.0 over chosen transport
    Flow:     Client → request → Server → response → Client
    ```
    
    ## Server Type Decision Tree
    
    ```
    What transport does your MCP server need?
    │
    ├─ Local CLI tool / single-user desktop integration?
    │  └─ stdio
    │     - Simplest setup, no networking
    │     - Claude Desktop, Claude Code native support
    │     - Process lifecycle managed by host
    │
    ├─ Web dashboard / browser-based client?
    │  └─ SSE (Server-Sent Events)
    │     - HTTP-based, works through firewalls
    │     - Persistent connection for server→client events
    │     - Good for development and internal tools
    │
    └─ Production API / multi-tenant / cloud deployment?
       └─ Streamable HTTP
          - HTTP POST for requests, SSE for streaming responses
          - Supports stateless and stateful modes
          - Full auth support, load balancer friendly
          - Recommended for production deployments
    ```
    
    ## Tool vs Resource vs Prompt Decision Tree
    
    ```
    What does the LLM need to do?
    │
    ├─ Perform an action or computation?
    │  └─ TOOL
    │     - Has side effects (API calls, file writes, DB mutations)
    │     - Accepts structured input, returns results
    │     - Examples: run_query, create_issue, send_email
    │
    ├─ Read data or context?
    │  └─ RESOURCE
    │     - Read-only data retrieval
    │     - Identified by URI (file://, db://, api://)
    │     - Examples: config://app, schema://users, file://readme.md
    │
    └─ Guide the LLM's behavior or workflow?
       └─ PROMPT
          - Templated instructions with arguments
          - Suggests conversation starters or workflows
          - Examples: code_review(language, file), summarize(topic)
    ```
    
    ## Python SDK Quick Start
    
    ```python
    from mcp.server.fastmcp import FastMCP
    
    mcp = FastMCP("my-server")
    
    @mcp.tool()
    def search_docs(query: str) -> str:
        """Search documentation by keyword."""
        results = perform_search(query)
        return "\n".join(f"- {r.title}: {r.snippet}" for r in results)
    
    @mcp.tool()
    def create_ticket(title: str, body: str, priority: str = "medium") -> str:
        """Create a support ticket."""
        ticket = api.create(title=title, body=body, priority=priority)
        return f"Created ticket #{ticket.id}: {ticket.url}"
    
    @mcp.resource("config://app")
    def get_config() -> str:
        """Return current application configuration."""
        return json.dumps(load_config(), indent=2)
    
    @mcp.resource("schema://db/{table}")
    def get_table_schema(table: str) -> str:
        """Return the schema for a database table."""
        return json.dumps(get_schema(table), indent=2)
    
    @mcp.prompt()
    def code_review(language: str, filepath: str) -> str:
        """Generate a code review prompt for the given file."""
        return f"Review this {language} code in {filepath} for bugs, style issues, and performance."
    
    if __name__ == "__main__":
        mcp.run()  # Defaults to stdio transport
    ```
    
    **Install and run:**
    
    ```bash
    uv init my-mcp-server && cd my-mcp-server
    uv add mcp[cli]
    # Run with: uv run python server.py
    # Or:       uv run mcp run server.py
    ```
    
    **Two Python FastMCPs — know which you're on.** The official `mcp` SDK bundles a frozen
    1.x-era FastMCP (`from mcp.server.fastmcp import FastMCP`, used in the samples above —
    stable, minimal). The standalone `fastmcp` package (gofastmcp.com) is where active
    development happens and is at **major 3**: same decorator surface, plus auth, proxying,
    OpenAPI generation, and a test client. To use it:
    
    ```bash
    uv add fastmcp
    ```
    
    ```python
    from fastmcp import FastMCP   # standalone FastMCP 3 — not mcp.server.fastmcp
    
    mcp = FastMCP("my-server")    # v3: constructor is identity/behaviour only;
                                  # transport config moved to run()/serve time
    ```
    
    FastMCP 3 breaking changes (from 2.x): 16 deprecated constructor kwargs removed
    (transport settings now passed at serve time), `ui=` replaced by `app=`,
    `ctx.set_state()`/`ctx.get_state()` are now async with session-scoped persistence, and
    the metadata namespace changed from `_fastmcp` to `fastmcp`.
    
    ## TypeScript SDK Quick Start
    
    ```typescript
    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-server",
      version: "1.0.0",
    });
    
    // Register a tool
    server.tool(
      "search_docs",
      "Search documentation by keyword",
      { query: z.string().describe("Search query") },
      async ({ query }) => {
        const results = await performSearch(query);
        return {
          content: [{ type: "text", text: results.join("\n") }],
        };
      }
    );
    
    // Register a resource
    server.resource(
      "config",
      "config://app",
      { description: "Current application configuration" },
      async (uri) => ({
        contents: [{
          uri: uri.href,
          mimeType: "application/json",
          text: JSON.stringify(loadConfig(), null, 2),
        }],
      })
    );
    
    // Register a prompt
    server.prompt(
      "code_review",
      "Generate a code review prompt",
      { language: z.string(), filepath: z.string() },
      async ({ language, filepath }) => ({
        messages: [{
          role: "user",
          content: {
            type: "text",
            text: `Review this ${language} code in ${filepath} for bugs and style issues.`,
          },
        }],
      })
    );
    
    async function main() {
      const transport = new StdioServerTransport();
      await server.connect(transport);
    }
    main().catch(console.error);
    ```
    
    **Install and run:**
    
    ```bash
    npm init -y
    npm install @modelcontextprotocol/sdk zod
    npx tsx server.ts
    ```
    
    ## Transport Selection Matrix
    
    | Feature | stdio | SSE | Streamable HTTP |
    |---------|-------|-----|-----------------|
    | **Use case** | Local CLI tools, desktop | Web dashboards, dev | Production APIs |
    | **Protocol** | stdin/stdout pipes | HTTP + EventSource | HTTP POST + SSE |
    | **Auth support** | Env vars only | Bearer tokens | Full OAuth2/PKCE |
    | **Deployment** | Local process | Single server | Load balanced |
    | **Reconnection** | Process restart | Auto-reconnect | Stateless resilient |
    | **Multi-client** | 1:1 only | Multiple clients | Horizontally scalable |
    | **Firewall** | N/A (local) | HTTP-friendly | HTTP-friendly |
    | **State** | Process lifetime | Connection lifetime | Session or stateless |
    | **Best for** | Claude Desktop/Code | Internal tools | Cloud/enterprise |
    
    ## Authentication Patterns Quick Reference
    
    ```python
    # Pattern 1: API keys from environment
    import os
    from mcp.server.fastmcp import FastMCP
    
    mcp = FastMCP("api-server")
    
    @mcp.tool()
    def call_api(endpoint: str) -> str:
        """Call external API with configured credentials."""
        api_key = os.environ["MY_API_KEY"]  # Set in client config
        resp = httpx.get(f"https://api.example.com/{endpoint}",
                         headers={"Authorization": f"Bearer {api_key}"})
        return resp.text
    ```
    
    ```python
    # Pattern 2: OAuth2 token refresh (in-memory cache)
    import time
    
    _token_cache: dict = {}
    
    async def get_valid_token() -> str:
        if _token_cache.get("expires_at", 0) > time.time() + 60:
            return _token_cache["access_token"]
        resp = await httpx.AsyncClient().post("https://auth.example.com/token", data={
            "grant_type": "refresh_token",
            "refresh_token": os.environ["REFRESH_TOKEN"],
            "client_id": os.environ["CLIENT_ID"],
        })
        data = resp.json()
        _token_cache.update({
            "access_token": data["access_token"],
            "expires_at": time.time() + data["expires_in"],
        })
        return data["access_token"]
    ```
    
    ```json
    // Claude Desktop config with env vars
    {
      "mcpServers": {
        "my-server": {
          "command": "uv",
          "args": ["run", "--directory", "/path/to/server", "python", "server.py"],
          "env": {
            "MY_API_KEY": "sk-...",
            "DATABASE_URL": "postgresql://..."
          }
        }
      }
    }
    ```
    
    ## Common Gotchas
    
    | Gotcha | Why | Fix |
    |--------|-----|-----|
    | Tool not appearing in client | `inputSchema` has invalid JSON Schema | Validate schema with jsonschema library; use Pydantic/Zod to generate |
    | Tool returns raw object | Results must be `content` list with typed items | Always return `{"content": [{"type": "text", "text": "..."}]}` |
    | Timeout on long operations | Default client timeout is often 30-60s | Add progress notifications; break into smaller operations |
    | Concurrent requests fail | Tool handler uses shared mutable state | Use asyncio locks, or make handlers stateless |
    | Large response crashes client | MCP messages have practical size limits | Paginate results; return summaries with detail-fetch tools |
    | Error swallowed silently | Exception in handler returns generic error | Set `isError: true` in response; include error message in content |
    | SSE connection drops | No keep-alive or reconnection logic | Implement heartbeat; client auto-reconnects on SSE |
    | Client ignores new tools | Capabilities not updated after tool change | Call `server.request_context.session.send_resource_list_changed()` |
    | Tool name collision | Two servers register same tool name | Namespace tools: `myserver_search` not just `search` |
    | Resource URI too generic | `data://info` is ambiguous | Use specific schemes: `db://myapp/users`, `config://myapp/settings` |
    | `async def` missing on handler | FastMCP tools can be sync or async, but I/O should be async | Use `async def` for any handler doing network/file I/O |
    | Server works locally, fails in Claude Desktop | Different working directory or PATH | Use absolute paths; log `os.getcwd()` on startup |
    
    ## Reference Files
    
    | File | Lines | Content |
    |------|-------|---------|
    | `references/server-architecture.md` | ~700 | Server lifecycle, FastMCP/TS SDK setup, capabilities, middleware, error handling |
    | `references/tool-handlers.md` | ~650 | Schema design, validation, return types, composition, side effects, examples |
    | `references/resources-prompts.md` | ~550 | Resource URIs, static/dynamic resources, templates, prompts, subscriptions |
    | `references/transport-auth.md` | ~550 | stdio/SSE/HTTP transports, session management, OAuth2, rate limiting, TLS |
    | `references/testing-debugging.md` | ~550 | MCP Inspector, unit/integration testing, protocol debugging, CI, performance |
    
    ## Staleness verifier
    
    This skill encodes fast-moving facts (the MCP SDK package names + spec URL). [`scripts/check-mcp-facts.py`](scripts/check-mcp-facts.py) guards them against silent drift:
    
    ```bash
    # Structural (PR CI, no network): every catalogued package's prose_token is
    # still named in this skill's prose, the spec URL is still cited, and the
    # currency note still carries a year.
    python scripts/check-mcp-facts.py --offline        # exit 0 consistent, 10 drift
    
    # Live (freshness job, never blocks a PR): each SDK still resolves on
    # npm/PyPI, no tracked major has moved off the sampled major, spec URL 200.
    python scripts/check-mcp-facts.py --live            # exit 10 drift, 7 registries unreachable
    ```
    
    The canonical fact set lives in [`assets/mcp-facts.json`](assets/mcp-facts.json); when you add or drop a package, update it to match or `--offline` fails CI.
    
    ## See Also
    
    - **MCP Specification**: https://modelcontextprotocol.io/specification/latest (the old spec.modelcontextprotocol.io subdomain no longer resolves)
    - **Python SDK**: https://github.com/modelcontextprotocol/python-sdk
    - **TypeScript SDK**: https://github.com/modelcontextprotocol/typescript-sdk
    - **Official MCP Servers**: https://github.com/modelcontextprotocol/servers
    - **MCP Inspector**: `npx @modelcontextprotocol/inspector`
    - **FastMCP Documentation**: https://gofastmcp.com
    - **Related skills**: `claude-code-hooks` (hook into Claude Code), `claude-code-debug` (debug Claude Code issues)
    

Comments (0)

Sign in to join the conversation.

No comments yet.

Reviews (0)

No reviews yet.

Related