← Tech Guides

Model Context Protocol

The universal standard for connecting AI to external tools, data, and capabilities

01

Quick Reference

MCP concepts at a glance — the protocol primitives, transports, and core JSON-RPC methods that form the foundation of every integration.

Protocol Primitives

MCP defines three primitives, each with a different control model that determines who initiates the interaction.

Primitive Control Description Example
Tools Model-controlled Functions the AI model can invoke to take actions or retrieve data get_weather(), query_db()
Resources App-controlled Structured data the host application can expose to the model as context file://project/src, config://app
Prompts User-controlled Templated messages and workflows the user selects from a menu explain_code, review_pr
Key insight

The control model matters for safety. Tools are model-initiated (the AI decides to call them), resources are application-initiated (the host decides what context to attach), and prompts are user-initiated (the human picks a workflow). This layered control keeps humans in the loop.

Transport Comparison

Transport Use Case Communication Auth
stdio Local processes, CLI tools, IDE extensions stdin/stdout JSON-RPC messages OS process isolation
Streamable HTTP Remote servers, cloud services, multi-client HTTP POST + optional SSE streaming OAuth 2.1 / API keys

Core JSON-RPC Methods

Method Direction Description
initialize Client → Server Handshake: exchange protocol version and capabilities
tools/list Client → Server Discover available tools with names, descriptions, and input schemas
tools/call Client → Server Invoke a tool with arguments, receive result or error
resources/list Client → Server Discover available resources with URIs and MIME types
resources/read Client → Server Fetch content of a specific resource by URI
prompts/list Client → Server Discover available prompt templates
prompts/get Client → Server Resolve a prompt template with arguments into messages

Capability Negotiation

During initialization, client and server declare what features they support. Only negotiated capabilities are used during the session.

Client Capabilities

Features the client can provide to the server

roots · sampling · elicitation

Server Capabilities

Features the server offers to the client

tools · resources · prompts · logging · completions
02

Architecture Overview

Understanding MCP's three participants, two protocol layers, and the lifecycle that governs every connection.

The Three Participants

MCP cleanly separates concerns across three roles. A single application can contain all three, but the boundaries matter for security and modularity.

  +--------------------------------------------------+
  |                     HOST                         |
  |  (Claude Desktop, IDE, AI application)           |
  |                                                  |
  |   +------------+   +------------+   +----------+ |
  |   |  MCP       |   |  MCP       |   |  MCP     | |
  |   |  Client A  |   |  Client B  |   |  Client  | |
  |   +-----+------+   +-----+------+   +----+-----+ |
  +---------|---------------|---------------- |-------+
            |               |                 |
     +------+------+  +-----+-------+  +------+------+
     |  MCP        |  |  MCP        |  |  MCP        |
     |  Server A   |  |  Server B   |  |  Server C   |
     |  (files)    |  |  (database) |  |  (API)      |
     +-------------+  +-------------+  +-------------+

Host

The AI application that creates and manages MCP clients. Controls permissions and security policies.

Claude Desktop · Claude Code · IDEs

Client

Maintains a 1:1 stateful session with a single server. Handles protocol negotiation and message routing.

1 client = 1 server

Server

Exposes tools, resources, and prompts to clients. Can be local processes or remote services.

tools · resources · prompts

Two Protocol Layers

Layer Concern Details
Data Layer Message format JSON-RPC 2.0 messages — requests (with id), responses, and notifications (no id)
Transport Layer Delivery mechanism stdio (local) or Streamable HTTP (remote). Pluggable — custom transports possible.

Lifecycle Phases

Every MCP connection goes through three phases. The handshake ensures both sides agree on protocol version and capabilities before any real work begins.

Phase What Happens Key Messages
1. Initialize Client sends protocol version and capabilities, server responds with its own initialize → response → initialized notification
2. Operation Normal request/response flow. Tool calls, resource reads, prompt gets tools/call, resources/read, prompts/get, notifications
3. Shutdown Clean disconnection. Transport-specific close procedures Close stdin/stdout or HTTP connection

Initialize Handshake

The initialization exchange sets the protocol version and declares capabilities. The latest protocol version is 2025-03-26.

Client Request
{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "initialize",
  "params": {
    "protocolVersion": "2025-03-26",
    "capabilities": {
      "roots": { "listChanged": true },
      "sampling": {}
    },
    "clientInfo": {
      "name": "my-ai-app",
      "version": "1.0.0"
    }
  }
}
Server Response
{
  "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": "weather-server",
      "version": "1.0.0"
    }
  }
}
Version negotiation

The server must respond with a protocol version it supports. If the server cannot support the client's requested version, it should respond with the closest version it does support, and the client decides whether to proceed or disconnect.

03

Getting Started

Install the SDK, build your first MCP server, and connect it to a host application in minutes.

Install the SDK

Python
pip install "mcp[cli]"
TypeScript / Node.js
npm install @modelcontextprotocol/sdk
Python extras

The [cli] extra installs the MCP Inspector and CLI dev tools. For production servers you can use pip install mcp for a minimal install.

First Server — Python (FastMCP)

FastMCP is the high-level Python framework that handles all the protocol details. You just write decorated functions.

server.py
from mcp.server.fastmcp import FastMCP

# Create an MCP server with a name
mcp = FastMCP("weather")

@mcp.tool
def get_weather(location: str) -> str:
    """Get current weather for a location."""
    # Your real implementation here
    return f"Weather in {location}: 72F, sunny"

@mcp.resource("config://app")
def get_config() -> str:
    """Application configuration."""
    return "debug=true\nlog_level=info"

@mcp.prompt()
def review_code(code: str) -> str:
    """Review the given code for issues."""
    return f"Please review this code:\n\n{code}"
Decorator syntax

In FastMCP 2+, @mcp.tool works without parentheses — they're optional. Use @mcp.tool(name="custom_name") only when you need to override the function name or pass other options.

Run the server locally with stdio transport:

mcp dev server.py

First Server — TypeScript

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

const server = new McpServer({
  name: "weather",
  version: "1.0.0"
});

server.tool(
  "get_weather",
  "Get current weather for a location",
  { location: z.string() },
  async ({ location }) => ({
    content: [{
      type: "text",
      text: `Weather in ${location}: 72F, sunny`
    }]
  })
);

const transport = new StdioServerTransport();
await server.connect(transport);
TypeScript imports use .js extensions

Even though your source files are .ts, MCP SDK imports must use .js extensions (e.g., "/server/mcp.js"). This is a TypeScript ESM requirement — the compiler resolves .js imports to their .ts sources automatically.

Configure Claude Desktop

Add your server to Claude Desktop's configuration file to make it available in the app.

claude_desktop_config.json
{
  "mcpServers": {
    "weather": {
      "command": "python",
      "args": ["server.py"],
      "env": {
        "WEATHER_API_KEY": "your-key-here"
      }
    }
  }
}
Config file location

On macOS: ~/Library/Application Support/Claude/claude_desktop_config.json
On Windows: %APPDATA%\Claude\claude_desktop_config.json

Configure Claude Code

Claude Code uses a project-level .mcp.json file or global settings.

.mcp.json (project root)
{
  "mcpServers": {
    "weather": {
      "command": "python",
      "args": ["server.py"],
      "env": {
        "API_KEY": "${WEATHER_KEY:-sk-default}"
      }
    }
  }
}
Variable expansion

The .mcp.json format supports shell-style variable expansion: ${VAR} uses the environment variable, and ${VAR:-default} provides a fallback value if unset.

Or add via the CLI:

# Basic: add a stdio server
claude mcp add weather python server.py

# With environment variables
claude mcp add weather --env WEATHER_KEY=sk-123 python server.py

# Scoped: project-only (default) or user-wide
claude mcp add weather --scope user python server.py

# HTTP transport (Streamable HTTP or SSE)
claude mcp add weather --transport http https://example.com/mcp

Configure VS Code

VS Code supports MCP servers through a workspace-level config file.

.vscode/mcp.json
{
  "servers": {
    "weather": {
      "command": "python",
      "args": ["server.py"],
      "env": {
        "API_KEY": "${WEATHER_KEY}"
      }
    }
  }
}
VS Code vs Claude Code format

Note the different root key: VS Code uses "servers" while Claude Code uses "mcpServers". Both support the same command/args/env structure.

Test with MCP Inspector

The MCP Inspector is a visual debugging tool that lets you interact with your server directly.

# Launch inspector for your server
mcp dev server.py

# Or use npx for the TypeScript inspector
npx @modelcontextprotocol/inspector node server.js

The Inspector opens a web UI where you can browse tools, resources, and prompts, invoke them with test inputs, and inspect the raw JSON-RPC messages.

04

Tools

Model-controlled functions that let the AI take actions and retrieve computed results.

What Are Tools?

Tools are executable functions exposed by MCP servers that the AI model can decide to invoke based on conversation context. Unlike resources (which are application-controlled) or prompts (which are user-controlled), tools are model-controlled — the LLM autonomously decides when and how to call them.

API Calls

Fetch data from external services — weather, search, databases

get_weather() · search_web()

File Operations

Read, write, and manipulate files on the local filesystem

read_file() · write_file()

Database Queries

Execute SQL or NoSQL queries against connected data stores

query_db() · insert_record()

Computations

Perform calculations, transformations, or code execution

calculate() · run_script()
Key distinction

Tools represent actions with side effects or computed results. The model sees the tool's name, description, and input schema, then decides whether to call it. The host application can enforce approval policies before execution.

Tool Definition Schema

Every tool is defined with a JSON Schema that describes its name, purpose, inputs, outputs, and behavioral annotations. Servers expose these definitions via tools/list.

Tool Definition
{
  "name": "get_weather",
  "title": "Weather Provider",
  "description": "Get current weather for a location",
  "inputSchema": {
    "type": "object",
    "properties": {
      "location": {
        "type": "string",
        "description": "City name or zip code"
      }
    },
    "required": ["location"]
  },
  "outputSchema": {
    "type": "object",
    "properties": {
      "temperature": { "type": "number" },
      "conditions": { "type": "string" }
    }
  },
  "annotations": {
    "readOnlyHint": true,
    "openWorldHint": true
  }
}
Field Required Description
name Yes 1-128 chars, case-sensitive, [A-Za-z0-9_\-.] only
title No Human-readable display name for UI presentation
description No Human-readable explanation of what the tool does (helps LLM decide when to use it)
inputSchema Yes Must be a valid JSON Schema object type defining the tool's parameters
outputSchema No JSON Schema describing the structured content in the tool's response
annotations No Behavioral hints about the tool's side effects and interaction model
No-parameter tools

For tools that take no arguments, use an empty object schema: { "type": "object", "additionalProperties": false }

Calling Tools

The client invokes a tool via tools/call, passing the tool name and arguments. The server executes the tool and returns structured content.

Request
{
  "jsonrpc": "2.0",
  "id": 2,
  "method": "tools/call",
  "params": {
    "name": "get_weather",
    "arguments": {
      "location": "New York"
    }
  }
}
Success Response
{
  "jsonrpc": "2.0",
  "id": 2,
  "result": {
    "content": [{
      "type": "text",
      "text": "72°F, sunny"
    }],
    "isError": false
  }
}
Tool Error Response
{
  "jsonrpc": "2.0",
  "id": 2,
  "result": {
    "content": [{
      "type": "text",
      "text": "Invalid location: coordinates out of range"
    }],
    "isError": true
  }
}

Tool Annotations

Annotations are optional behavioral hints that describe a tool's side effects and interaction model. Clients can use them to make UI and safety decisions.

Annotation Default Description
readOnlyHint false Tool does not modify the environment — safe to call without confirmation
destructiveHint true Tool may perform destructive updates (delete, overwrite) — prompt user for approval
idempotentHint false Repeated calls with same arguments have no additional effect — safe to retry
openWorldHint true Tool interacts with external systems beyond the MCP server's own environment
Trust boundary

Annotations are untrusted hints unless they come from a verified trusted server. A malicious server could claim readOnlyHint: true for a destructive tool. Hosts should enforce their own safety policies and not rely solely on annotations.

Error Handling

MCP distinguishes between two error modes. Understanding the difference is critical for building robust integrations.

Protocol Errors

JSON-RPC error field in the response. Indicates the request itself was invalid — unknown tool name, malformed arguments, transport failure.

-32601 · -32602 · -32603

Tool Execution Errors

Normal result with isError: true. The tool ran but encountered a problem — API timeout, validation failure, resource not found. LLMs can read the error and self-correct.

isError: true
Why this matters

Protocol errors are not shown to the LLM — they indicate a bug in the integration. Tool execution errors are returned to the model as content, allowing it to try a different approach, adjust parameters, or explain the problem to the user.

Content Types

Tool results can include multiple content items of different types in the content array.

Type Format Use Case
text Plain text string Most common — API responses, computed results, status messages
image Base64-encoded data + MIME type Screenshots, charts, generated images
audio Base64-encoded data + MIME type Audio recordings, synthesized speech
resource_link URI reference to an MCP resource Link to a resource for the client to fetch separately
resource Embedded resource content inline Full resource data included directly in the tool result
Mixed Content Example
{
  "content": [
    { "type": "text", "text": "Analysis complete. See chart below." },
    {
      "type": "image",
      "data": "iVBORw0KGgo...",
      "mimeType": "image/png"
    },
    {
      "type": "resource_link",
      "uri": "file:///reports/analysis.csv",
      "name": "Full Data",
      "mimeType": "text/csv"
    }
  ]
}
05

Resources

Application-controlled data sources that provide structured context to the model via URI-addressable content.

What Are Resources?

Resources are URI-addressable data sources that provide context to the LLM. Unlike tools, resources are application-controlled — the host application decides which resources to attach to the model's context, not the model itself.

File Contents

Source code, configuration files, documentation

file:///project/src/main.rs

Database Records

Query results, schema definitions, row data

db://tables/users

API Responses

Cached or live data from external services

api://github/repos

Live System Data

Logs, metrics, environment info, app state

config://app/settings
Resources vs Tools

Resources provide data for context. Tools perform actions with side effects. If you need to read a file, that is a resource. If you need to write a file, that is a tool. The host app decides which resources to include — the model never fetches resources on its own.

Listing Resources

Clients discover available resources via resources/list. The server returns a paginated list of concrete resources with their URIs, descriptions, and metadata.

resources/list Response
{
  "jsonrpc": "2.0",
  "id": 3,
  "result": {
    "resources": [
      {
        "uri": "file:///project/src/main.rs",
        "name": "main.rs",
        "description": "Primary application entry point",
        "mimeType": "text/x-rust",
        "size": 1234
      },
      {
        "uri": "config://app/settings",
        "name": "App Settings",
        "description": "Current application configuration",
        "mimeType": "application/json"
      }
    ]
  }
}
Field Required Description
uri Yes Unique identifier for the resource (any valid URI scheme)
name Yes Human-readable name for display in UI
description No Explanation of what the resource contains
mimeType No MIME type of the resource content (e.g. text/plain, application/json)
size No Size in bytes, if known. Helps clients decide whether to fetch

Reading Resources

Clients fetch resource content via resources/read. Resources can return text or binary (base64-encoded) content.

Text Resource
// Request
{
  "method": "resources/read",
  "params": { "uri": "file:///project/src/main.rs" }
}

// Response
{
  "result": {
    "contents": [{
      "uri": "file:///project/src/main.rs",
      "mimeType": "text/x-rust",
      "text": "fn main() {\n    println!(\"Hello, world!\");\n}"
    }]
  }
}
Binary Resource
{
  "result": {
    "contents": [{
      "uri": "file:///project/logo.png",
      "mimeType": "image/png",
      "blob": "iVBORw0KGgoAAAANSUhEUgAA..."
    }]
  }
}
Text vs Binary

Text resources use the text field. Binary resources use the blob field with base64-encoded data. A single resources/read response can return multiple content items for the same URI.

Resource Templates

Resource templates use URI Templates (RFC 6570) to expose dynamic, parameterized resources. Instead of listing every possible file, a server can expose a template that the client fills in.

Template Definition
{
  "uriTemplate": "file:///{path}",
  "name": "Project Files",
  "description": "Access any file in the project by path",
  "mimeType": "application/octet-stream"
}
More Template Examples
// Database table rows
{ "uriTemplate": "db://tables/{table}/rows/{id}" }

// Git repository at a specific ref
{ "uriTemplate": "git://{repo}/blob/{ref}/{path}" }

// API endpoint with query parameters
{ "uriTemplate": "api://search?q={query}&limit={limit}" }
Discovery

Templates are returned alongside concrete resources in resources/list via a separate resourceTemplates field. Clients can also use completions/complete to get autocomplete suggestions for template parameters.

Subscriptions

Clients can subscribe to resource changes and receive real-time notifications when content updates.

Subscribe to a Resource
// Client subscribes
{
  "method": "resources/subscribe",
  "params": { "uri": "file:///project/src/main.rs" }
}

// Server notifies on change
{
  "method": "notifications/resources/updated",
  "params": { "uri": "file:///project/src/main.rs" }
}

// Client re-reads the resource
{
  "method": "resources/read",
  "params": { "uri": "file:///project/src/main.rs" }
}
Capability required

Subscriptions are only available if the server declared "resources": { "subscribe": true } during capability negotiation. Servers also send notifications/resources/list_changed when the set of available resources changes.

Resource Annotations

Resources can include annotations that help clients decide how to present them to the model.

Annotation Type Description
audience Array of "user" | "assistant" Who the resource is intended for. Both if omitted.
priority Number (0.0 – 1.0) Relative importance. 1.0 = must include, 0.0 = low priority context.
lastModified ISO 8601 timestamp When the resource was last changed. Helps with cache invalidation.
Annotated Resource Example
{
  "uri": "file:///project/README.md",
  "name": "Project README",
  "mimeType": "text/markdown",
  "annotations": {
    "audience": ["assistant"],
    "priority": 0.8,
    "lastModified": "2025-03-15T10:30:00Z"
  }
}
06

Prompts

User-controlled templates that define reusable workflows and interaction patterns.

What Are Prompts?

Prompts are reusable message templates that users explicitly choose to invoke. They are user-controlled — the human selects a prompt from a menu, slash command, or UI action. The server defines the template; the user triggers it.

Slash Commands

User types /review or /explain to trigger a workflow

/review-code · /summarize

Menu Options

UI presents a list of available prompts for the user to pick from

Quick Actions · Workflows

Structured Workflows

Multi-step prompts that guide the model through a specific process

debug_error · git_commit
Prompts vs Tools vs Resources

Prompts generate messages for the conversation. They do not execute code or fetch data directly. A prompt can embed resource content and suggest tool usage, but the prompt itself just produces structured messages that get inserted into the conversation.

Listing Prompts

Clients discover available prompts via prompts/list. Each prompt declares its name, description, and any required arguments.

prompts/list Response
{
  "jsonrpc": "2.0",
  "id": 4,
  "result": {
    "prompts": [
      {
        "name": "review_code",
        "description": "Review code for bugs, style issues, and improvements",
        "arguments": [
          {
            "name": "code",
            "description": "The code to review",
            "required": true
          },
          {
            "name": "language",
            "description": "Programming language",
            "required": false
          }
        ]
      },
      {
        "name": "explain_error",
        "description": "Explain an error message and suggest fixes",
        "arguments": [
          {
            "name": "error",
            "description": "The error message or stack trace",
            "required": true
          }
        ]
      }
    ]
  }
}

Getting Prompts

The client resolves a prompt template into concrete messages via prompts/get, passing any required arguments. The server interpolates the arguments and returns ready-to-use messages.

Request
{
  "jsonrpc": "2.0",
  "id": 5,
  "method": "prompts/get",
  "params": {
    "name": "review_code",
    "arguments": {
      "code": "def hello():\n    print('world')"
    }
  }
}
Response
{
  "jsonrpc": "2.0",
  "id": 5,
  "result": {
    "description": "Code review prompt",
    "messages": [{
      "role": "user",
      "content": {
        "type": "text",
        "text": "Please review this code:\ndef hello():\n    print('world')"
      }
    }]
  }
}

Multi-Step Prompts

Prompts can return multiple messages with different roles, creating a structured conversation flow that guides the model's behavior.

Multi-Message Prompt Response
{
  "result": {
    "description": "Guided debugging workflow",
    "messages": [
      {
        "role": "user",
        "content": {
          "type": "text",
          "text": "I'm getting this error: TypeError: Cannot read properties of undefined"
        }
      },
      {
        "role": "assistant",
        "content": {
          "type": "text",
          "text": "I'll help debug this. Let me analyze the error systematically:\n\n1. First, I'll identify what variable is undefined\n2. Then trace where it should have been set\n3. Finally, suggest a fix"
        }
      },
      {
        "role": "user",
        "content": {
          "type": "text",
          "text": "Here is the relevant source code. Please proceed with the analysis."
        }
      }
    ]
  }
}
Why multi-step?

By including assistant role messages, prompts can prime the model with a specific reasoning approach or persona. This is more reliable than asking the model to "act as" something — you are directly seeding its conversation history.

Embedded Resources

Prompt messages can embed resource content directly, combining the prompt template with live data from the server.

Prompt with Embedded Resource
{
  "result": {
    "messages": [{
      "role": "user",
      "content": {
        "type": "resource",
        "resource": {
          "uri": "file:///project/src/main.rs",
          "mimeType": "text/x-rust",
          "text": "fn main() {\n    println!(\"Hello, world!\");\n}"
        }
      }
    },
    {
      "role": "user",
      "content": {
        "type": "text",
        "text": "Please review the above file for potential improvements."
      }
    }]
  }
}
Dynamic context

Embedded resources are resolved at prompt-get time, not at definition time. This means the prompt always includes the latest version of the resource content. The server fetches the resource and inlines it into the message before returning.

07

Transports

The communication layer -- stdio for local processes and Streamable HTTP for remote services, with OAuth 2.1 authorization.

Transport Overview

MCP uses JSON-RPC 2.0 messages encoded as UTF-8 over two standard transports. The protocol layer is identical regardless of transport — only the delivery mechanism changes.

stdio

Client launches server as a child process. Messages flow over stdin/stdout as newline-delimited JSON.

Local · Zero config · 1:1 only

Streamable HTTP

Server exposes an HTTP endpoint. Client sends JSON-RPC via POST, receives JSON or SSE responses.

Remote · Multi-client · OAuth ready
Protocol version note

Streamable HTTP replaces the deprecated HTTP+SSE transport from the 2024-11-05 protocol version. New implementations should use Streamable HTTP exclusively.

stdio Transport

The simplest transport: the client launches the server as a subprocess and communicates via standard I/O streams. Messages are newline-delimited JSON-RPC.

  Client launches subprocess
    → Client writes JSON-RPC to stdin
    ← Server writes JSON-RPC to stdout
    ← Server logs to stderr (optional)

  Shutdown:
    Client closes stdin → SIGTERM → SIGKILL
Rule Description
stdout is sacred Server MUST NOT write non-MCP messages to stdout. Use stderr for logging and debug output.
stdin is sacred Client MUST NOT write non-MCP messages to stdin. The stream is exclusively for JSON-RPC.
Newline-delimited Each JSON-RPC message is terminated by a newline. Messages MUST NOT contain embedded newlines.
Process lifecycle Client owns the process. Closing stdin signals shutdown; server should exit cleanly.

Best for: Local tools, IDE integrations, same-machine processes, zero network overhead. Most development-time MCP servers use stdio.

Streamable HTTP Transport

For remote and multi-client scenarios, the server exposes a single HTTP endpoint (e.g., https://example.com/mcp). This transport supports both request-response and streaming patterns.

Operation Method Details
Send message POST /mcp Body is a single JSON-RPC message. Client MUST include Accept: application/json, text/event-stream
JSON response POST → 200 Server responds with Content-Type: application/json for simple request/response
SSE response POST → 200 Server responds with Content-Type: text/event-stream for streaming results
Notification POST → 202 For notifications and responses that need no reply, server returns 202 Accepted
Listen for server messages GET /mcp Client opens SSE stream to receive server-initiated notifications and requests
Example POST Request
POST /mcp HTTP/1.1
Host: example.com
Content-Type: application/json
Accept: application/json, text/event-stream
MCP-Session-Id: abc123
MCP-Protocol-Version: 2025-03-26

{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/call",
  "params": {
    "name": "get_weather",
    "arguments": { "location": "NYC" }
  }
}
TypeScript — Express + StreamableHTTPServerTransport
import express from "express";
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StreamableHTTPServerTransport } from
  "@modelcontextprotocol/sdk/server/streamableHttp.js";

const app = express();
app.use(express.json());

app.all("/mcp", async (req, res) => {
  const server = new McpServer({ name: "remote", version: "1.0.0" });
  // Register tools on `server` here...
  const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined });
  await server.connect(transport);
  await transport.handleRequest(req, res, req.body);
});

app.listen(3000);

Best for: Remote servers, multi-tenant platforms, cloud services, cross-network integrations where multiple clients connect to the same server.

Session Management

Streamable HTTP connections use session IDs to maintain state across requests. Sessions are optional but recommended for any stateful interaction.

Header Direction Description
MCP-Session-Id Server → Client Server MAY assign a session ID in the InitializeResult response
MCP-Session-Id Client → Server Client MUST include the session ID on all subsequent requests once received
MCP-Protocol-Version Client → Server Client MUST include 2025-03-26 (or negotiated version) on every request

Session Termination

Client sends DELETE to the MCP endpoint with the session ID header to end the session.

DELETE /mcp

Expired Sessions

If the session has expired or is unknown, the server returns 404 Not Found. Client must re-initialize.

HTTP 404

Security Considerations

MCP transports require careful security configuration, especially when servers accept network connections.

Concern Mitigation
DNS rebinding Servers MUST validate the Origin header and return 403 Forbidden if it does not match an allowed origin
Local binding Local servers MUST bind to 127.0.0.1, never 0.0.0.0 — prevents external network access
Authentication Remote servers should implement OAuth 2.1 or use HTTP headers for API key authentication
TLS Remote Streamable HTTP endpoints should use HTTPS to encrypt traffic in transit
DNS Rebinding Warning

Without Origin header validation, a malicious website can trick a browser into sending requests to a local MCP server. Always validate the Origin header on every request and reject unrecognized origins with 403. This is critical even for servers that only listen on localhost.

Transport Comparison

Feature stdio Streamable HTTP
Connection Subprocess HTTP endpoint
Message format Newline-delimited JSON HTTP POST / SSE
Session state Implicit (process lifetime) MCP-Session-Id header
Multi-client No (1:1) Yes
Network Local only Local or remote
Auth Environment variables OAuth / HTTP headers
Best for Dev tools, IDE plugins Cloud services, APIs
08

Building Servers

Patterns and best practices for building production MCP servers with Python FastMCP and the TypeScript SDK.

Python with FastMCP

FastMCP is the high-level Python framework for building MCP servers. Decorators map directly to MCP primitives — @mcp.tool for tools, @mcp.resource() for resources, and @mcp.prompt() for prompts.

server.py
from mcp.server.fastmcp import FastMCP

mcp = FastMCP("my-server")

# Tools — model-controlled actions
@mcp.tool
def calculate(expression: str) -> str:
    """Evaluate a math expression."""
    return str(eval(expression))

# Resources — structured data with URI templates
@mcp.resource("data://users/{user_id}")
def get_user(user_id: str) -> str:
    """Fetch user profile data."""
    return json.dumps({"id": user_id, "name": "Alice"})

# Prompts — templated workflows
@mcp.prompt()
def review_code(code: str) -> str:
    """Generate a code review prompt."""
    return f"Please review this code for bugs and improvements:\n\n{code}"
Decorator MCP Primitive Control Notes
@mcp.tool Tool Model-controlled Type hints become the JSON Schema. Parens optional since FastMCP 2+
@mcp.resource() Resource App-controlled URI templates with {param} placeholders for dynamic resources
@mcp.prompt() Prompt User-controlled Return string or list of Message objects
Context object

FastMCP injects a Context object when you add it as a parameter. Use it for logging (ctx.info(), ctx.error()), reporting progress (ctx.report_progress()), reading resources (ctx.read_resource()), and accessing request metadata.

TypeScript with MCP SDK

The official TypeScript SDK provides McpServer for building servers and uses Zod schemas for input validation.

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

const server = new McpServer({
  name: "my-server",
  version: "1.0.0"
});

// Register a tool with Zod schema validation
server.tool(
  "calculate",
  { expression: z.string() },
  async ({ expression }) => ({
    content: [{ type: "text", text: String(eval(expression)) }]
  })
);

// Connect via stdio transport
const transport = new StdioServerTransport();
await server.connect(transport);

Zod Schemas

Tool input schemas are defined with Zod. The SDK automatically converts them to JSON Schema for the protocol.

z.string() · z.number() · z.object()

Content Types

Tool results return an array of content blocks. Supports text, images, and embedded resources.

type: "text" · type: "image" · type: "resource"

Server Lifecycle

Production servers need proper startup and shutdown handling. Both SDKs support lifecycle hooks for resource management.

Python — Lifespan management with typed state
from contextlib import asynccontextmanager
from dataclasses import dataclass

@dataclass
class AppState:
    db: Database
    cache: Redis

@asynccontextmanager
async def lifespan(server):
    # Startup: initialize connections, load config
    db = await Database.connect("postgres://...")
    cache = await Redis.connect("redis://...")
    try:
        yield AppState(db=db, cache=cache)
    finally:
        # Shutdown: clean up resources
        await cache.close()
        await db.close()

mcp = FastMCP("my-server", lifespan=lifespan)

@mcp.tool
async def query(ctx: Context, sql: str) -> str:
    state: AppState = ctx.request_context.lifespan_context
    return str(await state.db.execute(sql))
Typed vs dict lifespan

You can yield a plain dict like {"db": db} for simple cases. A @dataclass gives you autocomplete and type checking — recommended when managing multiple resources.

Lifecycle Event What to Do
Startup Open database connections, load configuration, initialize caches, validate credentials
Shutdown Close connections, flush buffers, release file handles, save state
Error recovery Use try/finally to ensure cleanup runs even when tools throw exceptions

Error Handling Patterns

MCP distinguishes between tool execution errors (reported in the result) and protocol errors (thrown as exceptions). Getting this right helps LLMs self-correct.

Error Type How to Return When to Use
Tool error Return result with isError: true Invalid arguments, API failures, expected errors the LLM should see and retry
Protocol error Throw McpError exception Unknown tool, invalid JSON-RPC, internal server errors
Python — Tool error (visible to LLM)
@mcp.tool
def get_file(path: str) -> str:
    try:
        return Path(path).read_text()
    except FileNotFoundError:
        # Return error as tool result — LLM sees this and can try another path
        raise ToolError(f"File not found: {path}. Check the path and try again.")
LLM-friendly error messages

Write error messages that help the model self-correct. Instead of "Error 404", say "File not found: config.yaml. Available files are: app.yaml, db.yaml". Give the LLM enough context to make a better second attempt.

Testing with MCP Inspector

The MCP Inspector is an interactive debugging tool for testing servers during development. It connects to your server, discovers capabilities, and lets you call tools and read resources manually.

Launch the Inspector
# Test a Python server
npx @modelcontextprotocol/inspector python server.py

# Test a Node.js server
npx @modelcontextprotocol/inspector node server.js

# Test with environment variables
npx @modelcontextprotocol/inspector -e API_KEY=sk-xxx python server.py

Discover Tools

View all registered tools with their names, descriptions, and input schemas in a browsable UI.

tools/list

Test Tool Calls

Fill in arguments and invoke tools interactively. See raw JSON-RPC request and response.

tools/call

Browse Resources

List available resources, read their content, and test URI template resolution.

resources/read

Debug Transport

Inspect raw JSON-RPC messages flowing between client and server. Diagnose protocol issues.

JSON-RPC log
09

Building Clients

How to connect to MCP servers from your own AI applications -- session management, tool integration, and sampling.

Client Architecture

In MCP, the host application creates one Client instance per server connection. Each client manages its own lifecycle independently — initialization, capability discovery, tool calls, and shutdown are all scoped to a single server session.

  +-------------------------------------------+
  |              HOST APPLICATION              |
  |                                            |
  |   +----------+  +----------+  +----------+ |
  |   | Client 1 |  | Client 2 |  | Client 3 | |
  |   +----+-----+  +----+-----+  +----+-----+ |
  +--------|-------------|-------------|--------+
           |             |             |
     +-----+-----+ +----+------+ +----+------+
     | Server A   | | Server B  | | Server C  |
     | (files)    | | (git)     | | (database)|
     +------------+ +-----------+ +-----------+
Responsibility Who Handles It
Creating clients The host application — one client per server in the configuration
Managing connections Each client independently — handles its own initialize/shutdown
Aggregating tools The host — merges tool lists from all clients for the LLM
Routing tool calls The host — maps tool names back to the correct client/server
Security policies The host — enforces permissions, user consent, rate limits

Python Client

The Python SDK provides ClientSession with async context managers for clean connection lifecycle management.

client.py
from mcp import ClientSession, StdioServerParameters
from mcp.client.stdio import stdio_client

# Define server connection parameters
params = StdioServerParameters(
    command="python",
    args=["server.py"]
)

# Connect and interact
async with stdio_client(params) as (read, write):
    async with ClientSession(read, write) as session:
        # Initialize the connection
        await session.initialize()

        # Discover available tools
        tools = await session.list_tools()
        for tool in tools.tools:
            print(f"{tool.name}: {tool.description}")

        # Call a tool
        result = await session.call_tool(
            "get_weather",
            {"location": "NYC"}
        )
        print(result.content[0].text)

Session Methods

Core methods available on ClientSession

initialize() · list_tools() · call_tool() · list_resources() · read_resource() · list_prompts() · get_prompt()

Transport Options

Choose the transport that matches your server

stdio_client · streamablehttp_client

TypeScript Client

The TypeScript SDK's Client class manages server connections with explicit transport configuration.

client.ts
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";

// Create transport and client
const transport = new StdioClientTransport({
  command: "python",
  args: ["server.py"]
});

const client = new Client({
  name: "my-app",
  version: "1.0.0"
});

// Connect and initialize
await client.connect(transport);

// Discover and call tools
const tools = await client.listTools();
const result = await client.callTool("get_weather", {
  location: "NYC"
});

console.log(result.content[0].text);

// Clean up
await client.close();

Capability Discovery

After initialization, clients should check what capabilities the server declared before attempting to use features. Servers also send notifications when their capabilities change.

Capability Check Before Change Notification
tools Calling tools/list or tools/call notifications/tools/list_changed
resources Calling resources/list or resources/read notifications/resources/list_changed
prompts Calling prompts/list or prompts/get notifications/prompts/list_changed
logging Calling logging/setLevel N/A
Checking capabilities (Python)
# After session.initialize(), check server capabilities
caps = session.server_capabilities

if caps.tools:
    tools = await session.list_tools()

if caps.resources:
    resources = await session.list_resources()

if caps.prompts:
    prompts = await session.list_prompts()
Handling listChanged notifications

When a server's tools, resources, or prompts change at runtime, it sends a listChanged notification. Clients should listen for these and re-fetch the corresponding list to stay in sync. This is essential for servers that dynamically register or unregister capabilities.

Sampling

Sampling lets servers request LLM completions through the client, using sampling/createMessage. This keeps servers model-agnostic — the server asks the client to generate text, and the client decides which model to use.

  Server                    Client                   LLM
    |                         |                        |
    |  sampling/createMessage |                        |
    | ----------------------> |                        |
    |                         |   (user approval)      |
    |                         | ---------------------->|
    |                         |   completion result     |
    |                         | <----------------------|
    |   CreateMessageResult   |                        |
    | <---------------------- |                        |
Server requests a completion
# Server-side: ask the client to generate text
result = await ctx.session.create_message(
    messages=[
        {
            "role": "user",
            "content": {
                "type": "text",
                "text": "Summarize: " + document_text
            }
        }
    ],
    max_tokens=500
)
User consent required

Sampling is a powerful capability that gives servers indirect access to an LLM. Host applications MUST get user approval before fulfilling sampling requests. The user should see what the server is asking the model to do and have the ability to reject or modify the request. Never auto-approve sampling without human review.

Why Sampling?

Servers stay model-agnostic. They request text generation without knowing which model, provider, or configuration the client uses.

Model-agnostic

Human-in-the-Loop

The host controls approval. Users see sampling requests and can approve, modify, or reject them before they reach the LLM.

User consent
10

MCP vs Alternatives

How MCP compares to function calling, OpenAPI plugins, LangChain tools, and custom REST integrations.

The Decision Landscape

There are several approaches for connecting AI models to external capabilities. Each has distinct trade-offs in portability, complexity, and control. Understanding when to reach for each approach is key to building robust AI integrations.

MCP Servers

Universal protocol for AI-to-tool communication. Separate server processes that any MCP-compatible host can connect to.

Universal · Stateful · Dynamic

Direct API / Function Calling

Functions defined in-process or via provider-specific schemas. The host manages execution directly without a separate server.

Simple · Provider-tied · Static

Skills & Plugins

Host-specific extensions that customize AI behavior and add capabilities within a single application. Configuration-driven.

Host-specific · Lightweight · Behavioral

Agent Frameworks

Libraries like LangChain, CrewAI, or AutoGen that orchestrate multi-step AI workflows with built-in tool abstractions.

Orchestration · Multi-step · Framework-specific

MCP vs Direct Function Calling

Direct function calling (as provided by OpenAI, Anthropic, and other APIs) defines tool schemas alongside API requests. MCP separates tool hosting into independent server processes. The choice depends on whether you need portability or simplicity.

Aspect MCP Direct Function Calling
Architecture Separate server process Functions in same process
Reusability Any MCP-compatible host Tied to specific API provider
State management Server manages own state Host manages all state
Discovery Dynamic (tools/list) Static schema definition
Transport stdio or HTTP In-process or API call
Best for Shared tools across apps App-specific integrations

When to use MCP: You want the same tool available in Claude Desktop, VS Code, and custom applications. You need dynamic discovery or server-managed state.

When to use direct function calling: Simple, single-app integrations where the overhead of a separate server process is not justified. You are building for one API provider and do not need portability.

MCP vs Skills & Plugins

Skills and plugins (like Claude Code skills or ChatGPT plugins) are host-specific extensions that add capabilities to a single application. MCP servers are universal -- they work with any host that speaks the protocol.

Aspect MCP Servers Skills / Plugins
Scope Universal protocol Host-specific
Portability Works with any MCP host Tied to one application
Complexity Separate server process Configuration files
Discovery Protocol-level negotiation Host-specific registry
State Server-managed, persistent Varies by host
Best for Cross-app tools, external services Host customization, workflows
Key insight

Skills customize how the AI behaves. MCP servers give it capabilities. They are complementary, not competing. A Claude Code project might use skills to define commit message conventions alongside MCP servers for database access and deployment tooling.

MCP vs REST APIs

MCP wraps APIs with AI-friendly metadata -- rich schemas, semantic descriptions, and tool annotations. Raw REST APIs require the host to understand each API's conventions, authentication patterns, and response formats independently.

Aspect MCP Direct REST API
Schema JSON Schema + descriptions OpenAPI / Swagger
Discovery Dynamic tool listing Static documentation
AI integration Native (model invokes tools) Requires adapter code
Streaming SSE built-in Varies per API
Best for AI-first integrations General HTTP clients

Decision Flowchart

Use this guide when choosing how to connect your AI application to external capabilities.

  Need tool reusable across multiple AI apps?
    → YES → MCP Server
    → NO  ↓

  Building for a single AI app only?
    → YES → Direct Function Calling
    → NO  ↓

  Customizing AI behavior or persona?
    → YES → Skills / Plugins
    → NO  ↓

  Wrapping an existing API for AI consumption?
    → YES → MCP Server around the API
    → NO  ↓

  Simple one-off integration?
    → YES → Direct API Call
Complementary approaches

MCP and other approaches are complementary. Many real-world setups use MCP servers for shared capabilities alongside host-specific skills for workflow customization. A production AI system might combine direct function calling for internal business logic, MCP servers for shared developer tooling, and skills for team-specific conventions.

11

Ecosystem & Patterns

The growing MCP ecosystem -- popular servers, registries, agent composition patterns, and the future of the protocol.

Popular MCP Servers

The MCP ecosystem includes a growing collection of production-ready servers for common integrations. These are some of the most widely used.

Filesystem

Read and write files, search directories, and manage file metadata. Sandboxed to configured root directories.

@modelcontextprotocol/server-filesystem

GitHub

Repositories, issues, pull requests, code search, and branch management via the GitHub API.

@modelcontextprotocol/server-github

Slack

Channels, messages, threads, and user lookups. Read conversations and post messages to Slack workspaces.

@modelcontextprotocol/server-slack

PostgreSQL

Query databases, inspect schemas, list tables, and describe columns. Read-only by default for safety.

@modelcontextprotocol/server-postgres

Puppeteer

Browser automation, screenshots, page navigation, and DOM interaction via headless Chrome.

@modelcontextprotocol/server-puppeteer

Brave Search

Web search integration with Brave Search API. Returns structured results for AI consumption.

@modelcontextprotocol/server-brave-search

Server Registries

Finding and sharing MCP servers is supported by several registries and directories.

Registry Description URL
Official MCP Servers Reference implementations and officially maintained servers from Anthropic github.com/modelcontextprotocol/servers
Smithery.ai Community registry with search, installation guides, and server metadata smithery.ai
Community Lists Curated awesome-lists and community-maintained directories on GitHub github.com/punkpeye/awesome-mcp-servers

Debugging & Inspector

The MCP Inspector is the primary debugging tool for server development. It connects to any MCP server and lets you interactively test tools, resources, and prompts.

Launch Inspector
npx @modelcontextprotocol/inspector

The Inspector provides a web UI where you can connect to servers via stdio or HTTP, browse capabilities, invoke tools with test arguments, and inspect the raw JSON-RPC messages.

Issue Symptom Fix
Connection refused Client cannot connect to server Verify server is running, check config path, confirm transport settings
Tool not found tools/call returns method not found Check tool name matches exactly, verify server declared tools capability
Schema mismatch Tool call rejected with validation error Compare arguments against the tool's inputSchema, check required fields
Timeout Request hangs, no response Check server logs for errors, verify async handlers are awaited, check network

Debug checklist:

# 1. Verify config path is correct
cat ~/Library/Application\ Support/Claude/claude_desktop_config.json

# 2. Check server starts independently
python -m your_server          # Should start without errors

# 3. Test with Inspector
npx @modelcontextprotocol/inspector

# 4. Check capabilities negotiation
# Inspector shows the initialize handshake — verify tools/resources/prompts are listed

# 5. Check server stderr for errors (stdio transport)
# Server stderr is not captured by the protocol — use it for debug logging
Logging via MCP

Servers can send structured log messages to clients using the MCP logging capability. Call ctx.info(), ctx.warning(), or ctx.error() in FastMCP handlers. For stdio servers, always log to stderr -- stdout is reserved for protocol messages.

Security Best Practices

MCP servers expose capabilities to AI models. Security requires defense in depth -- validate inputs, limit permissions, and audit all tool invocations.

Least Privilege

Expose only the tools the model needs. A file server should not expose delete operations unless specifically required.

Minimize surface area

Input Validation

Always validate tool arguments server-side. Never trust that the model will send well-formed or safe inputs.

Server-side checks

Credential Management

Use environment variables for stdio servers and OAuth 2.1 for HTTP servers. Never embed credentials in tool schemas.

env vars · OAuth 2.1

Rate Limiting

Protect against excessive tool calls. Implement per-tool and per-session rate limits to prevent runaway usage.

Per-tool limits

Audit Logging

Log all tool invocations with timestamps, arguments, and results. Essential for debugging and compliance.

Full audit trail
The reality of MCP auth today

In surveyed MCP servers, 53% use static secrets (API keys, tokens) and only 8.5% implement OAuth. Most servers rely on environment variables for credentials — quick to set up but hard to rotate. For production HTTP servers, OAuth 2.1 with PKCE is the spec-recommended approach.

Never trust tool annotations from unverified servers

Tool annotations like readOnlyHint and destructiveHint are self-reported by the server. A malicious server can claim a destructive tool is read-only. Host applications must enforce their own safety policies and treat annotations from unverified servers as untrusted hints, not security guarantees.

Real-World Patterns

Production MCP deployments often combine multiple servers and add operational layers around the protocol.

Multi-Server Composition

Connect to GitHub, filesystem, and database servers simultaneously. Each maintains independent state and capabilities.

1 host → N servers

Gateway Pattern

A single MCP server that proxies requests to multiple backend services. Simplifies client configuration and centralizes auth.

1 server → N backends

Middleware Layers

Add logging, rate limiting, authentication, and metrics as middleware around tool handlers. FastMCP supports decorator patterns for this.

Logging · Auth · Metrics

Graceful Degradation

Handle server disconnects and capability changes at runtime. Re-fetch tool lists on reconnect, fall back when servers are unavailable.

Resilient connections
Composition in practice

A typical development setup might connect Claude Desktop to a filesystem server (for code context), a GitHub server (for issues and PRs), and a database server (for schema inspection) -- all running simultaneously as separate processes. The host manages routing and the model sees a unified set of tools from all connected servers.

JSON-RPC Error Codes & Retry Strategy

When protocol errors occur, the error code tells you whether retrying is safe.

Code Meaning Retry?
-32700 Parse error — malformed JSON No — fix the request
-32600 Invalid request — missing required fields No — fix the request
-32601 Method not found — unknown tool or method name No — check tool list
-32602 Invalid params — wrong argument types or missing required args No — fix arguments
-32603 Internal error — server crashed or unexpected failure Yes — with backoff

Middleware Pipeline

Production HTTP servers benefit from a middleware stack around MCP tool handlers. Process requests through layers before they reach your tool logic.

Middleware ordering (outside → inside)
Request flow:

┌─────────────────────────────────────────┐
│  1. Authentication                      │
│  Verify OAuth token / API key           │
│  ┌─────────────────────────────────────┐│
│  │  2. Rate Limiting                   ││
│  │  Per-session + per-tool limits      ││
│  │  ┌─────────────────────────────────┐││
│  │  │  3. Audit Logging               │││
│  │  │  Log tool name, args, caller    │││
│  │  │  ┌─────────────────────────────┐│││
│  │  │  │  4. Circuit Breaker         ││││
│  │  │  │  Skip if backend is down    ││││
│  │  │  │  ┌─────────────────────────┐││││
│  │  │  │  │  Tool Handler          │││││
│  │  │  │  └─────────────────────────┘││││
│  │  │  └─────────────────────────────┘│││
│  │  └─────────────────────────────────┘││
│  └─────────────────────────────────────┘│
└─────────────────────────────────────────┘

Circuit Breaker States

Closed → requests pass through normally. After N consecutive failures → Open (all requests fail immediately). After a timeout → Half-Open (one probe request allowed). If probe succeeds → back to Closed.

ClosedOpenHalf-OpenClosed