Back to all articles
13 MIN READ

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.

Loading diagram…

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.

Loading diagram…

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

  1. Provide modelPreferences with hints, not requirements. The client chooses the final model.
  2. Limit maxTokens to what's strictly necessary — sampling consumes the user's budget.
  3. Clear messages: The prompt sent to the LLM must be self-contained (no implicit context).
  4. 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.

Loading diagram…

The 6 MCP Security Principles

  1. Input validation: Every tool argument must be validated (type, format, bounds).
  2. Least privilege: A server should only expose the tools strictly necessary.
  3. Human-in-the-loop: Sensitive operations require explicit approval.
  4. Logging: Every tool invocation must be logged with its parameters and results.
  5. Isolation: Code execution must be sandboxed (containers, VMs, etc.).
  6. 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:

CategoryCheckPriority
TransportStreamable HTTP configured with sessionsCritical
AuthOAuth 2.1 implemented and testedCritical
TLSHTTPS only, valid certificatesCritical
ValidationAll tool inputs validatedHigh
Rate LimitPer-client limits configuredHigh
LoggingEvery invocation loggedHigh
ErrorsCircuit breaker / retry implementedMedium
MonitoringLatency and error rate metricsMedium
SandboxCode execution isolatedCase-dependent
ProgressProgress tokens for long opsCase-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.
Newsletter

Weekly AI Insights

Tools, techniques & news — curated for AI practitioners. Free, no spam.

Free, no spam. Unsubscribe anytime.

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.