Advanced MCP: Transports, Sampling, and Production Patterns
By Learnia AI Research Team
Advanced MCP: Transports, Sampling, and Production Patterns
The Model Context Protocol (MCP) goes far beyond a simple client-server interface. This guide explores transport mechanisms, sampling (server-initiated model interaction), notifications, file system access, and production deployment patterns. If you're new to MCP, start with our introduction to MCP with Claude Code.
The 3 MCP Transport Mechanisms
The transport layer determines how JSON-RPC messages flow between the MCP client and server. Each transport has different tradeoffs in complexity, performance, and use cases.
STDIO — Local Transport
The simplest transport: the client spawns the server as a subprocess and communicates via stdin/stdout.
// MCP server with STDIO transport (TypeScript SDK)
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
const server = new McpServer({
name: "file-analyzer",
version: "1.0.0",
});
// Declare a tool
server.tool(
"analyze_file",
"Analyzes the content of a file",
{ path: { type: "string", description: "File path" } },
async ({ path }) => {
const content = await fs.readFile(path, "utf-8");
return {
content: [{ type: "text", text: `File: ${content.length} characters` }],
};
}
);
// Start with STDIO
const transport = new StdioServerTransport();
await server.connect(transport);
Advantages: Zero network configuration, minimal latency, process isolation. Limitations: Local only, one client per server, no session resumption.
SSE — Server-Sent Events (Legacy)
SSE uses HTTP for client → server requests and an event stream for server → client messages.
// MCP server with SSE transport
import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
import express from "express";
const app = express();
app.get("/sse", async (req, res) => {
const transport = new SSEServerTransport("/messages", res);
await server.connect(transport);
});
app.post("/messages", async (req, res) => {
await transport.handlePostMessage(req, res);
});
app.listen(3001);
⚠️ SSE is considered legacy in the current MCP specification. Prefer Streamable HTTP for new projects.
Streamable HTTP — The Production Standard
The recommended transport for production. The client sends HTTP POST requests, and the server can respond with either standard JSON or open an SSE stream for streaming.
// MCP server with Streamable HTTP transport
import { StreamableHTTPServerTransport } from
"@modelcontextprotocol/sdk/server/streamableHttp.js";
const transport = new StreamableHTTPServerTransport({
sessionIdGenerator: () => crypto.randomUUID(),
onsessioninitialized: (sessionId) => {
sessions.set(sessionId, transport);
},
});
app.post("/mcp", async (req, res) => {
const sessionId = req.headers["mcp-session-id"];
// Route to existing session or create new one
await transport.handleRequest(req, res);
});
// GET support for server → client streaming
app.get("/mcp", async (req, res) => {
const sessionId = req.headers["mcp-session-id"];
await transport.handleSSEStream(req, res);
});
// DELETE support for session termination
app.delete("/mcp", async (req, res) => {
const sessionId = req.headers["mcp-session-id"];
sessions.delete(sessionId);
res.status(200).end();
});
Advantages: Compatible with existing HTTP infrastructure, session resumption, optional streaming, multi-client.
Sampling: When the Server Asks the Model
Sampling is one of MCP's most powerful and least understood features. It reverses the usual flow: instead of the client calling the server, the server asks the client to perform an LLM call.
Why Sampling?
Some server-side operations require intermediate "AI reasoning." For example:
- →Code analysis: The server reads files but needs the LLM to identify problematic patterns.
- →Iterative summarization: The server collects data, asks for a summary, then refines it.
- →Contextual decisions: The server needs the model's judgment to decide on the next step.
// Server-side: request sampling from the client
server.tool(
"smart_refactor",
"Intelligently refactors a file",
{ filePath: { type: "string" } },
async ({ filePath }) => {
const code = await fs.readFile(filePath, "utf-8");
// Ask the client to call the LLM
const analysis = await server.createMessage({
messages: [
{
role: "user",
content: {
type: "text",
text: `Analyze this code and identify improvements:\n\n${code}`,
},
},
],
maxTokens: 2048,
modelPreferences: {
hints: [{ name: "claude-sonnet-4-20250514" }],
},
});
// Use the LLM response to perform refactoring
return {
content: [{ type: "text", text: analysis.content.text }],
};
}
);
# Server-side in Python: sampling
@server.tool("smart_refactor")
async def smart_refactor(file_path: str) -> str:
code = Path(file_path).read_text()
# The server asks the client to call the LLM
result = await server.create_message(
messages=[
{
"role": "user",
"content": {
"type": "text",
"text": f"Analyze this code and identify improvements:\n\n{code}",
},
}
],
max_tokens=2048,
model_preferences={
"hints": [{"name": "claude-sonnet-4-20250514"}]
},
)
return result.content.text
Sampling Best Practices
- →Provide
modelPreferenceswith hints, not requirements. The client chooses the final model. - →Limit
maxTokensto what's strictly necessary — sampling consumes the user's budget. - →Clear messages: The prompt sent to the LLM must be self-contained (no implicit context).
- →Handle rejection: The client can refuse a sampling request. Always provide a fallback path.
Notifications and Progress Tokens
MCP is a bidirectional protocol that supports notifications — messages that don't expect a response.
Server → Client Notifications
// Server notifies about a resource change
server.notification({
method: "notifications/resources/updated",
params: { uri: "file:///project/config.json" },
});
// Server notifies that its tool list has changed
server.notification({
method: "notifications/tools/list_changed",
});
Progress Tokens — Tracking Long-Running Operations
For long-running operations, the client can send a progressToken that the server uses to emit progress updates.
// Client-side: send a progress token
const result = await client.callTool({
name: "index_repository",
arguments: { repoPath: "/project" },
_meta: { progressToken: "idx-001" },
});
// Server-side: emit progress notifications
server.tool(
"index_repository",
"Indexes an entire repository",
{ repoPath: { type: "string" } },
async ({ repoPath }, { meta }) => {
const files = await glob(`${repoPath}/**/*`);
for (let i = 0; i < files.length; i++) {
// Notify progress
await server.notification({
method: "notifications/progress",
params: {
progressToken: meta.progressToken,
progress: i + 1,
total: files.length,
message: `Indexing: ${files[i]}`,
},
});
await indexFile(files[i]);
}
return {
content: [{ type: "text", text: `${files.length} files indexed` }],
};
}
);
File System Access
One of the most common MCP use cases is secure file system access. The official @modelcontextprotocol/server-filesystem server illustrates the recommended patterns.
Principle of Least Privilege
// Configuration: limit accessible directories
const ALLOWED_DIRS = [
"/home/user/project",
"/home/user/docs",
];
function validatePath(requestedPath: string): string {
const resolved = path.resolve(requestedPath);
const isAllowed = ALLOWED_DIRS.some(
(dir) => resolved.startsWith(dir)
);
if (!isAllowed) {
throw new Error(`Access denied: ${resolved} outside allowed directories`);
}
return resolved;
}
// Secure read tool
server.tool(
"read_file",
"Reads file content within allowed directories",
{ path: { type: "string" } },
async ({ path: filePath }) => {
const safePath = validatePath(filePath);
const content = await fs.readFile(safePath, "utf-8");
return {
content: [{ type: "text", text: content }],
};
}
);
Exposing Files as MCP Resources
MCP resources allow exposing files to the client in a structured way:
// Expose configuration files as resources
server.resource(
"project-config",
"file:///project/config.json",
"Project configuration",
async () => {
const config = await fs.readFile("/project/config.json", "utf-8");
return {
contents: [{
uri: "file:///project/config.json",
mimeType: "application/json",
text: config,
}],
};
}
);
MCP Security Model
MCP security relies on multiple layers. Understanding this model is essential before any production deployment. For complementary patterns on secure agent architecture, see our agent architecture patterns guide.
The 6 MCP Security Principles
- →Input validation: Every tool argument must be validated (type, format, bounds).
- →Least privilege: A server should only expose the tools strictly necessary.
- →Human-in-the-loop: Sensitive operations require explicit approval.
- →Logging: Every tool invocation must be logged with its parameters and results.
- →Isolation: Code execution must be sandboxed (containers, VMs, etc.).
- →Authentication: Use OAuth 2.1 for network transports.
// Security middleware for a production MCP server
function securityMiddleware(handler) {
return async (req, res) => {
// 1. Verify OAuth token
const token = req.headers.authorization?.replace("Bearer ", "");
if (!token || !await verifyOAuthToken(token)) {
return res.status(401).json({ error: "Unauthorized" });
}
// 2. Rate limiting
const clientId = extractClientId(token);
if (await isRateLimited(clientId)) {
return res.status(429).json({ error: "Too many requests" });
}
// 3. Audit logging
logger.info("mcp_request", {
clientId,
method: req.body?.method,
params: sanitize(req.body?.params),
timestamp: new Date().toISOString(),
});
return handler(req, res);
};
}
Production Deployment Patterns
Pattern 1: MCP Gateway
A reverse proxy centralizes authentication, routing, and monitoring for multiple MCP servers.
// MCP Gateway with Express
import express from "express";
const app = express();
const SERVER_REGISTRY = {
"file-server": { url: "http://localhost:3001/mcp", auth: "internal" },
"db-server": { url: "http://localhost:3002/mcp", auth: "oauth" },
"search-server": { url: "http://localhost:3003/mcp", auth: "api-key" },
};
app.post("/mcp/:serverId", securityMiddleware(async (req, res) => {
const { serverId } = req.params;
const server = SERVER_REGISTRY[serverId];
if (!server) {
return res.status(404).json({ error: "Unknown server" });
}
// Proxy to the target MCP server
const response = await fetch(server.url, {
method: "POST",
headers: {
"Content-Type": "application/json",
"mcp-session-id": req.headers["mcp-session-id"],
},
body: JSON.stringify(req.body),
});
const data = await response.json();
res.json(data);
}));
Pattern 2: Multi-Server with Discovery
# MCP client with dynamic server discovery (Python)
from mcp import ClientSession
from mcp.client.streamable_http import streamablehttp_client
class McpOrchestrator:
def __init__(self):
self.servers: dict[str, ClientSession] = {}
async def discover_and_connect(self, registry_url: str):
"""Discovers and connects to available MCP servers."""
async with httpx.AsyncClient() as http:
registry = await http.get(registry_url)
servers = registry.json()["servers"]
for server_info in servers:
async with streamablehttp_client(server_info["url"]) as (
read, write, _
):
session = ClientSession(read, write)
await session.initialize()
self.servers[server_info["name"]] = session
tools = await session.list_tools()
print(f" {server_info['name']}: {len(tools.tools)} tools")
async def call_tool(self, server_name: str, tool_name: str, args: dict):
"""Calls a tool on a specific server."""
session = self.servers[server_name]
result = await session.call_tool(tool_name, args)
return result
Pattern 3: Robust Error Handling
// Error handling with retry and circuit breaker
class ResilientMcpClient {
private circuitOpen = false;
private failureCount = 0;
private readonly MAX_FAILURES = 5;
private readonly RETRY_DELAY = 1000;
async callTool(name: string, args: Record<string, unknown>) {
if (this.circuitOpen) {
throw new Error("Circuit open — server unavailable");
}
for (let attempt = 0; attempt < 3; attempt++) {
try {
const result = await this.session.callTool({ name, arguments: args });
this.failureCount = 0; // Reset on success
return result;
} catch (error) {
this.failureCount++;
if (this.failureCount >= this.MAX_FAILURES) {
this.circuitOpen = true;
setTimeout(() => {
this.circuitOpen = false;
this.failureCount = 0;
}, 30_000); // Retry after 30s
}
if (attempt < 2) {
await new Promise((r) =>
setTimeout(r, this.RETRY_DELAY * (attempt + 1))
);
}
}
}
throw new Error(`Failed after 3 attempts: ${name}`);
}
}
For more production patterns with Claude, see our prompt caching and MCP protocol guide.
Production Deployment Checklist
Before deploying an MCP server to production, validate this checklist:
| Category | Check | Priority |
|---|---|---|
| Transport | Streamable HTTP configured with sessions | Critical |
| Auth | OAuth 2.1 implemented and tested | Critical |
| TLS | HTTPS only, valid certificates | Critical |
| Validation | All tool inputs validated | High |
| Rate Limit | Per-client limits configured | High |
| Logging | Every invocation logged | High |
| Errors | Circuit breaker / retry implemented | Medium |
| Monitoring | Latency and error rate metrics | Medium |
| Sandbox | Code execution isolated | Case-dependent |
| Progress | Progress tokens for long ops | Case-dependent |
To integrate these patterns into a team workflow, see our team collaboration guide with Claude Code. For a deeper dive into using tools with Claude, see our comprehensive tool use guide.
Conclusion
Production MCP demands a deep understanding of transport mechanisms, the security model, and resilience patterns. Key takeaways:
- →Use Streamable HTTP for any network deployment — SSE is legacy.
- →Sampling unlocks unique capabilities but requires strict client-side control.
- →Notifications and progress tokens improve UX for long-running operations.
- →Security is multi-layered: transport, authentication, validation, sandboxing.
- →Gateway and Circuit Breaker patterns are essential in production.
Weekly AI Insights
Tools, techniques & news — curated for AI practitioners. Free, no spam.
Free, no spam. Unsubscribe anytime.
→Related Articles
FAQ
What are the differences between the three MCP transport mechanisms?+
STDIO is the simplest, ideal for local processes with minimal latency. SSE (Server-Sent Events) enables one-way server-to-client streaming over HTTP. Streamable HTTP is the recommended production transport: it combines standard HTTP requests with optional SSE streaming and native session resumption support.
What is sampling in the MCP protocol?+
Sampling allows an MCP server to request that the client perform an LLM call. This reverses the usual flow: instead of the client calling the server, the server initiates a model interaction through the client, while keeping the user in the control loop.
How do you secure an MCP server in production?+
Best practices include: validating all inputs server-side, implementing least privilege for tools, using OAuth 2.1 for authentication, logging all tool invocations, rate limiting requests, and sandboxing code execution where necessary.
When should you use progress tokens in MCP?+
Progress tokens are useful for long-running operations (file indexing, downloads, complex analyses). The client sends a _meta.progressToken in its request, and the server emits notifications/progress with current status, allowing the client to display a progress bar.