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:
- Understand the protocol boundary — What belongs in the protocol layer, and what is application logic?
- Choose an implementation approach — Use an SDK, integrate with a framework, or implement from scratch?
- Build the server or client — Implement lifecycle, capabilities, and request handling.
- Test and debug — Ensure interoperability and robust error handling.
- 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-mcpplugin - 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
| Aspect | Official SDK | Framework Integration | From Scratch |
|---|---|---|---|
| Implementation speed | Fastest | Fast | Slow |
| Protocol knowledge required | Low | Low | High |
| Control over behavior | High | Moderate | Complete |
| Debugging complexity | Low | Moderate | High |
| Production suitability | High | High | Depends on implementation quality |
| Maintenance burden | Low | Moderate | High |
| Learning investment | Low | Moderate | High |
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
| Aspect | Resources | Tools |
|---|---|---|
| Purpose | Provide context | Perform actions |
| Control | Application-controlled | Model-controlled |
| Side effects | None (read-only) | May have side effects |
| Execution | Client fetches | Model invokes |
| Method | resources/read | tools/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 nullmethod: 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:
| Code | Name | Description |
|---|---|---|
| -32700 | Parse error | Invalid JSON was received |
| -32600 | Invalid Request | The JSON sent is not a valid Request object |
| -32601 | Method not found | The method does not exist |
| -32602 | Invalid params | Invalid method parameters |
| -32603 | Internal error | Internal JSON-RPC error |
| -32000..-32099 | Server error | Reserved 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 Operation | JSON-RPC Method |
|---|---|
| Initialize | initialize |
| List tools | tools/list |
| Call tool | tools/call |
| List resources | resources/list |
| Read resource | resources/read |
| List prompts | prompts/list |
| Get prompt | prompts/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
Originheader - The server must implement OAuth 2.1 authentication
- The server should support CORS for web clients
Security: OAuth 2.1, TLS required.
Transport Comparison
| Aspect | stdio | Streamable HTTP |
|---|---|---|
| Deployment | Local subprocess | Remote service |
| Clients | Typically one client | Many clients |
| Authentication | Environment credentials | OAuth 2.1 |
| Network | No network surface | HTTP/HTTPS |
| Streaming | No | Yes (SSE) |
| Scalability | Process-level | Service-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
- Agent receives user request
- Agent reasons about the task — Determines what tools or resources are needed
- Agent queries the MCP client — For available tools and their descriptions
- Agent selects a tool — Based on the task and tool descriptions
- Agent invokes the tool via the MCP client
- 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 = {
}
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
| Category | Examples | Handling Strategy |
|---|---|---|
| Protocol errors | Invalid JSON, unsupported version | Return JSON-RPC error responses |
| Validation errors | Invalid arguments, missing required fields | Return descriptive error messages |
| Authorization errors | Missing authentication, insufficient permissions | Return 401/403 with appropriate messages |
| Application errors | Tool execution failure, dependency unavailable | Return error with details, retry if transient |
| Network errors | Connection timeout, transport failure | Retry 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
- Use JSON Schema for structure validation — Type, required fields, patterns
- Implement additional validation in code — Business rules, resource limits
- Validate at the protocol boundary — Before executing the tool
- Return descriptive errors — Help clients fix their requests
- 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 Type | Purpose | When |
|---|---|---|
| Unit tests | Test individual components | Every commit |
| Protocol tests | Verify JSON-RPC message handling | Every commit |
| Integration tests | Test server-client interaction | Every merge |
| Contract tests | Verify client-server compatibility | Every release |
| Negative tests | Test error handling | Every release |
| Security tests | Test authentication, injection | Weekly |
| Load tests | Test performance under load | Monthly |
Example Test Matrix
| Test Case | Expected Behavior |
|---|---|
initialize with valid version | Success response |
initialize with unsupported version | Error response |
tools/list after initialization | List of tools |
tools/list before initialization | Error response |
tools/call with valid arguments | Tool result |
tools/call with missing required argument | Error response |
tools/call with invalid argument type | Error response |
tools/call with non-existent tool | Error response |
resources/read with valid URI | Resource content |
resources/read with invalid URI | Error response |
prompts/get with valid name | Prompt 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:
- Connect a standard MCP client (e.g., Claude Desktop)
- Connect a test client
- 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
| Control | Implementation |
|---|---|
| Authentication | OAuth 2.1 for HTTP, environment credentials for stdio |
| Authorization | Scoped access, tool-level permissions |
| Input validation | JSON Schema validation, additional code validation |
| Tool allowlists | Validate tool names against allowlist |
| Secrets management | Environment variables, secret managers |
| Audit logging | Log all tool calls, authorization decisions |
| Rate limiting | Gateway-level or server-level |
Secure Coding Practices
- Never trust client input — Validate everything at the protocol boundary
- Use least privilege — Only expose necessary capabilities
- Prevent path traversal — Validate file paths
- Sanitize outputs — Prevent data leakage
- Log security events — Authentication failures, authorization decisions, tool calls
Production Implementation
Turning a Prototype into Production
| Aspect | Implementation |
|---|---|
| Containerization | Docker, OCI image with SBOM |
| Health checks | /health or /ready endpoint |
| Configuration | Environment variables, ConfigMap |
| Secrets | Vault, Secret Manager |
| Observability | Logs, metrics, traces |
| Scaling | Horizontal scaling, autoscaling |
| Deployment | CI/CD pipeline, rolling/canary updates |
| Rollback | Quick rollback capability |
| Versioning | Semantic 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
| Mistake | Why It's Problematic | How to Avoid |
|---|---|---|
| Implementing protocol behavior manually when an SDK is available | Prone to subtle bugs, wasted time | Use official SDK |
| Mixing business logic and protocol logic | Hard to test, hard to evolve | Separate layers |
| Weak input schemas | Security vulnerabilities, poor interoperability | Define strong schemas |
| Missing error handling | Poor user experience, undetected failures | Implement comprehensive error handling |
| Exposing too many tools | Increased attack surface | Minimize tool surface |
| Trusting unvalidated input | Security vulnerabilities | Validate all inputs |
| Ignoring capability negotiation | Missing features, breakage with clients | Implement capability negotiation correctly |
| Assuming local and remote deployments behave identically | Security issues, transport differences | Test both models |
| Not testing client/server interoperability | Breakage when used with real clients | Run compliance tests |
| Logging secrets or sensitive payloads | Data exposure | Redact 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
| Problem | Likely Cause | How to Diagnose | Recommended Fix |
|---|---|---|---|
| Server not discovered | Transport issue, wrong command/path | Check transport configuration | Verify command path, test with stdio |
| Initialization failure | Version mismatch, missing capabilities | Inspect initialize request/response | Use compatible protocol version |
| Tool missing | Tool registration failed, capability mismatch | Check tools/list response | Verify tool registration, check capabilities |
| Tool arguments rejected | Invalid schema, missing required arguments | Inspect tool call request | Validate against schema, provide required args |
| Timeout | Long-running operation | Check operation duration | Optimize, increase timeout |
| Authentication failure | Missing/invalid token | Check Authorization header | Implement proper token validation |
| Authorization failure | Insufficient scope | Check token scopes | Request correct scopes |
| Unexpected response | Protocol bug, version mismatch | Inspect response format | Use correct protocol version |
| Connection dropped | Network issue, server crash | Check logs, network connectivity | Implement reconnect, health checks |
| Deployment regression | Breaking change, configuration issue | Compare with working version | Rollback, 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.