MCP Protocol in Practice: Building AI Agent Tool Chains with Model Context Protocol
Summary
- MCP (Model Context Protocol) is an AI Agent tool invocation standard protocol proposed by Anthropic, and has become the de facto Agent tool chain protocol in 2026
- MCP implements tool registration, discovery, and invocation through JSON-RPC 2.0, solving the AI Agent "tool silo" problem
- This article covers the full chain from MCP protocol principles to Server development, from tool registration to multi-Agent orchestration, with complete code
- MCP supports two transport modes: SSE and Stdio. SSE is suitable for remote deployment, Stdio for local development
- Bonus: MCP Server production deployment solution and tool chain security audit checklist
Table of Contents
- Why AI Agents Need the MCP Protocol
- MCP Protocol Core Mechanisms
- MCP Server Development: Build a Custom Tool Service in 5 Steps
- MCP Client Integration: Enabling LLMs to Call Tools
- Multi-Agent Orchestration: MCP Tool Chain Composition
- Production Deployment and Security Audit
- Summary and Further Reading
Why AI Agents Need the MCP Protocol
Before 2026, AI Agents invoked tools in a "everyone for themselves" manner — OpenAI used Function Calling, LangChain used Tool Abstraction, and AutoGPT used custom plugins. Each framework defined its own tool interface, making tools impossible to reuse across frameworks and Agents unable to collaborate across platforms.
` ┌──────────────────────────────────────────────────────────────┐ │ The "Tower of Babel" of AI Agent Tool Invocation │ │ │ │ OpenAI Agent ──→ Function Calling (JSON Schema) │ │ LangChain ──→ Tool Abstraction (Python Class) │ │ AutoGPT ──→ Plugin System (YAML Config) │ │ Dify ──→ Tool Node (API Config) │ │ Coze ──→ Plugin Market (Proprietary) │ │ │ │ ❌ No tool reuse ❌ No unified interface ❌ No security guarantees ❌ No discovery standard │ └──────────────────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────────────────┐ │ MCP Protocol: The "USB-C" for AI Agents │ │ │ │ Any Agent ←──→ MCP Protocol ←──→ Any Tool │ │ │ │ ┌────────┐ MCP ┌──────────┐ MCP ┌──────────┐ │ │ │ Claude │←──────→│ Search │←──────→│ GPT-4 │ │ │ │ Agent │ │ Database │ │ Agent │ │ │ └────────┘ │ API GW │ └──────────┘ │ │ └──────────┘ │ │ │ │ ✅ Tool reuse ✅ Unified interface ✅ Security sandbox ✅ Auto discovery │ └──────────────────────────────────────────────────────────────┘ `
MCP vs Function Calling vs LangChain Tool
| Dimension | MCP | OpenAI Function Calling | LangChain Tool |
|---|---|---|---|
| Protocol Standard | Open Standard (Anthropic) | Vendor-proprietary | Framework-proprietary |
| Tool Discovery | Auto-discovery (capabilities) | Manual registration | Manual registration |
| Transport Protocol | JSON-RPC 2.0 (SSE/Stdio) | HTTP API | Python function call |
| Security Sandbox | ✅ Permission control | ❌ | ❌ |
| Cross-Agent Reuse | ✅ Any MCP Client | ❌ OpenAI only | ❌ LangChain only |
| Streaming Output | ✅ SSE | ✅ SSE | ⚠️ Partial support |
| Multi-tool Orchestration | ✅ Native support | ⚠️ Manual required | ✅ Chain abstraction |
Reference: Model Context Protocol Specification
MCP Protocol Core Mechanisms
Three Roles
| Role | Description | Analogy |
|---|---|---|
| MCP Host | The AI application that initiates the connection (e.g., Claude Desktop, IDE plugin) | USB Host |
| MCP Client | The protocol client that establishes a connection with the MCP Server, embedded in the Host | USB Controller |
| MCP Server | The server that provides tools, resources, and prompts | USB Device |
Protocol Interaction Flow
┌──────────────────────────────────────────────────────────────┐ │ MCP Protocol Interaction Flow │ │ │ │ MCP Host (Claude Desktop) │ │ │ │ │ │ 1. initialize │ │ ├──→ MCP Client ──→ MCP Server (Search Tool) │ │ │ │ │ │ │ 2. capabilities response │ │ │ │←─────────────────────────────────────────┘ │ │ │ │ │ │ 3. tools/list │ │ ├──→ MCP Client ──→ MCP Server │ │ │ │ │ │ │ 4. Tool list response │ │ │ │←─────────────────────────────────────────┘ │ │ │ │ │ │ 5. tools/call (search "K8s GPU scheduling") │ │ ├──→ MCP Client ──→ MCP Server │ │ │ │ │ │ │ 6. Tool execution result │ │ │ │←─────────────────────────────────────────┘ │ └──────────────────────────────────────────────────────────────┘
MCP Capability Types
| Capability | Description | Example |
|---|---|---|
| Tools | Functions that can be called by Agents | Search, database query, API call |
| Resources | Data that can be read by Agents | Files, database records, configuration |
| Prompts | Prompt templates that can be used by Agents | Code review template, translation template |
| Sampling | Server requests LLM completion from Agent | Multi-step reasoning, content generation |
MCP Server Development: Build a Custom Tool Service in 5 Steps
Step 1: Project Initialization
ash mkdir mcp-search-server && cd mcp-search-server pip install mcp fastapi httpx
Step 2: Define Tool Schema
`python from mcp.server import Server from mcp.types import Tool, TextContent import httpx import json
server = Server("search-tools")
@server.list_tools() async def list_tools() -> list[Tool]: return [ Tool( name="web_search", description="Search the internet for the latest information. Use this tool when you need to find real-time data, news, or technical documentation.", inputSchema={ "type": "object", "properties": { "query": { "type": "string", "description": "Search keywords" }, "max_results": { "type": "integer", "description": "Maximum number of results to return", "default": 5 }, "search_depth": { "type": "string", "enum": ["basic", "advanced"], "description": "Search depth: basic for quick search, advanced for deep search", "default": "basic" } }, "required": ["query"] } ), Tool( name="code_search", description="Search code repositories and code snippets on GitHub. Use this when you need to find open-source implementations or API usage examples.", inputSchema={ "type": "object", "properties": { "query": { "type": "string", "description": "Code search keywords" }, "language": { "type": "string", "description": "Programming language filter", "enum": ["python", "go", "rust", "typescript", "java"] } }, "required": ["query"] } ), Tool( name="api_tester", description="Send HTTP requests to test API endpoints. Use this when you need to verify API availability or retrieve API response data.", inputSchema={ "type": "object", "properties": { "url": { "type": "string", "description": "API endpoint URL" }, "method": { "type": "string", "enum": ["GET", "POST", "PUT", "DELETE"], "default": "GET" }, "headers": { "type": "object", "description": "Request headers" }, "body": { "type": "object", "description": "Request body (JSON)" } }, "required": ["url"] } ), ] `
Step 3: Implement Tool Logic
`python @server.call_tool() async def call_tool(name: str, arguments: dict) -> list[TextContent]: if name == "web_search": return await handle_web_search(arguments) elif name == "code_search": return await handle_code_search(arguments) elif name == "api_tester": return await handle_api_tester(arguments) else: raise ValueError(f"Unknown tool: {name}")
async def handle_web_search(args: dict) -> list[TextContent]: query = args["query"] max_results = args.get("max_results", 5) search_depth = args.get("search_depth", "basic")
async with httpx.AsyncClient(timeout=30) as client:
response = await client.post(
"https://api.search.brave.com/res/v1/web/search",
headers={"X-Subscription-Token": BRAVE_API_KEY},
params={
"q": query,
"count": max_results,
"search_depth": search_depth,
}
)
data = response.json()
results = []
for item in data.get("web", {}).get("results", [])[:max_results]:
results.append({
"title": item.get("title", ""),
"url": item.get("url", ""),
"description": item.get("description", ""),
})
return [TextContent(
type="text",
text=json.dumps(results, ensure_ascii=False, indent=2)
)]
async def handle_code_search(args: dict) -> list[TextContent]: query = args["query"] language = args.get("language", "")
async with httpx.AsyncClient(timeout=30) as client:
response = await client.get(
"https://api.github.com/search/code",
headers={"Authorization": f"token {GITHUB_TOKEN}"},
params={
"q": f"{query} language:{language}" if language else query,
"per_page": 5,
}
)
data = response.json()
results = []
for item in data.get("items", [])[:5]:
results.append({
"name": item.get("name", ""),
"path": item.get("path", ""),
"repository": item.get("repository", {}).get("full_name", ""),
"html_url": item.get("html_url", ""),
})
return [TextContent(
type="text",
text=json.dumps(results, ensure_ascii=False, indent=2)
)]
async def handle_api_tester(args: dict) -> list[TextContent]: url = args["url"] method = args.get("method", "GET") headers = args.get("headers", {}) body = args.get("body")
async with httpx.AsyncClient(timeout=30) as client:
response = await client.request(
method=method,
url=url,
headers=headers,
json=body,
)
return [TextContent(
type="text",
text=json.dumps({
"status_code": response.status_code,
"headers": dict(response.headers),
"body": response.json() if "json" in response.headers.get("content-type", "") else response.text[:2000],
}, ensure_ascii=False, indent=2)
)]
`
Step 4: Configure SSE Transport
`python from mcp.server.sse import SseServerTransport from starlette.applications import Starlette from starlette.routing import Route
sse = SseServerTransport("/messages")
async def handle_sse(request): async with sse.connect_sse(request) as streams: await server.run(streams[0], streams[1], server.create_initialization_options())
async def handle_messages(request): await sse.handle_post_message(request)
app = Starlette( routes=[ Route("/sse", endpoint=handle_sse), Route("/messages", endpoint=handle_messages, methods=["POST"]), ] )
if name == "main": import uvicorn uvicorn.run(app, host="0.0.0.0", port=8080) `
Step 5: Docker Deployment
`dockerfile FROM python:3.11-slim
WORKDIR /app COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt COPY . .
EXPOSE 8080
HEALTHCHECK --interval=30s --timeout=5s --retries=3
CMD curl -f http://localhost:8080/sse || exit 1
CMD ["python", "server.py"] `
MCP Client Integration: Enabling LLMs to Call Tools
Python MCP Client
`python from mcp import ClientSession, StdioServerParameters from mcp.client.stdio import stdio_client from openai import OpenAI import json
server_params = StdioServerParameters( command="python", args=["server.py"], env={"BRAVE_API_KEY": "...", "GITHUB_TOKEN": "..."} )
async def run_agent(user_query: str): async with stdio_client(server_params) as (read, write): async with ClientSession(read, write) as session: await session.initialize()
tools_result = await session.list_tools()
available_tools = [
{
"type": "function",
"function": {
"name": tool.name,
"description": tool.description,
"parameters": tool.inputSchema,
}
}
for tool in tools_result.tools
]
client = OpenAI(base_url="http://localhost:8000/v1", api_key="not-needed")
messages = [{"role": "user", "content": user_query}]
response = client.chat.completions.create(
model="Qwen/Qwen2.5-7B-Instruct",
messages=messages,
tools=available_tools,
tool_choice="auto",
)
message = response.choices[0].message
if message.tool_calls:
for tool_call in message.tool_calls:
result = await session.call_tool(
tool_call.function.name,
arguments=json.loads(tool_call.function.arguments),
)
messages.append(message)
messages.append({
"role": "tool",
"tool_call_id": tool_call.id,
"content": result[0].text,
})
final_response = client.chat.completions.create(
model="Qwen/Qwen2.5-7B-Instruct",
messages=messages,
)
return final_response.choices[0].message.content
return message.content
`
Tool Invocation Flow
┌──────────────────────────────────────────────────────────────┐ │ MCP Agent Tool Invocation Complete Flow │ │ │ │ User: "Search for the latest K8s GPU scheduling solutions │ │ and test the related APIs" │ │ │ │ │ ▼ │ │ LLM analyzes intent → needs to call 2 tools: │ │ 1. web_search("K8s GPU scheduling 2026") │ │ 2. api_tester("https://kubernetes.io/docs/concepts/") │ │ │ │ │ ├──→ MCP Client ──→ web_search ──→ Returns search results │ │ │ │ │ ├──→ MCP Client ──→ api_tester ──→ Returns API response │ │ │ │ │ ▼ │ │ LLM integrates results → Generates final answer │ └──────────────────────────────────────────────────────────────┘
Multi-Agent Orchestration: MCP Tool Chain Composition
Multi-Server Orchestration Architecture
`python from mcp import ClientSession from mcp.client.sse import sse_client import asyncio
class MCPToolOrchestrator: def init(self): self.servers = {} self.sessions = {}
async def register_server(self, name: str, url: str):
self.servers[name] = url
async def connect_all(self):
for name, url in self.servers.items():
read, write = await sse_client(url)
session = ClientSession(read, write)
await session.initialize()
self.sessions[name] = session
async def discover_all_tools(self) -> list[dict]:
all_tools = []
for name, session in self.sessions.items():
tools = await session.list_tools()
for tool in tools.tools:
all_tools.append({
"server": name,
"name": tool.name,
"description": tool.description,
"inputSchema": tool.inputSchema,
})
return all_tools
async def call_tool(self, tool_name: str, arguments: dict) -> str:
for name, session in self.sessions.items():
tools = await session.list_tools()
if any(t.name == tool_name for t in tools.tools):
result = await session.call_tool(tool_name, arguments)
return result[0].text
raise ValueError(f"Tool {tool_name} not found in any server")
async def close_all(self):
for session in self.sessions.values():
await session.close()
orchestrator = MCPToolOrchestrator() await orchestrator.register_server("search", "http://search-mcp:8080/sse") await orchestrator.register_server("database", "http://db-mcp:8081/sse") await orchestrator.register_server("code", "http://code-mcp:8082/sse") await orchestrator.connect_all() `
Deploying MCP Server Cluster on K8s
`yaml apiVersion: apps/v1 kind: Deployment metadata: name: mcp-search-server namespace: ai-agent spec: replicas: 2 selector: matchLabels: app: mcp-search-server template: metadata: labels: app: mcp-search-server spec: containers: - name: mcp-server image: myregistry/mcp-search-server:v1.0 ports: - containerPort: 8080 resources: requests: cpu: "1" memory: 512Mi limits: cpu: "2" memory: 1Gi env: - name: BRAVE_API_KEY valueFrom: secretKeyRef: name: mcp-secrets key: brave-api-key livenessProbe: httpGet: path: /sse port: 8080 initialDelaySeconds: 10 periodSeconds: 30
apiVersion: v1 kind: Service metadata: name: mcp-search-svc namespace: ai-agent spec: selector: app: mcp-search-server ports: - port: 8080 targetPort: 8080 `
Production Deployment and Security Audit
MCP Security Model
| Security Layer | Mechanism | Description |
|---|---|---|
| Transport | TLS + SSE | Encrypted transmission, prevents man-in-the-middle attacks |
| Authentication | OAuth 2.0 / API Key | Server verifies Client identity |
| Authorization | Tool permission whitelist | Restricts the range of callable tools |
| Data | Input validation + Output filtering | Prevents injection attacks and data leakage |
| Audit | Call logs + Metering | Full-chain tracing and cost control |
Security Audit Checklist
| Check Item | Risk Level | Check Method |
|---|---|---|
| Tool input parameter validation | High | Schema validation + Type checking |
| Tool output data desensitization | High | Regex filtering of sensitive information |
| API Keys not hardcoded | High | Environment variables / Secret management |
| SSE connection timeout control | Medium | Heartbeat + Timeout disconnect |
| Tool call rate limiting | Medium | Rate Limiting |
| Call log completeness | Medium | Structured logging + Audit trail |
| Error messages do not leak internal state | Low | Unified error format |
MCP Server Performance Benchmarks
| Metric | Single Instance | 3-Instance Cluster |
|---|---|---|
| Tool call QPS | 500 | 1400 |
| Average latency (P50) | 45ms | 50ms |
| P99 latency | 180ms | 200ms |
| SSE connections | 200 | 600 |
| Memory usage | 256MB | 256MB x 3 |
Summary and Further Reading
The MCP protocol is the "USB-C" for AI Agent tool chains — a single open standard that solves the three major problems of tool silos, inconsistent interfaces, and lack of security guarantees. Through MCP Server development, Client integration, and multi-Agent orchestration, you can rapidly build production-grade AI Agent tool chains.
Key Development Takeaways:
- MCP implements tool registration, discovery, and invocation through JSON-RPC 2.0
- SSE mode is suitable for remote deployment, Stdio mode for local development
- Tool Schema definition is the core of MCP — parameters and semantics must be described in detail
- Multi-Agent orchestration enables cross-Server tool invocation through MCPToolOrchestrator
- Production deployment requires attention to security auditing: input validation, output desensitization, rate limiting
Related Reading:
- AI Agent Workflow Engine in Practice: From LangGraph to Custom Agent Frameworks — Agent framework design and MCP integration
- LLM Inference Acceleration Benchmarks: vLLM vs TensorRT-LLM vs SGLang — Inference backend selection for MCP Agents
- Python AI Workflow Dify in Practice — MCP tool integration on the Dify platform
Authoritative References:
Try these browser-local tools — no sign-up required →