Skip to main content

MCP Implementation Guide: Build MCP Servers & Clients

Implementing the Model Context Protocol (MCP) means building software that speaks MCP—servers that expose capabilities to AI agents, and clients that discover and consume those capabilities. It is a protocol-level engineering task that sits at the intersection of distributed systems, API design, and AI integration.

This guide is for developers who have understood the MCP architecture and are now ready to build. It walks through the complete implementation workflow:

  1. Understand the protocol boundary — What belongs in the protocol layer, and what is application logic?
  2. Choose an implementation approach — Use an SDK, integrate with a framework, or implement from scratch?
  3. Build the server or client — Implement lifecycle, capabilities, and request handling.
  4. Test and debug — Ensure interoperability and robust error handling.
  5. Secure and operate — Deploy to production with observability and reliability.

This guide focuses on the how. For conceptual background on MCP architecture, components, and primitives, refer to the MCP Architecture guide. For deep dives on specific primitives, see the Tools, Resources, and Prompts guides.

MCP Implementation Architecture

Understanding where MCP fits in the overall system architecture is the first implementation decision. The protocol layer sits between your AI application logic and the external systems you want to connect.

┌─────────────────────────────────────────────────────────────────────────────┐
│ AI Application / Host │
│ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ Application Logic │ │
│ │ - User interaction │ │
│ │ - Agent reasoning (LLM) │ │
│ │ - Workflow orchestration │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ┌─────────────────────────────────▼───────────────────────────────────┐ │
│ │ MCP Implementation Layer │ │
│ │ │ │
│ │ ┌─────────────────────────────────────────────────────────────┐ │ │
│ │ │ MCP Client │ │ │
│ │ │ - Connection management │ │ │
│ │ │ - Lifecycle (initialize, shutdown) │ │ │
│ │ │ - Capability discovery (tools, resources, prompts) │ │ │
│ │ │ - Request/response handling │ │ │
│ │ │ - Error handling │ │ │
│ │ └─────────────────────────────────────────────────────────────┘ │ │
│ │ │ │ │
│ │ ┌───────────────────────────▼─────────────────────────────────┐ │ │
│ │ │ Transport Layer │ │ │
│ │ │ - stdio (local subprocess) │ │ │
│ │ │ - Streamable HTTP (remote) │ │ │
│ │ └─────────────────────────────────────────────────────────────┘ │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │ │
└────────────────────────────────────┼────────────────────────────────────────┘

│ JSON-RPC over transport

┌─────────────────────────────────────────────────────────────────────────────┐
│ MCP Server Implementation │
│ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ MCP Protocol Layer │ │
│ │ - Lifecycle management │ │
│ │ - Capability declaration (tools, resources, prompts) │ │
│ │ - Request routing (tools/call, resources/read, prompts/get) │ │
│ │ - Error handling │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ┌─────────────────────────────────▼───────────────────────────────────┐ │
│ │ Business Logic │ │
│ │ - Tool execution (filesystem, database, API calls) │ │
│ │ - Resource resolution (file content, query results) │ │
│ │ - Prompt generation (template rendering) │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │ │
└────────────────────────────────────┼────────────────────────────────────────┘


┌─────────────┐
│ External │
│ Systems │
└─────────────┘

Figure 1: Implementation architecture showing the separation between application logic, MCP protocol layer, and external systems.

Where Application Logic Belongs

  • Protocol layer: Connection management, message serialization, lifecycle handling, capability discovery
  • Application layer: Business-specific tool implementations, data access, prompt content, resource resolution

The protocol layer should not contain business logic. The business logic should not contain protocol-specific code. This separation makes testing easier, enables independent evolution, and allows you to swap protocol implementations without rewriting business functionality.

Choosing an MCP Implementation Approach

Developers have three main paths to MCP implementation:

Option 1: Use an Official MCP SDK

Official SDKs provide the fastest path to a working implementation with the least protocol knowledge required. They handle lifecycle management, JSON-RPC serialization, transport configuration, and error handling, letting you focus on capability implementation.

  • TypeScript/JavaScript: @modelcontextprotocol/sdk
  • Python: mcp (official SDK)
  • Java: Available through the MCP community
  • .NET: Available through the MCP community
  • Go, Rust, Kotlin: Community SDKs

Option 2: Use a Framework Integration

Many AI agent frameworks provide MCP integration abstractions:

  • LangGraph: Can use MCP clients as tool providers
  • CrewAI: Dedicated crewai-mcp plugin
  • OpenAI Agents SDK: MCP integration via Python MCP SDK

This approach is ideal if you are already using one of these frameworks and want to add MCP capabilities with minimal additional protocol knowledge.

Option 3: Implement MCP from Scratch

Implementing MCP directly gives you maximum control and is appropriate when:

  • The language or platform you use lacks SDK support
  • You need to handle the protocol at a very low level
  • You have strict size or dependency constraints
  • You are implementing MCP in an embedded environment

SDK Comparison Table

AspectOfficial SDKFramework IntegrationFrom Scratch
Implementation speedFastestFastSlow
Protocol knowledge requiredLowLowHigh
Control over behaviorHighModerateComplete
Debugging complexityLowModerateHigh
Production suitabilityHighHighDepends on implementation quality
Maintenance burdenLowModerateHigh
Learning investmentLowModerateHigh

Recommendation

Start with an official SDK. The official SDKs are maintained alongside the specification and handle the most error-prone parts of the protocol. Use framework integrations if you are already invested in a framework. Implement from scratch only if you have specific requirements that SDKs cannot meet.

Building Your First MCP Server

This section walks through the core steps of implementing an MCP server. The examples use conceptual Python and TypeScript patterns; actual syntax depends on your chosen SDK.

1. Project Setup

Initialize a new project with the MCP SDK dependency.

Python (using mcp SDK) :

# Install the SDK
pip install mcp

TypeScript/JavaScript (using @modelcontextprotocol/sdk) :

npm install @modelcontextprotocol/sdk

2. Server Initialization

Create the server instance and declare its basic information.

# Conceptual Python
from mcp.server import Server

server = Server(
name="example-server",
version="1.0.0"
)
// Conceptual TypeScript
import { Server } from "@modelcontextprotocol/sdk/server";

const server = new Server(
{
name: "example-server",
version: "1.0.0"
},
{
capabilities: {
tools: {}
}
}
);

3. Capability Declaration

Declare what capabilities the server supports during initialization. This tells the client what to expect.

# Conceptual capability declaration
server = Server(
name="example-server",
version="1.0.0",
capabilities={
"tools": {"listChanged": True},
"resources": {"subscribe": True, "listChanged": True},
"prompts": {"listChanged": True}
}
)

4. Tool Registration

Register a tool with its name, description, and input schema.

# Conceptual tool registration
@server.tool()
def read_file(path: str) -> str:
"""
Read the contents of a file.

Args:
path: The path to the file to read.
"""
with open(path, "r") as f:
return f.read()

5. Request Handling

The SDK handles request routing, but if implementing from scratch, route requests based on the method field:

{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/list"
}

To:

{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "read_file",
"arguments": {"path": "/path/to/file"}
}
}

6. Transport Setup

Configure the transport based on deployment model.

Local (stdio) :

# Conceptual stdio transport
async with stdio_server() as streams:
await server.run(
streams.read_stream,
streams.write_stream,
server.create_initialization_options()
)

Remote (Streamable HTTP) :

# Conceptual HTTP transport
async with sse_server("localhost", 8000) as server_app:
await run_streamable_http_server(
server_app,
host="0.0.0.0",
port=8000
)

7. Shutdown

Handle graceful shutdown by cleaning up resources and closing connections.

# Conceptual shutdown
def shutdown():
# Close database connections
# Release file handles
# Close transport
pass

Complete Minimal Server Example

# Conceptual minimal MCP server
from mcp.server import Server
from mcp.server.stdio import stdio_server

server = Server(
name="example-server",
version="1.0.0",
capabilities={"tools": {}}
)

@server.tool()
def hello(name: str = "World") -> str:
"""Say hello to someone."""
return f"Hello, {name}!"

async def main():
async with stdio_server() as streams:
await server.run(
streams.read_stream,
streams.write_stream,
server.create_initialization_options()
)

if __name__ == "__main__":
import asyncio
asyncio.run(main())

Implementing MCP Tools

Tools are the primary way MCP servers expose executable capabilities to agents. Tools are discovered via tools/list and invoked via tools/call.

Tool Definition Steps

1. Choose a tool name: The name must be unique within the server and descriptive of the action.

2. Write a description: A clear, concise description of what the tool does. The description helps LLMs decide when to use the tool.

3. Define the input schema: Use JSON Schema to declare the arguments the tool accepts.

{
"type": "object",
"properties": {
"path": {
"type": "string",
"description": "The path to the file to read"
}
},
"required": ["path"]
}

4. Implement the execution logic: The actual code that performs the operation.

5. Format the result: Return the result in a structured format.

Example: File Read Tool

# Conceptual tool implementation
from mcp.types import Tool
import json

# 1. Define the tool
file_tool = Tool(
name="read_file",
description="Read the contents of a file from the filesystem",
inputSchema={
"type": "object",
"properties": {
"path": {
"type": "string",
"description": "The path to the file to read"
}
},
"required": ["path"]
}
)

# 2. Implement the handler
async def handle_read_file(path: str) -> dict:
try:
with open(path, "r") as f:
content = f.read()
return {
"content": content,
"size": len(content)
}
except FileNotFoundError:
raise Exception(f"File not found: {path}")
except PermissionError:
raise Exception(f"Permission denied: {path}")

Input Validation

Always validate tool inputs before execution:

  • Type checking: Ensure arguments match the expected types
  • Required fields: Verify all required arguments are present
  • Path validation: Prevent path traversal attacks
  • Size limits: Enforce maximum input sizes
  • Format validation: Validate formats (e.g., email, URL)

Safe Input Handling

# Conceptual input validation
def validate_file_path(path: str) -> bool:
# Prevent path traversal
if ".." in path:
return False
# Check path is within allowed directory
if not path.startswith(allowed_base_path):
return False
# Check file size
if os.path.getsize(path) > MAX_FILE_SIZE:
return False
return True

Implementing MCP Resources

Resources provide contextual data to the model. They are application-controlled and side-effect-free.

Resource Implementation Steps

1. Define the resource: Choose a URI and name.

2. Implement reading: Return the resource content when requested.

3. (Optional) Implement templates: For dynamic resources with variable URIs.

4. (Optional) Implement subscriptions: For resources that can change.

Example: Static Resource

# Conceptual resource implementation
# A static resource with a fixed URI
resource = {
"uri": "docs://readme",
"name": "README",
"description": "Project readme file",
"mimeType": "text/markdown"
}

async def handle_read_resource(uri: str) -> str:
if uri == "docs://readme":
with open("README.md", "r") as f:
return f.read()
raise Exception(f"Resource not found: {uri}")

Example: Resource Template

For resources where the URI varies (e.g., one per customer, one per file):

# Conceptual resource template
resource_template = {
"uriTemplate": "file:///{path}",
"name": "File resource",
"description": "A file from the filesystem",
"mimeType": "text/plain"
}

async def handle_read_resource(uri: str) -> str:
# Extract path from URI (e.g., file:///etc/hosts → /etc/hosts)
path = uri.replace("file://", "")
with open(path, "r") as f:
return f.read()

Resources vs. Tools

AspectResourcesTools
PurposeProvide contextPerform actions
ControlApplication-controlledModel-controlled
Side effectsNone (read-only)May have side effects
ExecutionClient fetchesModel invokes
Methodresources/readtools/call

Implementing MCP Prompts

Prompts provide reusable templates for guiding LLM interactions. They are user-controlled and return message sequences.

Prompt Implementation Steps

1. Define the prompt: Choose a name and description.

2. Define arguments: Declare required and optional arguments.

3. Implement message generation: Return the messages for the prompt.

Example: Code Review Prompt

# Conceptual prompt implementation
prompt = {
"name": "code_review",
"description": "Review code for best practices and issues",
"arguments": [
{"name": "code", "description": "The code to review", "required": True},
{"name": "language", "description": "Programming language", "required": False}
]
}

async def handle_get_prompt(name: str, arguments: dict) -> list:
if name == "code_review":
code = arguments["code"]
language = arguments.get("language", "Unknown")
return [
{
"role": "system",
"content": {"type": "text", "text": "You are an expert code reviewer."}
},
{
"role": "user",
"content": {"type": "text", "text": f"Review this {language} code:\n\n{code}"}
}
]
raise Exception(f"Prompt not found: {name}")

Prompt Argument Validation

Validate prompt arguments before generating messages:

def validate_prompt_arguments(prompt_name: str, arguments: dict) -> bool:
# Check required arguments
required = get_required_args(prompt_name)
for arg in required:
if arg not in arguments:
return False
return True

Building an MCP Client

The client is responsible for discovering and invoking MCP capabilities. Here is the step-by-step implementation flow:

1. Client Initialization

# Conceptual client initialization
from mcp.client import Client
from mcp.transport import stdio_transport

# Create client
client = Client()

# Configure transport
transport = stdio_transport(
command="python",
args=["-m", "my_mcp_server"]
)

2. Connection and Initialization

# Conceptual connection
async def connect():
# Establish transport
await transport.connect()

# Send initialize request
response = await client.initialize({
"protocolVersion": "2026-07-28",
"capabilities": {},
"clientInfo": {"name": "example-client", "version": "1.0.0"}
})

# Store server capabilities
server_capabilities = response["capabilities"]

# Send initialized notification
await client.send_initialized()

3. Tool Discovery

# Conceptual tool discovery
async def discover_tools():
response = await client.request("tools/list", {})
tools = response["tools"]
return tools

4. Resource Discovery

# Conceptual resource discovery
async def discover_resources():
response = await client.request("resources/list", {})
resources = response["resources"]
return resources

5. Prompt Discovery

# Conceptual prompt discovery
async def discover_prompts():
response = await client.request("prompts/list", {})
prompts = response["prompts"]
return prompts

6. Tool Invocation

# Conceptual tool invocation
async def call_tool(name: str, arguments: dict):
response = await client.request("tools/call", {
"name": name,
"arguments": arguments
})
return response["result"]

7. Resource Reading

# Conceptual resource reading
async def read_resource(uri: str):
response = await client.request("resources/read", {
"uri": uri
})
return response["content"]

8. Prompt Retrieval

# Conceptual prompt retrieval
async def get_prompt(name: str, arguments: dict):
response = await client.request("prompts/get", {
"name": name,
"arguments": arguments
})
return response["messages"]

9. Error Handling

# Conceptual error handling
try:
result = await call_tool("read_file", {"path": "/nonexistent"})
except ToolExecutionError as e:
# Handle tool execution failure
print(f"Tool failed: {e}")
except ConnectionError as e:
# Handle connection failure
print(f"Connection lost: {e}")
except TimeoutError as e:
# Handle timeout
print(f"Request timed out: {e}")

10. Shutdown

# Conceptual shutdown
async def shutdown():
# Close client
await client.close()
# Close transport
await transport.close()

MCP Lifecycle Implementation

The MCP lifecycle consists of four phases that must be implemented correctly for interoperability.

1. Initialization Phase

The client initiates by sending an initialize request with its protocol version and capabilities. The server responds with its own capabilities. The client must then send an initialized notification.

Client:

{
"jsonrpc": "2.0",
"id": 1,
"method": "initialize",
"params": {
"protocolVersion": "2026-07-28",
"capabilities": {"sampling": {}},
"clientInfo": {"name": "example-client", "version": "1.0.0"}
}
}

Server Response:

{
"jsonrpc": "2.0",
"id": 1,
"result": {
"protocolVersion": "2026-07-28",
"capabilities": {"tools": {}},
"serverInfo": {"name": "example-server", "version": "1.0.0"}
}
}

Client Notification:

{
"jsonrpc": "2.0",
"method": "notifications/initialized"
}

2. Capability Negotiation

Capabilities are exchanged during initialization. Both parties must respect declared capabilities throughout the session.

3. Normal Operation

During normal operation, clients can send any supported requests (tools/list, tools/call, resources/list, etc.).

4. Shutdown

Graceful termination should clean up resources and close the connection.

Sequence Diagram

┌──────────┐ ┌──────────┐
│ Client │ │ Server │
└────┬─────┘ └────┬─────┘
│ │
│ 1. initialize (version + capabilities) │
│────────────────────────────────────────▶│
│ │
│ 2. initialize response │
│ (version + server capabilities) │
│◀────────────────────────────────────────│
│ │
│ 3. initialized notification │
│────────────────────────────────────────▶│
│ │
│ ──── Normal Operation ──── │
│ │
│ 4. tools/list │
│────────────────────────────────────────▶│
│ │
│ 5. tools/list response │
│◀────────────────────────────────────────│
│ │
│ 6. tools/call │
│────────────────────────────────────────▶│
│ │
│ 7. tools/call response │
│◀────────────────────────────────────────│
│ │
│ 8. (Optional) shutdown │
│────────────────────────────────────────▶│
│ │

Figure 2: MCP lifecycle sequence diagram showing initialization, capability negotiation, normal operation, and shutdown.

MCP JSON-RPC Implementation

MCP uses JSON-RPC 2.0 as its message format. Understanding the JSON-RPC semantics is essential for correct implementation.

Request

{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/list",
"params": {}
}
  • jsonrpc: Must be "2.0"
  • id: A string or integer; must not be null
  • method: The MCP method name (e.g., tools/list, tools/call, resources/read)
  • params: Optional; the method parameters

Success Response

{
"jsonrpc": "2.0",
"id": 1,
"result": {
"resultType": "complete",
"tools": [...]
}
}
  • resultType: Must be "complete" for success, "input_required" for operations needing user input

Error Response

{
"jsonrpc": "2.0",
"id": 1,
"error": {
"code": -32000,
"message": "Tool execution failed"
}
}

Standard error codes:

CodeNameDescription
-32700Parse errorInvalid JSON was received
-32600Invalid RequestThe JSON sent is not a valid Request object
-32601Method not foundThe method does not exist
-32602Invalid paramsInvalid method parameters
-32603Internal errorInternal JSON-RPC error
-32000..-32099Server errorReserved for implementation-defined server errors

Notification

{
"jsonrpc": "2.0",
"method": "notifications/initialized"
}

Notifications do not include an id and are not responded to.

Request-Response Correlation

Requests and responses are correlated by the id field. This allows multiple requests to be in flight simultaneously.

Mapping MCP Methods to JSON-RPC

MCP OperationJSON-RPC Method
Initializeinitialize
List toolstools/list
Call tooltools/call
List resourcesresources/list
Read resourceresources/read
List promptsprompts/list
Get promptprompts/get

Transport Implementation

The transport layer handles message delivery between client and server. MCP supports two primary transports.

stdio Transport

The stdio transport is used for local subprocess communication.

How it works:

  • The client launches the server as a subprocess
  • The server reads JSON-RPC messages from stdin
  • The server writes JSON-RPC messages to stdout
  • The server writes logs to stderr

Implementation considerations:

  • Each JSON-RPC message must be a single line (no embedded newlines)
  • Log messages go to stderr, not stdout
  • Process lifecycle is managed by the client

Security: No network surface, credentials from environment.

Streamable HTTP Transport

The Streamable HTTP transport is used for remote network communication.

How it works:

  • The server provides a single HTTP endpoint
  • Clients send POST requests with JSON-RPC messages
  • The server responds with HTTP responses
  • Optional Server-Sent Events for streaming

Implementation considerations:

  • The server must validate the Origin header
  • The server must implement OAuth 2.1 authentication
  • The server should support CORS for web clients

Security: OAuth 2.1, TLS required.

Transport Comparison

AspectstdioStreamable HTTP
DeploymentLocal subprocessRemote service
ClientsTypically one clientMany clients
AuthenticationEnvironment credentialsOAuth 2.1
NetworkNo network surfaceHTTP/HTTPS
StreamingNoYes (SSE)
ScalabilityProcess-levelService-level

Connecting an AI Agent to MCP

The final step is connecting your AI agent to an MCP server so it can discover and invoke capabilities.

Complete Architecture

┌─────────────────────────────────────────────────────────────────────────────┐
│ User │
└─────────────────────────────────────────────────────────────────────────────┘


┌─────────────────────────────────────────────────────────────────────────────┐
│ AI Agent │
│ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ User Interface │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ┌─────────────────────────────────▼───────────────────────────────────┐ │
│ │ LLM / Reasoning │ │
│ │ - Understands user intent │ │
│ │ - Selects appropriate tools │ │
│ │ - Generates structured responses │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ┌─────────────────────────────────▼───────────────────────────────────┐ │
│ │ Orchestration │ │
│ │ - Decomposes complex tasks │ │
│ │ - Manages workflow execution │ │
│ │ - Handles tool call results │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ┌─────────────────────────────────▼───────────────────────────────────┐ │
│ │ MCP Client │ │
│ │ - Connects to MCP server │ │
│ │ - Discovers available tools │ │
│ │ - Invokes tools │ │
│ │ - Handles responses and errors │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │ │
└────────────────────────────────────┼────────────────────────────────────────┘


┌─────────────┐
│ MCP Server │
│ │
│ read_file │
│ query_db │
│ create_issue│
└─────────────┘

Figure 3: AI agent connecting to MCP—separation between reasoning, orchestration, and protocol communication.

Agent Integration Flow

  1. Agent receives user request
  2. Agent reasons about the task — Determines what tools or resources are needed
  3. Agent queries the MCP client — For available tools and their descriptions
  4. Agent selects a tool — Based on the task and tool descriptions
  5. Agent invokes the tool via the MCP client
  6. Agent processes the result — And formats the response for the user

Tool Selection in the Agent

The LLM is typically responsible for tool selection:

Prompt to LLM:
"You have access to these tools: [tool descriptions].
Your task is to: [user request].
Choose the appropriate tool and provide arguments."

LLM response:
{
"tool": "read_file",
"arguments": {"path": "/data/report.pdf"}
}

Practical Integration Code

# Conceptual agent integration
class MCPAgent:
def __init__(self, mcp_client):
self.client = mcp_client
self.tools = []

async def initialize(self):
# Discover available tools
response = await self.client.request("tools/list", {})
self.tools = response.get("tools", [])

async def process_request(self, user_request: str):
# 1. Get LLM to select a tool
selected_tool = await self.select_tool(user_request, self.tools)

# 2. Invoke the tool
result = await self.client.request("tools/call", {
"name": selected_tool["name"],
"arguments": selected_tool["arguments"]
})

# 3. Process and return result
return self.format_response(result)

MCP Implementation Example

Let's build a complete end-to-end example: a customer support agent that queries customer information and creates support tickets.

Server Implementation

# Conceptual server: customer_support_server.py
from mcp.server import Server
from mcp.types import Tool
import json

server = Server(
name="customer-support",
version="1.0.0",
capabilities={"tools": {}}
)

# Tool 1: Get customer information
@server.tool()
def get_customer(customer_id: str) -> dict:
"""
Get customer information by ID.

Args:
customer_id: The customer's unique identifier.
"""
# Simulated database lookup
customers = {
"123": {"name": "Alice", "email": "[email protected]", "tier": "premium"},
"456": {"name": "Bob", "email": "[email protected]", "tier": "standard"}
}
customer = customers.get(customer_id)
if not customer:
raise Exception(f"Customer not found: {customer_id}")
return customer

# Tool 2: Search orders
@server.tool()
def search_orders(customer_id: str, limit: int = 10) -> list:
"""
Search orders for a customer.

Args:
customer_id: The customer's unique identifier.
limit: Maximum number of orders to return.
"""
# Simulated order search
orders = [
{"id": "001", "date": "2026-01-15", "amount": 149.99},
{"id": "002", "date": "2026-02-20", "amount": 89.50}
]
return orders[:limit]

# Tool 3: Create support ticket
@server.tool()
def create_ticket(customer_id: str, subject: str, description: str, priority: str = "normal") -> dict:
"""
Create a support ticket for a customer.

Args:
customer_id: The customer's unique identifier.
subject: The ticket subject.
description: Detailed ticket description.
priority: Priority level (low, normal, high, urgent).
"""
# Simulated ticket creation
ticket = {
"id": "TICKET-" + str(hash(customer_id + subject))[:8],
"customer_id": customer_id,
"subject": subject,
"priority": priority,
"status": "open",
"created_at": "2026-08-28T10:00:00Z"
}
return ticket

if __name__ == "__main__":
# Run server with stdio transport
import asyncio
from mcp.server.stdio import stdio_server

async def main():
async with stdio_server() as streams:
await server.run(
streams.read_stream,
streams.write_stream,
server.create_initialization_options()
)

asyncio.run(main())

Client Discovery and Invocation

# Conceptual client: customer_support_client.py
from mcp.client import Client
import json

async def main():
client = Client()

# Connect to server (stdio)
from mcp.transport import stdio_transport
transport = stdio_transport(
command="python",
args=["customer_support_server.py"]
)
await transport.connect()

# Initialize
await client.initialize({
"protocolVersion": "2026-07-28",
"capabilities": {},
"clientInfo": {"name": "support-client", "version": "1.0.0"}
})

# Discover tools
tools_response = await client.request("tools/list", {})
tools = tools_response["tools"]
print("Available tools:", [t["name"] for t in tools])

# 1. Get customer information
customer = await client.request("tools/call", {
"name": "get_customer",
"arguments": {"customer_id": "123"}
})
print("Customer:", customer["result"])

# 2. Search orders
orders = await client.request("tools/call", {
"name": "search_orders",
"arguments": {"customer_id": "123", "limit": 5}
})
print("Orders:", orders["result"])

# 3. Create ticket
ticket = await client.request("tools/call", {
"name": "create_ticket",
"arguments": {
"customer_id": "123",
"subject": "Login issue",
"description": "User cannot log in after password reset",
"priority": "high"
}
})
print("Ticket created:", ticket["result"])

# Shutdown
await client.close()
await transport.close()

if __name__ == "__main__":
import asyncio
asyncio.run(main())

Error Handling

Error Categories

CategoryExamplesHandling Strategy
Protocol errorsInvalid JSON, unsupported versionReturn JSON-RPC error responses
Validation errorsInvalid arguments, missing required fieldsReturn descriptive error messages
Authorization errorsMissing authentication, insufficient permissionsReturn 401/403 with appropriate messages
Application errorsTool execution failure, dependency unavailableReturn error with details, retry if transient
Network errorsConnection timeout, transport failureRetry with backoff, circuit breaker

Protocol vs Application Errors

Protocol errors should be returned as JSON-RPC errors:

{
"jsonrpc": "2.0",
"id": 1,
"error": {
"code": -32602,
"message": "Invalid params: missing required argument 'path'"
}
}

Application errors (tool execution failures) should be returned as error responses:

{
"jsonrpc": "2.0",
"id": 1,
"result": {
"resultType": "complete",
"error": {
"message": "File not found: /path/to/file",
"type": "FileNotFoundError"
}
}
}

Client-Side Error Handling

# Conceptual client-side error handling
async def safe_call_tool(name: str, arguments: dict):
try:
return await client.request("tools/call", {
"name": name,
"arguments": arguments
})
except ConnectionError as e:
# Retry with backoff
return await retry_with_backoff(name, arguments)
except TimeoutError as e:
# Return timeout message
return {"error": "Operation timed out"}
except InvalidArgumentError as e:
# Return validation error
return {"error": str(e)}
except Exception as e:
# Log and return generic error
logger.error(f"Unexpected error: {e}")
return {"error": "An unexpected error occurred"}

Validation and Schema Design

JSON Schema for Tools

Tool input schemas use JSON Schema for validation:

{
"type": "object",
"properties": {
"path": {
"type": "string",
"description": "The file path",
"pattern": "^[a-zA-Z0-9/._-]+$"
},
"max_size": {
"type": "integer",
"minimum": 1,
"maximum": 10485760,
"default": 1048576
},
"encoding": {
"type": "string",
"enum": ["utf-8", "ascii", "latin-1"]
}
},
"required": ["path"]
}

Validation Best Practices

  1. Use JSON Schema for structure validation — Type, required fields, patterns
  2. Implement additional validation in code — Business rules, resource limits
  3. Validate at the protocol boundary — Before executing the tool
  4. Return descriptive errors — Help clients fix their requests
  5. Reject invalid inputs — Do not silently correct them

Strong Schemas Improve Interoperability

  • Clients know exactly what to send
  • Servers can trust the structure of incoming requests
  • LLMs can generate correctly structured tool calls
  • Testing can validate against the schema

Testing an MCP Implementation

Testing Strategy

Test TypePurposeWhen
Unit testsTest individual componentsEvery commit
Protocol testsVerify JSON-RPC message handlingEvery commit
Integration testsTest server-client interactionEvery merge
Contract testsVerify client-server compatibilityEvery release
Negative testsTest error handlingEvery release
Security testsTest authentication, injectionWeekly
Load testsTest performance under loadMonthly

Example Test Matrix

Test CaseExpected Behavior
initialize with valid versionSuccess response
initialize with unsupported versionError response
tools/list after initializationList of tools
tools/list before initializationError response
tools/call with valid argumentsTool result
tools/call with missing required argumentError response
tools/call with invalid argument typeError response
tools/call with non-existent toolError response
resources/read with valid URIResource content
resources/read with invalid URIError response
prompts/get with valid namePrompt messages

Protocol Compliance Testing

The MCP specification includes a test suite. Run your implementation against it:

# Conceptual test command
mcp-test --server ./my_server.py

Interoperability Testing

Test your server against multiple clients:

  1. Connect a standard MCP client (e.g., Claude Desktop)
  2. Connect a test client
  3. Verify all capabilities work as expected

Debugging MCP Applications

Debugging Workflow

┌─────────────────────────────────────────────────────────────────────────────┐
│ MCP Debugging Workflow │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ Issue detected → Check logs → Isolate layer → Verify capability │
│ │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ Layer Isolation │ │
│ │ - Transport (stdio/HTTP) │ │
│ │ - Protocol (JSON-RPC format) │ │
│ │ - Capability (tool/resource/prompt) │ │
│ │ - Business logic │ │
│ └──────────────────────────────────────────────────────────┘ │
│ │
│ Found issue → Fix → Test → Verify │
│ │
└─────────────────────────────────────────────────────────────────────────────┘

Common Debugging Techniques

Enable verbose logging:

# Conceptual verbose logging
import logging
logging.basicConfig(level=logging.DEBUG)

Inspect JSON-RPC messages:

Log all sent and received messages to verify format.

Use a test client:

Write a simple test client that sends known requests and validates responses.

Check capability negotiation:

Ensure the client and server agree on capabilities. Mismatches cause missing features.

Debugging Tools

  • mcp-inspector: A tool for inspecting MCP protocol messages
  • mcp-cli: Command-line client for testing MCP servers
  • mcp-test: Protocol compliance test runner

Security Considerations

Implementation-Level Security Controls

ControlImplementation
AuthenticationOAuth 2.1 for HTTP, environment credentials for stdio
AuthorizationScoped access, tool-level permissions
Input validationJSON Schema validation, additional code validation
Tool allowlistsValidate tool names against allowlist
Secrets managementEnvironment variables, secret managers
Audit loggingLog all tool calls, authorization decisions
Rate limitingGateway-level or server-level

Secure Coding Practices

  1. Never trust client input — Validate everything at the protocol boundary
  2. Use least privilege — Only expose necessary capabilities
  3. Prevent path traversal — Validate file paths
  4. Sanitize outputs — Prevent data leakage
  5. Log security events — Authentication failures, authorization decisions, tool calls

Production Implementation

Turning a Prototype into Production

AspectImplementation
ContainerizationDocker, OCI image with SBOM
Health checks/health or /ready endpoint
ConfigurationEnvironment variables, ConfigMap
SecretsVault, Secret Manager
ObservabilityLogs, metrics, traces
ScalingHorizontal scaling, autoscaling
DeploymentCI/CD pipeline, rolling/canary updates
RollbackQuick rollback capability
VersioningSemantic versioning, backward compatibility

Production Configuration

# Conceptual production configuration
mcp_server:
name: "production-server"
version: "1.0.0"
transport: "http"
host: "0.0.0.0"
port: 8000
auth:
oauth:
issuer: "https://auth.example.com"
audience: "https://mcp.example.com"
logging:
level: "INFO"
format: "json"
limits:
max_file_size: 10485760
max_requests_per_minute: 1000
max_tool_timeout_seconds: 30

Common Implementation Mistakes

MistakeWhy It's ProblematicHow to Avoid
Implementing protocol behavior manually when an SDK is availableProne to subtle bugs, wasted timeUse official SDK
Mixing business logic and protocol logicHard to test, hard to evolveSeparate layers
Weak input schemasSecurity vulnerabilities, poor interoperabilityDefine strong schemas
Missing error handlingPoor user experience, undetected failuresImplement comprehensive error handling
Exposing too many toolsIncreased attack surfaceMinimize tool surface
Trusting unvalidated inputSecurity vulnerabilitiesValidate all inputs
Ignoring capability negotiationMissing features, breakage with clientsImplement capability negotiation correctly
Assuming local and remote deployments behave identicallySecurity issues, transport differencesTest both models
Not testing client/server interoperabilityBreakage when used with real clientsRun compliance tests
Logging secrets or sensitive payloadsData exposureRedact sensitive data

MCP Implementation Best Practices

Architecture

  • Separate protocol logic from business logic
  • Use dependency injection for testability
  • Keep server capabilities focused and limited

Protocol

  • Implement full lifecycle management
  • Declare capabilities accurately
  • Handle all MCP methods your server supports
  • Respond with correct JSON-RPC structures

Tools

  • Use clear, descriptive names
  • Provide detailed descriptions
  • Define strong input schemas
  • Validate all inputs
  • Handle errors gracefully
  • Return structured results

Resources

  • Use meaningful URIs
  • Provide clear names and descriptions
  • Implement resource templates for dynamic resources
  • Handle URI validation

Prompts

  • Use clear, descriptive names
  • Document required arguments
  • Validate arguments
  • Return structured message sequences

Validation

  • Validate at the protocol boundary
  • Use JSON Schema for structure
  • Add code validation for business rules
  • Return descriptive error messages

Errors

  • Distinguish protocol errors from application errors
  • Return appropriate JSON-RPC error codes
  • Provide actionable error messages
  • Log errors for debugging

Security

  • Implement authentication
  • Enforce authorization
  • Validate all inputs
  • Use least privilege
  • Log security events

Testing

  • Unit tests for business logic
  • Protocol compliance tests
  • Integration tests with real clients
  • Negative tests for error handling
  • Security tests

Observability

  • Structured logging
  • Request metrics (latency, errors)
  • Distributed tracing
  • Health checks

Deployment

  • Containerization
  • CI/CD pipeline
  • Zero-downtime deployment
  • Rollback capability
  • Versioning

Troubleshooting Guide

ProblemLikely CauseHow to DiagnoseRecommended Fix
Server not discoveredTransport issue, wrong command/pathCheck transport configurationVerify command path, test with stdio
Initialization failureVersion mismatch, missing capabilitiesInspect initialize request/responseUse compatible protocol version
Tool missingTool registration failed, capability mismatchCheck tools/list responseVerify tool registration, check capabilities
Tool arguments rejectedInvalid schema, missing required argumentsInspect tool call requestValidate against schema, provide required args
TimeoutLong-running operationCheck operation durationOptimize, increase timeout
Authentication failureMissing/invalid tokenCheck Authorization headerImplement proper token validation
Authorization failureInsufficient scopeCheck token scopesRequest correct scopes
Unexpected responseProtocol bug, version mismatchInspect response formatUse correct protocol version
Connection droppedNetwork issue, server crashCheck logs, network connectivityImplement reconnect, health checks
Deployment regressionBreaking change, configuration issueCompare with working versionRollback, test in staging

Frequently Asked Questions

1. How do I implement MCP?

Implement MCP by choosing an SDK (TypeScript, Python), building a server or client, implementing capabilities (tools, resources, prompts), and handling lifecycle. See the section on building your first server.

2. How do I build an MCP server?

Initialize the server, declare capabilities, register tools/resources/prompts, configure a transport (stdio or HTTP), and implement request handlers. Use an official MCP SDK for the fastest path.

3. How do I build an MCP client?

Create a client, connect via a transport, send an initialize request, handle capability negotiation, discover capabilities (tools/list, resources/list, prompts/list), and invoke operations.

4. Should I use an MCP SDK?

Yes, for most use cases. Official SDKs handle lifecycle, JSON-RPC, and transport—letting you focus on capabilities.

5. What language is best for MCP development?

TypeScript and Python have the most mature official SDKs. Choose the language your team is most comfortable with.

6. How does MCP JSON-RPC work?

MCP uses JSON-RPC 2.0 messages sent over the transport. Requests have an ID, method, and params. Responses have a result or error.

7. How do MCP tools get registered?

Tools are registered by adding them to the server's capability declaration. The SDK handles routing from the protocol to your implementation.

8. How does an MCP client discover tools?

The client calls tools/list after initialization. The server returns a list of available tools with names and input schemas.

9. How do I implement MCP Resources?

Resources are implemented by defining URIs and providing a resources/read handler that returns content. Use resource templates for dynamic URIs.

10. How do I implement MCP Prompts?

Prompts are implemented by providing a prompts/get handler that returns messages for the prompt with arguments.

11. Can I implement MCP without an SDK?

Yes, but you must handle JSON-RPC, lifecycle, transport, and all protocol details yourself. This is significantly more work.

12. How do I test an MCP server?

Use unit tests, protocol compliance tests, integration tests with real clients, and negative tests for error handling.

13. How do I debug an MCP client?

Enable verbose logging, inspect JSON-RPC messages, use a test server, and check capability negotiation.

14. How do I secure an MCP implementation?

Use OAuth 2.1 for remote HTTP deployments, validate all inputs, enforce least privilege, log security events, and use TLS.

15. How do I deploy an MCP server to production?

Containerize the server, set up health checks, configure authentication and observability, and use CI/CD with zero-downtime deployment.

16. What's the difference between local and remote MCP deployment?

Local uses stdio transport; remote uses Streamable HTTP with OAuth 2.1 and TLS.

17. How do I handle MCP server versioning?

Use semantic versioning, maintain backward compatibility, and support multiple versions during transitions.

18. How do I handle tool execution errors?

Return error responses in the tools/call result with descriptive messages and appropriate error codes.

19. How do I implement streaming responses?

Use the Streamable HTTP transport with Server-Sent Events for streaming responses.

20. What is the most common MCP implementation mistake?

Not using an SDK when one is available, leading to protocol bugs and unnecessary complexity.

Implementation Checklist

Before Starting

  • Understand what capabilities your server/client needs to expose
  • Choose an implementation language and SDK
  • Set up development environment

Implementation

  • Initialize the server/client
  • Implement lifecycle (initialize, initialized, shutdown)
  • Declare capabilities (tools, resources, prompts)
  • Implement tools (name, description, schema, handler)
  • Implement resources (URI, read handler) if needed
  • Implement prompts (name, args, message generation) if needed
  • Implement error handling
  • Implement input validation

Testing

  • Unit tests for business logic
  • Protocol compliance tests
  • Integration tests with clients
  • Negative tests for errors
  • Security tests

Security

  • Authentication implemented
  • Authorization enforced
  • Input validation applied
  • Least privilege
  • Secrets not hard-coded

Deployment

  • Containerized
  • Health checks
  • Observability (logs, metrics, traces)
  • CI/CD pipeline
  • Rollback capability
  • Versioning

Documentation

  • README with setup instructions
  • API documentation for tools/resources/prompts
  • Deployment instructions
  • Troubleshooting guide

Conclusion

Implementing MCP is a journey from understanding the protocol to shipping a production-ready integration. The key steps are:

Understand the protocol

Choose an implementation approach (SDK recommended)

Build the server or client

Add capabilities (tools, resources, prompts)

Test thoroughly

Secure the implementation

Deploy to production

Monitor and operate

A high-quality MCP implementation is not just about making messages work. It requires:

  • Clear protocol separation — Keeping protocol and business logic separate
  • Strong validation — Ensuring inputs are safe and correctly formed
  • Security — Authentication, authorization, and least privilege
  • Interoperability — Working correctly with any MCP client or server
  • Observability — Knowing what the system is doing
  • Production operations — Deployment, scaling, and incident response

Start with an official SDK. The SDKs handle the most error-prone parts of the protocol and let you focus on building the capabilities that matter. As you gain experience, you can implement more of the protocol yourself if needed.

MCP is the integration layer between AI reasoning and enterprise action. A well-implemented MCP server makes it easy for AI agents to access the systems and data they need. A poorly implemented one creates security risks and operational headaches. Invest the time to implement it right.

Further Reading

  • MCP Protocol Overview – An introduction to the Model Context Protocol, its architecture, core primitives, and how it connects agents to tools.
  • MCP Architecture – A complete production-oriented guide to MCP architecture, covering hosts, clients, servers, JSON-RPC communication, and deployment patterns.
  • MCP Server Development – A step-by-step guide to building, configuring, and deploying your own MCP servers.
  • MCP Client Development – Learn how to implement an MCP client that discovers and consumes tools, resources, and prompts.
  • MCP Tools – Deep dive into the tool primitive: exposing functions, handling parameters, and executing actions.
  • MCP Resources – Understanding resources as application-controlled contextual data with URI-based addressing.
  • MCP Prompts – Learn how to expose reusable prompt templates that guide language model interactions.
  • MCP Security – Best practices for securing MCP servers, including authentication, authorization, and tool-safety measures.
  • MCP Deployment – Strategies for deploying MCP servers in production environments.
  • MCP Best Practices – Production-ready recommendations for designing, building, and operating MCP-based systems.
  • MCP vs A2A – A deep-dive comparison between the Model Context Protocol and the Agent-to-Agent Protocol.
  • Agent Tools – A foundational look at tool design, integration patterns, and common tool categories for AI agents.
  • Agent Workflow – Core concepts of agentic workflows: planning, execution, and feedback loops.
  • Production Testing – Strategies for testing AI agents in production environments.
  • Production Evaluation – Comprehensive guide to evaluating AI agent performance and quality.
  • Production Monitoring – Strategies for monitoring AI agent systems in production.
  • Production Observability – Comprehensive guide to logging, metrics, and tracing for production AI agent systems.
  • Production Reliability – Building reliable AI agent systems with fault tolerance and graceful degradation.
  • Production Security – Comprehensive security guidelines for production agents, covering data privacy, authentication, and threat mitigation.