The Complete Guide to Implementing the Model Context Protocol in 2025: From Spec Evolution to Streamable HTTP

The Complete Guide to Implementing the Model Context Protocol in 2025: From Spec Evolution to Streamable HTTP

Hello!
MCP is spreading through the LLM industry at breakneck speed, and today we want to focus specifically on the implementation side.

We say "MCP" as if it were a single thing, but the specification people call the "standard" has actually changed quite a bit in a short period. So we will walk through the variations of the spec in order, and then build a real implementation.


The Model Context Protocol (MCP), announced by Anthropic in late 2024, marked an important turning point for the AI field.

It standardized the tool-invocation capability (known as tool use) that each AI vendor had previously implemented in its own way, providing a unified mechanism for connecting AI models with external systems

In this article, we trace in detail the technical evolution of MCP from its birth to the present, and explain the best implementation approach as of 2025, complete with full source code. In particular, from the perspective of implementers who tend to get whipsawed by spec changes, we clarify why things converged on the current form and which implementation approach you should take going forward.

Chapter 1: The Problem MCP Set Out to Solve

The chaotic state of AI tool invocation

Until 2024, each model provider used its own tool invocation specification. OpenAI had Function Calling, Anthropic and Meta had Tool Use, Google had Function Declarations — the names and the implementations were all over the place.

Even for something as simple as "get the current time," developers had to prepare a different implementation for each LLM provider.

Granted, when a new technology first emerges, putting interoperability on the back burner is a classic pattern in the software industry.

But this situation was not merely tedious — it limited what AI agents could become. Building systems that combine multiple AI models, or applications that can swap models, was difficult, and tool reusability was remarkably poor.

MCP as the answer

MCP was a clear answer to this chaos. Built on JSON-RPC 2.0, it standardized everything needed for AI-tool integration, from tool definitions to session management and streaming communication. Implement an MCP server once, and the same tools become usable from Claude Desktop, VS Code, and any other MCP-capable client.

Chapter 2: How the Spec Evolved, and Why

The stdio era — an ideal design for local execution

Early MCP assumed communication over standard input/output (stdio). This is the most basic form of inter-process communication, and in local environments it was efficient.

# Conceptual early stdio implementation
import sys
import json

while True:
    line = sys.stdin.readline()
    request = json.loads(line)
    # Handle the request
    response = handle_request(request)
    sys.stdout.write(json.dumps(response) + "\n")
    sys.stdout.flush()

However, this design has a fundamental constraint.

That's right — stdio is a communication model premised on a parent-child process relationship, and it is inherently unsuited to communication over a network.

One wonders how many engineers today even think of stdio as a communication channel. Aside from people like me who wrote code back when a black console was all we had, or the folks still writing C for a living, stdio is probably not something most people consciously think about.

Still, for developers using it from VS Code or a local Claude Desktop, it posed no particular problem.

But serving multiple concurrent users as a web service, or running as part of a distributed system, was simply never part of the design ("why not?", you might ask).

The HTTP+SSE era — the first step toward network support

Next came the approach combining HTTP POST with Server-Sent Events (SSE). It is an asymmetric model: HTTP POST from client to server, SSE from server to client.

This approach had a certain logic to it. SSE is natively supported in browsers, and real-time progress notifications were possible. But implementation complexity grew. Session management, separate endpoints, connection synchronization — there was simply too much for developers to keep track of.
That is the kind of tedium peculiar to SSE.

The Streamable HTTP era — a mature, unified specification

And now, as of 2025, MCP has converged on Streamable HTTP: a refined design in which a single HTTP endpoint handles both regular request/response and streaming.

# The basic idea of Streamable HTTP
@app.post("/mcp")
async def mcp_endpoint(request: Request):
    if "text/event-stream" in request.headers.get("accept", ""):
        # Streaming response
        return StreamingResponse(generate_sse())
    else:
        # Regular JSON response
        return JSONResponse(handle_request(await request.json()))

This unification greatly reduced implementation complexity while improving scalability at the same time. Laying out the characteristics of each transport reveals the following progression.

The evolution of MCP transports

Aspect Local-only implementation Remote-capable
stdio HTTP+SSE Streamable HTTP
Introduced Early (late 2024) Transitional (end of 2024) Current (2025)
Communication Standard I/O
direct inter-process communication
HTTP POST (C→S)
SSE (S→C)
asymmetric two-way communication
Single HTTP endpoint
auto-switched via Accept header
Protocol JSON-RPC 2.0
newline-delimited
JSON-RPC 2.0
separate POST/SSE
JSON-RPC 2.0
unified
Implementation complexity Low (simple loop) High (two endpoints to manage) Medium (simplified by SDKs)
Streaming Newline-based SSE events SSE/chunked auto-selected
Session management Process lifecycle URL parameters (manual) Mcp-Session-Id header (standardized)
Scalability N/A (local only) △ Limited (SSE connection management is a challenge) ◎ Excellent (can be stateless)
Typical uses VS Code extensions
CLI tools
local IDEs
Experimental implementations
small demos
prototypes
Production web services
enterprise APIs
SaaS products
Advantages Fastest
simple to implement
secure
Network-capable
browser-compatible
built from existing tech
Unified implementation
cloud-native
works with ops tooling
Disadvantages No remote access
hard to expose as a web service
Complex implementation
hard to debug
scaling challenges
HTTP overhead
overkill for local use

What this table makes clear is that MCP started with stdio, specialized for local execution, passed through the transitional HTTP+SSE implementation to answer the demand for remote access, and finally converged on Streamable HTTP as the unified solution.

Today there is a clear division of use: stdio for local-only scenarios, Streamable HTTP whenever remote access is a possibility.

The essential significance of streaming

Now — why is streaming such a good thing in MCP in the first place?

What is the point of returning responses in streaming form?

Let's start there.

"Streaming" carries a meaning that goes well beyond a mere choice of transport.

First, let's compare the traditional synchronous communication model with the asynchronous streaming model in diagrams.

The synchronous model looks something like this.

[Traditional synchronous model]

User ──────> Web UI ──────> LLM ──────> MCP Server
                               │             │
                               │             ▼
                               │        [Execute task]
                               │         (long-running)
                               │             │
                               ◄─────────────┘
                               │         (waiting...)
                               ▼
                       [Generate response]
                               │
User ◄────── Web UI ◄──────────┘

Problem: the LLM waits blindly until the task completes

Next, the streaming model

[Streaming model]

User ──────> Web UI ──────> LLM ──────> MCP Server
     │          │             │             │
     │          │             │             ▼
     │          │             │        [Task started]
     │          │             │             │
     │          │             ◄─ stream ────┤ "Task started"
     │          │             │             │
     │          │       [Interim reply]     ▼
     │          ◄─ stream ────┤        [Processing 30%]
     │      "Working..."      │             │
     ◄──────────┤             ◄─ stream ────┤ "30% done"
                │             │             │
                │      [Adjust strategy]    ▼
                │             │        [Processing 60%]
                │             ◄─ stream ────┤ "60% done"
     │          ◄─ stream ────┤             │
     │      "Almost there..." │             ▼
     ◄──────────┤             │        [Task complete]
                │             ◄─ stream ────┤ "Result: XXX"
                │             │             │
                │        [Final reply]      │
                ◄─ stream ────┤             │
     │      "All done"        │             │
     ◄──────────┘             │             │

Benefit: every participant shares the state in real time

In application development centered on AI models, for the model to make progress step by step while conversing with external tools, a mechanism that returns intermediate progress as it happens is indispensable.

The AI needs to keep generating its response while understanding "what is happening right now" and "how far the work has progressed." A synchronous request model that returns only the final result in one lump usually cannot satisfy that requirement.

[Adaptive processing via streaming]

LLM ──────> MCP Server (database search)
 │              │
 │              ▼ stream: "Searched 100 of 1000 records..."
 │◄─────────────┤
 │              │
 ├─Decision: taking longer than expected
 │       
 ├──────────> MCP Server (narrow the search criteria)
 │              │
 │              ▼ stream: "Criteria changed: narrowed to 50 records"
 │◄─────────────┤
 │              │
 │              ▼ stream: "Search complete"
 │◄─────────────┤
 │
 ▼
[Generate response for the user]
"I optimized the search scope and retrieved results
 from the 50 most relevant records"

The greatest significance of streaming is that the model no longer has to keep "waiting" on external work — it can "continue generating its response while observing the progress of that work." AI models have no true asynchronous processing of their own. When an external tool ran for a long time, the model had no choice but to sit blocked on the response. With streaming, the external MCP server returns partial progress and intermediate data to the AI as it goes, and the AI can make its next decision each time something arrives.

[Improved user experience]

Before:
User ──> Web UI ──> [Loading spinner...] ──> (anxious waiting) ──> result

Streaming:
User ──> Web UI ┬─> "Connecting to the database..."
                ├─> "Searching 1,000 records..."
                ├─> "Analyzing related data..."
                ├─> "Formatting results..."
                └─> "Done: [detailed results]"

The user can see what is happening at each step,
greatly improving transparency and trust

Streaming responses are also decisively important for the application. In a conventional server implementation, when a task took a long time, the user was left in an opaque "wait for the response" state. In an AI-centered UI, that translates directly into the quality of the experience. When the MCP server streams results and logs incrementally, the application can reflect them in the UI as they arrive, making "what the AI is doing" visible.

For example, when an external tool is rendering a video, sending progress like "67% complete" back to the web UI clearly improves the UX.

Moreover, this mechanism ties directly into making AI agents more sophisticated. Having external results flow in as a stream makes it possible, for the first time, for the AI model to "look at partial results and change its strategy". That is what makes this feel so distinctly like the AI era.

[Recovering from errors]

LLM ──────> MCP Server (API call)
 │              │
 │              ▼ stream: "Connecting to the API..."
 │◄─────────────┤
 │              │
 │              ▼ stream: "Error: rate limited"
 │◄─────────────┤
 │              
 ├─Decision: use a fallback
 │       
 ├──────────> MCP Server (fetch from cache)
 │              │
 │              ▼ stream: "Fetching from cache..."
 │◄─────────────┤
 │              │
 │              ▼ stream: "Success: data retrieved"
 │◄─────────────┤
 │
 ▼
[Response to the user]
"Fetching the latest data failed, so I used
 cached data from one hour ago"

If an anomaly is detected midway through a long task, the AI can immediately propose an alternative, or recognize the moments when additional instructions are needed after work has begun. With a synchronous response returned all at once, this kind of incremental decision-making was fundamentally impossible.

In short, the significance of MCP servers returning streaming responses is not merely a matter of efficiency. It is the foundation for a new execution model in which the AI model, the MCP server, and the application share the same timeline and "work together while conversing" through incremental exchanges of information. And Streamable HTTP can be called MCP's mature form: it maximizes the value of streaming while keeping implementation complexity to a minimum.

Chapter 3: A Complete Implementation — a Spec-Compliant Server with FastMCP

Why you should use FastMCP

The MCP spec looks simple at first glance, but once you try to implement a compliant server, the number of details to handle balloons: batch request handling, 204 responses for notification-only messages, managing the Mcp-Session-Id header, and much more.

FastMCP absorbs all of this and gives developers an environment where they can focus on business logic.

A complete MCP server implementation

Below is a production-ready MCP server implementation using FastMCP.

#!/usr/bin/env python3
"""
Production-Ready MCP DateTime Server
A complete MCP server implementation ready for production use
"""
import asyncio
from datetime import datetime, timedelta
from typing import Dict, Any, Optional
import pytz
import logging
from mcp.server.fastmcp import FastMCP

# Logging configuration
logging.basicConfig(
    level=logging.INFO,
    format='%(asctime)s [%(levelname)s] %(name)s: %(message)s'
)
logger = logging.getLogger("mcp-datetime-server")

# Timezone configuration
JST = pytz.timezone('Asia/Tokyo')

# Create the FastMCP instance
mcp = FastMCP(
    name="datetime-server-production",
    version="1.0.0"
)

# Server metadata
mcp.metadata = {
    "description": "Production-ready datetime server with JST support",
    "author": "Your Organization",
    "timezone": "Asia/Tokyo",
    "supported_languages": ["ja", "en"]
}


@mcp.tool(
    description="Gets the current time in Japan Standard Time (JST)"
)
async def get_current_time(format: str = "standard") -> str:
    """
    Return the current time in the specified format
    
    Args:
        format: output format (standard, iso, unix)
    """
    now = datetime.now(JST)
    
    if format == "iso":
        return now.isoformat()
    elif format == "unix":
        return str(int(now.timestamp()))
    else:
        return f"The current time is {now.strftime('%H:%M:%S')}"


@mcp.tool(
    description="Gets today's date in Japanese format"
)
async def get_current_date(
    include_era: bool = False
) -> str:
    """
    Return the current date (output uses Japanese date formatting)
    
    Args:
        include_era: whether to include the Japanese era (wareki)
    """
    now = datetime.now(JST)
    date_str = now.strftime("%Y年%m月%d日")
    day_of_week = ["月", "火", "水", "木", "金", "土", "日"][now.weekday()]
    
    result = f"今日は {date_str} ({day_of_week}曜日) です"  # "Today is <date> (<weekday>)" in Japanese
    
    if include_era:
        # Reiwa era calculation (the era started May 1, 2019)
        reiwa_start = datetime(2019, 5, 1, tzinfo=JST)
        if now >= reiwa_start:
            reiwa_year = now.year - 2018
            result += f" (令和{reiwa_year}年)"  # "(Reiwa year N)"
    
    return result


@mcp.tool(
    description="Calculates the date a given number of days ahead"
)
async def calculate_future_date(
    days: int,
    from_date: Optional[str] = None
) -> str:
    """
    Calculate a future or past date (output uses Japanese date formatting)
    
    Args:
        days: number of days (negative for the past)
        from_date: starting date (ISO format; today if omitted)
    """
    if from_date:
        base_date = datetime.fromisoformat(from_date).replace(tzinfo=JST)
    else:
        base_date = datetime.now(JST)
    
    target_date = base_date + timedelta(days=days)
    date_str = target_date.strftime("%Y年%m月%d日")
    day_of_week = ["月", "火", "水", "木", "金", "土", "日"][target_date.weekday()]
    
    if days > 0:
        return f"{days}日後は {date_str} ({day_of_week}曜日) です"  # "N days from now is <date>"
    elif days < 0:
        return f"{abs(days)}日前は {date_str} ({day_of_week}曜日) でした"  # "N days ago was <date>"
    else:
        return f"指定日は {date_str} ({day_of_week}曜日) です"  # "The specified day is <date>"


@mcp.tool(
    description="Calculates the number of days between two dates"
)
async def calculate_days_between(
    date1: str,
    date2: str
) -> str:
    """
    Calculate the difference in days between two dates
    
    Args:
        date1: the first date (ISO format)
        date2: the second date (ISO format)
    """
    d1 = datetime.fromisoformat(date1).replace(tzinfo=JST)
    d2 = datetime.fromisoformat(date2).replace(tzinfo=JST)
    
    diff = abs((d2 - d1).days)
    
    if d1 < d2:
        return f"It is {diff} days from {date1} to {date2}"
    elif d1 > d2:
        return f"It is {diff} days from {date2} to {date1}"
    else:
        return "The dates are the same"


@mcp.resource(
    uri="timezone://current",
    name="Current Timezone Information",
    description="Detailed information about the current timezone",
    mime_type="application/json"
)
async def get_timezone_resource() -> Dict[str, Any]:
    """Provide timezone information as a resource"""
    now = datetime.now(JST)
    utc_now = datetime.now(pytz.UTC)
    
    return {
        "timezone": "Asia/Tokyo",
        "abbreviation": "JST",
        "offset": "+09:00",
        "offset_seconds": 32400,
        "current_jst": now.isoformat(),
        "current_utc": utc_now.isoformat(),
        "is_dst": False
    }


@mcp.on_initialize
async def on_initialize(params: Dict[str, Any]) -> None:
    """Handling at server initialization"""
    client_info = params.get("clientInfo", {})
    logger.info(f"MCP Server initialized by {client_info.get('name', 'unknown')}")
    logger.info(f"Protocol version: {params.get('protocolVersion')}")


@mcp.on_error
async def on_error(error: Exception) -> None:
    """Handling when an error occurs"""
    logger.error(f"MCP Server error: {error}", exc_info=True)


def main():
    """Main entry point"""
    import argparse
    
    parser = argparse.ArgumentParser(
        description="Production MCP DateTime Server"
    )
    parser.add_argument(
        "--port", type=int, default=8080,
        help="Server port (default: 8080)"
    )
    parser.add_argument(
        "--host", type=str, default="0.0.0.0",
        help="Server host (default: 0.0.0.0)"
    )
    
    args = parser.parse_args()
    
    print(f"Starting MCP Server on {args.host}:{args.port}")
    print(f"Endpoint: http://{args.host}:{args.port}/mcp")
    
    # FastMCP automates all of the protocol handling
    mcp.run(
        transport="streamable-http",
        host=args.host,
        port=args.port
    )


if __name__ == "__main__":
    main()

The important point about this code is that using FastMCP's decorators automatically guarantees spec compliance. Developers can focus on tool logic without having to think about protocol details.

Chapter 4: Client Implementations — Integrating with Each LLM Provider

With the MCP server complete, the next step is the client side. We need to integrate each LLM provider's API with MCP.

Claude (Anthropic) client implementation

Claude, being the model from Anthropic — MCP's creator — integrates with MCP the most naturally.

import asyncio
from typing import List, Dict, Any
from mcp.client.session import ClientSession
from mcp.client.sse import sse_client
from anthropic import AsyncAnthropic

class ClaudeMCPIntegration:
    def __init__(self, mcp_url: str, api_key: str):
        self.mcp_url = mcp_url
        self.anthropic = AsyncAnthropic(api_key=api_key)
        self.session = None
        
    async def connect(self):
        """Connect to the MCP server"""
        transport = await sse_client(self.mcp_url)
        self.session = ClientSession(
            transport.read_stream,
            transport.write_stream
        )
        await self.session.__aenter__()
        await self.session.initialize()
        
    async def ask_claude_with_tools(self, prompt: str) -> str:
        """Ask Claude a question using MCP tools"""
        # Get the list of MCP tools
        tools_result = await self.session.list_tools()
        
        # Convert to the Claude API format
        claude_tools = []
        for tool in tools_result.tools:
            claude_tools.append({
                "name": tool.name,
                "description": tool.description,
                "input_schema": tool.inputSchema
            })
        
        # Query Claude
        response = await self.anthropic.messages.create(
            model="claude-3-5-sonnet-20241022",
            max_tokens=1024,
            tools=claude_tools,
            messages=[{"role": "user", "content": prompt}]
        )
        
        # Handle tool calls
        for content in response.content:
            if content.type == "tool_use":
                # Execute the MCP tool
                result = await self.session.call_tool(
                    content.name,
                    content.input or {}
                )
                
                # Return the result to Claude to generate the final answer
                final_response = await self.anthropic.messages.create(
                    model="claude-3-5-sonnet-20241022",
                    max_tokens=1024,
                    messages=[
                        {"role": "user", "content": prompt},
                        {"role": "assistant", "content": response.content},
                        {
                            "role": "user",
                            "content": [{
                                "type": "tool_result",
                                "tool_use_id": content.id,
                                "content": result.content[0].text
                            }]
                        }
                    ]
                )
                
                return final_response.content[0].text
        
        return response.content[0].text

OpenAI GPT client implementation

OpenAI's Function Calling also integrates seamlessly with MCP tools.

from openai import AsyncOpenAI
import json

class OpenAIMCPIntegration:
    def __init__(self, mcp_url: str, api_key: str):
        self.mcp_url = mcp_url
        self.openai = AsyncOpenAI(api_key=api_key)
        self.session = None
    
    async def connect(self):
        """Connect to the MCP server"""
        transport = await sse_client(self.mcp_url)
        self.session = ClientSession(
            transport.read_stream,
            transport.write_stream
        )
        await self.session.__aenter__()
        await self.session.initialize()
    
    async def ask_gpt_with_tools(self, prompt: str) -> str:
        """Ask GPT a question using MCP tools"""
        # Get the list of MCP tools
        tools_result = await self.session.list_tools()
        
        # Convert to the OpenAI format
        openai_tools = []
        for tool in tools_result.tools:
            openai_tools.append({
                "type": "function",
                "function": {
                    "name": tool.name,
                    "description": tool.description,
                    "parameters": tool.inputSchema
                }
            })
        
        # Query GPT
        response = await self.openai.chat.completions.create(
            model="gpt-4-turbo-preview",
            tools=openai_tools,
            messages=[{"role": "user", "content": prompt}]
        )
        
        message = response.choices[0].message
        
        # Handle tool calls
        if message.tool_calls:
            tool_results = []
            for tool_call in message.tool_calls:
                # Execute the MCP tool
                result = await self.session.call_tool(
                    tool_call.function.name,
                    json.loads(tool_call.function.arguments)
                )
                
                tool_results.append({
                    "role": "tool",
                    "tool_call_id": tool_call.id,
                    "content": result.content[0].text
                })
            
            # Generate the final answer
            final_response = await self.openai.chat.completions.create(
                model="gpt-4-turbo-preview",
                messages=[
                    {"role": "user", "content": prompt},
                    message,
                    *tool_results
                ]
            )
            
            return final_response.choices[0].message.content
        
        return message.content

Google Gemini client implementation

Gemini can be implemented with the same pattern.

import google.generativeai as genai

class GeminiMCPIntegration:
    def __init__(self, mcp_url: str, api_key: str):
        self.mcp_url = mcp_url
        genai.configure(api_key=api_key)
        self.session = None
        self.model = None
    
    async def connect(self):
        """Connect to the MCP server"""
        transport = await sse_client(self.mcp_url)
        self.session = ClientSession(
            transport.read_stream,
            transport.write_stream
        )
        await self.session.__aenter__()
        await self.session.initialize()
        
        # Get tool definitions and initialize the model
        tools_result = await self.session.list_tools()
        
        functions = []
        for tool in tools_result.tools:
            functions.append({
                "name": tool.name,
                "description": tool.description,
                "parameters": tool.inputSchema
            })
        
        self.model = genai.GenerativeModel(
            model_name="gemini-1.5-pro",
            tools=functions
        )
    
    async def ask_gemini_with_tools(self, prompt: str) -> str:
        """Ask Gemini a question using MCP tools"""
        response = self.model.generate_content(prompt)
        
        # Handle function calls
        for part in response.candidates[0].content.parts:
            if hasattr(part, 'function_call'):
                fc = part.function_call
                
                # Execute the MCP tool
                result = await self.session.call_tool(
                    fc.name,
                    dict(fc.args)
                )
                
                # Return the result to generate the final answer
                function_response = genai.protos.FunctionResponse(
                    name=fc.name,
                    response={"result": result.content[0].text}
                )
                
                final_response = self.model.generate_content(
                    contents=[
                        {"role": "user", "parts": [{"text": prompt}]},
                        {"role": "model", "parts": response.candidates[0].content.parts},
                        {"role": "user", "parts": [{"function_response": function_response}]}
                    ]
                )
                
                return final_response.candidates[0].content.parts[0].text
        
        return response.candidates[0].content.parts[0].text

Chapter 5: Important Implementation Considerations

Error handling and retries

In production, you need to cope with transient network problems and server overload.

import asyncio
from typing import TypeVar, Callable
import random

T = TypeVar('T')

async def retry_with_exponential_backoff(
    func: Callable[[], T],
    max_retries: int = 3,
    base_delay: float = 1.0
) -> T:
    """Retry implementation with exponential backoff"""
    for attempt in range(max_retries):
        try:
            return await func()
        except Exception as e:
            if attempt == max_retries - 1:
                raise
            
            delay = base_delay * (2 ** attempt) + random.uniform(0, 1)
            logger.warning(
                f"Attempt {attempt + 1} failed: {e}. "
                f"Retrying in {delay:.2f} seconds..."
            )
            await asyncio.sleep(delay)

Session management and connection pooling

For long-running services, proper session management is important.

class MCPConnectionPool:
    """Connection pool manager for the MCP server"""
    
    def __init__(self, mcp_url: str, max_connections: int = 10):
        self.mcp_url = mcp_url
        self.max_connections = max_connections
        self.connections = asyncio.Queue(maxsize=max_connections)
        self.lock = asyncio.Lock()
    
    async def acquire(self) -> ClientSession:
        """Acquire a connection"""
        try:
            return self.connections.get_nowait()
        except asyncio.QueueEmpty:
            async with self.lock:
                if self.connections.qsize() < self.max_connections:
                    # Create a new connection
                    transport = await sse_client(self.mcp_url)
                    session = ClientSession(
                        transport.read_stream,
                        transport.write_stream
                    )
                    await session.__aenter__()
                    await session.initialize()
                    return session
                else:
                    # Wait until a connection frees up
                    return await self.connections.get()
    
    async def release(self, session: ClientSession):
        """Release a connection"""
        await self.connections.put(session)

Performance optimization

For large-scale deployments, performance optimization matters.

import functools
from typing import Any, Dict
import hashlib
import pickle

class MCPResponseCache:
    """Cache implementation for MCP responses"""
    
    def __init__(self, ttl_seconds: int = 300):
        self.cache: Dict[str, tuple[Any, float]] = {}
        self.ttl_seconds = ttl_seconds
    
    def _generate_key(self, tool_name: str, arguments: Dict[str, Any]) -> str:
        """Generate a cache key"""
        data = f"{tool_name}:{pickle.dumps(arguments, protocol=pickle.HIGHEST_PROTOCOL)}"
        return hashlib.sha256(data.encode()).hexdigest()
    
    async def get_or_fetch(
        self,
        tool_name: str,
        arguments: Dict[str, Any],
        fetch_func: Callable
    ) -> Any:
        """Get from cache, or fetch if missing"""
        key = self._generate_key(tool_name, arguments)
        
        # Check the cache
        if key in self.cache:
            result, timestamp = self.cache[key]
            if time.time() - timestamp < self.ttl_seconds:
                return result
        
        # Fetch and cache the result
        result = await fetch_func()
        self.cache[key] = (result, time.time())
        return result

Chapter 6: Operations and Monitoring

This chapter is half a bonus, but we include it as a reminder of what not to forget when building an MCP server.

Implementing health checks

In production you need to monitor service health continuously, so at the very least add a health check endpoint

@mcp.tool(
    description="Checks the server's health"
)
async def health_check() -> str:
    """Health check tool"""
    checks = {
        "server_status": "healthy",
        "timezone_check": datetime.now(JST).isoformat(),
        "memory_usage": get_memory_usage(),
        "uptime": get_uptime()
    }
    
    return json.dumps(checks, ensure_ascii=False, indent=2)

def get_memory_usage() -> str:
    """Get memory usage"""
    import psutil
    process = psutil.Process()
    memory_mb = process.memory_info().rss / 1024 / 1024
    return f"{memory_mb:.2f} MB"

def get_uptime() -> str:
    """Get uptime"""
    global server_start_time
    if not server_start_time:
        server_start_time = datetime.now()
    
    uptime = datetime.now() - server_start_time
    return str(uptime)

Logging and metrics

Proper logging is essential for detecting and resolving problems early, so make sure it is in place

import structlog
from prometheus_client import Counter, Histogram, generate_latest

# Structured logging configuration
logger = structlog.get_logger()

# Prometheus metrics
tool_calls_total = Counter(
    'mcp_tool_calls_total',
    'Total number of tool calls',
    ['tool_name']
)
tool_duration_seconds = Histogram(
    'mcp_tool_duration_seconds',
    'Tool execution duration',
    ['tool_name']
)

@mcp.tool()
async def monitored_tool(param: str) -> str:
    """Tool with monitoring built in"""
    start_time = time.time()
    
    try:
        # Increment the tool-call counter
        tool_calls_total.labels(tool_name='monitored_tool').inc()
        
        # The actual work
        result = await process_something(param)
        
        # Log success
        logger.info(
            "tool_executed",
            tool_name="monitored_tool",
            param=param,
            duration=time.time() - start_time
        )
        
        return result
        
    except Exception as e:
        # Log the error
        logger.error(
            "tool_failed",
            tool_name="monitored_tool",
            param=param,
            error=str(e),
            duration=time.time() - start_time
        )
        raise
    
    finally:
        # Record the execution time
        tool_duration_seconds.labels(
            tool_name='monitored_tool'
        ).observe(time.time() - start_time)

Chapter 7: Outlook and Conclusion

The future of MCP

We believe MCP will continue to play a central role in the AI agent ecosystem. Even if some other specification were to appear, something like MCP will remain essential technology.

We have raced through the journey from the stdio era to Streamable HTTP; from here, we expect development along the following lines.

First, tool interoperability will improve further. Sharing and combining tools across AI models from different vendors will become commonplace. Security features should also mature: authentication, authorization, audit logging, and other enterprise capabilities are likely to be standardized.

We can also expect better real-time characteristics. Support for more efficient bidirectional protocols such as WebSockets and WebTransport may be added.

Recommendations for MCP implementers today

For developers implementing MCP servers as of 2025 — including our own engineers — we recommend the following approach

First, make active use of (semi-)official SDKs like FastMCP.

I actually started with a hand-rolled implementation myself and, sure enough, ran into interoperability problems and regretted it...

You can focus on business logic without being whipsawed by spec details. This really matters

Second, adopt the Streamable HTTP transport as the standard for remote MCP servers.

It is the most mature and practical choice.

Third, build with production in mind from the very start. Error handling, logging, and monitoring should be designed in from the beginning, not bolted on later. And if you expose the server on the internet, you will also need authentication and authorization.

Conclusion

Today we walked through how the MCP specification has evolved and how to implement it.

MCP has established itself as an important standard for connecting AI with external systems. The journey from the early stdio-based implementation, through HTTP+SSE, to today's Streamable HTTP was a natural evolution shaped by feedback from real-world use.

We also covered practical ground: server implementation with FastMCP, integration with each LLM provider, and operational considerations.

We touched as well on the significance of MCP servers streaming their responses. This is not just a matter of communication efficiency — it holds the potential for a new execution model, and a new UX, in which the AI model, the MCP server, and the application share the same timeline and work together through incremental exchanges. If we had to compare, it feels like the excitement when Ajax arrived in the early 2000s.

So now that the spec has largely stabilized, isn't this the perfect moment to start building innovative AI applications on MCP?

Thank you for reading!

See you next time!

Read more