The release of the open-source Model Context Protocol (MCP) by Anthropic has fundamentally solved the fragmented landscape of AI tool calling. Before MCP, connecting an LLM to your local database, developer terminal, or internal API required writing bespoke integration wrappers for every distinct framework (LangChain, LlamaIndex, Semantic Kernel, and AutoGen).
With MCP, you write your tool once as a standardized server, and any compliant host—such as the Claude Desktop app, Cursor, or custom Python autonomous agents—can immediately discover, inspect schemas, and execute your tools securely.
If you have already read our introductory guide on how to Build a Model Context Protocol (MCP) Server in Python and explored Tool Calling in AI Agents: 2026, this tutorial takes your skills to the production level. In this hands-on engineering walkthrough, we cover how to build custom local tools and connect python mcp server ai agent systems step-by-step.
1. High-Level Architecture: Client-Server Protocol Model
MCP follows a client-server architecture where communication occurs through standardized JSON-RPC 2.0 messages transmitted via standard input/output (stdio) or Server-Sent Events (SSE):
Bidirectional JSON-RPC 2.0
1. Host / Client Layer (Claude Desktop or Custom Python Agent)
Runs the LLM client application and initiates tool discovery using mcp.client.stdio. When you prompt Claude, it checks registered schemas to determine if a local action is required.
2. Local Subprocess Boundary (FastMCP Server)
An isolated Python worker process managed by FastMCP. The server exposes type-safe functions without binding public HTTP ports, executing commands entirely within your secure local environment.
Tool 2: Git Telemetry
Tool 3: S3 Healthcheck
Because the server runs as an isolated subprocess, it runs safely inside your local development environment without exposing public HTTP ports to the internet.
2. Prerequisites & Environment Setup
🚀 High-Performance GPU Cloud for LLMs
Need on-demand GPUs to run Ollama, vLLM, or fine-tune models in production? RunPod offers serverless GPUs and secure pods from $0.20/hr.
Set up a dedicated Python 3.10+ virtual environment and install the official Anthropic and MCP SDKs:
pip install --upgrade mcp anthropic pydantic
Verify your installation:
import mcp
import anthropic
print(f"MCP SDK Version: {mcp.__version__}")
print(f"Anthropic SDK Version: {anthropic.__version__}")
3. Step 1: Building a Custom Local MCP Server in Python
Create a file named server.py. We will expose two essential developer tools: a system resource inspector and an SQLite query executor:
import os
import psutil
import sqlite3
from typing import Dict, Any, List
from mcp.server.fastmcp import FastMCP
# Initialize FastMCP Server
mcp = FastMCP("LocalDevOpsTools")
@mcp.tool()
def get_system_telemetry() -> Dict[str, Any]:
"""Fetches live CPU utilization, memory pressure, and available disk space."""
return {
"cpu_percent": psutil.cpu_percent(interval=0.5),
"memory_used_gb": round(psutil.virtual_memory().used / (1024**3), 2),
"memory_total_gb": round(psutil.virtual_memory().total / (1024**3), 2),
"disk_free_gb": round(psutil.disk_usage("/").free / (1024**3), 2)
}
@mcp.tool()
def query_developer_db(sql_query: str) -> List[Dict[str, Any]]:
"""Executes a read-only SELECT query against the local SQLite developer database."""
# Enforce read-only safety guardrail
cleaned = sql_query.strip().upper()
if not cleaned.startswith("SELECT") or any(kw in cleaned for kw in ["DROP", "DELETE", "UPDATE", "INSERT", "ALTER"]):
raise ValueError("Security Violation: Only read-only SELECT queries are permitted.")
db_path = os.path.expanduser("~/dev_database.db")
conn = sqlite3.connect(db_path)
conn.row_factory = sqlite3.Row
cursor = conn.cursor()
cursor.execute(sql_query)
rows = cursor.fetchall()
results = [dict(row) for row in rows]
conn.close()
return results
if __name__ == "__main__":
# Launch via stdio transport
mcp.run(transport="stdio")
4. Step 2: Connecting Claude Desktop to Your Local MCP Tools
Claude Desktop natively reads configured MCP servers from a JSON manifest.
Table of Contents
- 1 Configuration File Locations:
- 2 Error 1: MCP server exited with code 1 before handshake
- 3 Error 2: FileNotFoundError: [Errno 2] No such file or directory: 'python'
- 4 Can I run MCP servers across different programming languages?
- 5 Does MCP support streaming output?
- 6 📦 Download the Production Code & Architecture Checklist
Configuration File Locations:
- macOS:
~/Library/Application Support/Claude/claude_desktop_config.json - Windows:
%APPDATA%\Claude\claude_desktop_config.json - Linux:
~/.config/Claude/claude_desktop_config.json
Open or create claude_desktop_config.json and add your Python server configuration:
{
"mcpServers": {
"local-devops": {
"command": "/Users/username/.venv/bin/python",
"args": [
"/Users/username/projects/mcp-tools/server.py"
],
"env": {
"PYTHONUNBUFFERED": "1"
}
}
}
}
Important: Always use the absolute path to your virtual environment Python binary and your script to prevent PATH resolution errors.
Restart Claude Desktop. In the bottom-right corner of the chat input box, you will now see a Hammer (Tools) icon indicating that your custom tools get_system_telemetry and query_developer_db are registered and available to Claude!
5. Step 3: Building a Custom Autonomous AI Agent in Python
If you are developing custom agentic software, you can connect directly to the MCP server from Python using the mcp.client.stdio transport and Anthropic’s Messages API:
import asyncio
from mcp import ClientSession, StdioServerParameters
from mcp.client.stdio import stdio_client
from anthropic import Anthropic
async def run_mcp_autonomous_agent(user_query: str):
# 1. Define server parameters pointing to local Python MCP server
server_params = StdioServerParameters(
command="python",
args=["server.py"],
env=None
)
anthropic_client = Anthropic()
# 2. Establish stdio client connection to MCP server
async with stdio_client(server_params) as (read_stream, write_stream):
async with ClientSession(read_stream, write_stream) as session:
# Initialize connection and handshake
await session.initialize()
# 3. Discover available tools from MCP server dynamically
tools_result = await session.list_tools()
available_tools = []
for tool in tools_result.tools:
available_tools.append({
"name": tool.name,
"description": tool.description,
"input_schema": tool.inputSchema
})
print(f"✓ Discovered {len(available_tools)} MCP tools: {[t['name'] for t in available_tools]}")
# 4. Invoke Claude with user instruction and MCP tool schemas
messages = [{"role": "user", "content": user_query}]
response = anthropic_client.messages.create(
model="claude-3-5-sonnet-20241022",
max_tokens=1024,
tools=available_tools,
messages=messages
)
# 5. Check if Claude requested tool execution
if response.stop_reason == "tool_use":
for block in response.content:
if block.type == "tool_use":
tool_name = block.name
tool_args = block.input
print(f"⚡ Claude requested execution: {tool_name}({tool_args})")
# Execute tool via MCP session
execution_result = await session.call_tool(tool_name, tool_args)
tool_output = execution_result.content[0].text
# Feed tool result back to Claude for final synthesis
messages.append({"role": "assistant", "content": response.content})
messages.append({
"role": "user",
"content": [
{
"type": "tool_result",
"tool_use_id": block.id,
"content": str(tool_output)
}
]
})
final_response = anthropic_client.messages.create(
model="claude-3-5-sonnet-20241022",
max_tokens=1024,
messages=messages
)
print(f"\nAgent Final Answer:\n{final_response.content[0].text}")
else:
print(f"Claude Response: {response.content[0].text}")
if __name__ == "__main__":
asyncio.run(run_mcp_autonomous_agent("Check my local system telemetry and report if memory usage is within safe bounds."))
6. Security & Sandboxing Best Practices
- Read-Only Enforcements: When connecting tools to databases or filesystem paths, enforce strict whitelist checks (e.g., verifying
SELECTstatements only). - Path Sanitization: When accepting filenames or directory paths, use
os.path.abspathand verify that the target path does not escape your sandbox via directory traversal (../). - Execution Timeouts: Wrap tool execution in
asyncio.wait_for(timeout=15.0)to prevent hung processes from blocking your AI agent loop.
7. Common Errors & Troubleshooting
Error 1: MCP server exited with code 1 before handshake
Root Cause: A syntax error or missing module import in server.py.
Fix: Run python server.py directly in your terminal. If the script throws a traceback or exits immediately, fix the Python exception before launching through Claude Desktop.
Error 2: FileNotFoundError: [Errno 2] No such file or directory: 'python'
Root Cause: Claude Desktop runs without inheriting your terminal’s custom shell $PATH.
Fix: Specify the complete absolute path to your Python interpreter (e.g. /usr/local/bin/python3 or /home/user/venv/bin/python).
8. Frequently Asked Questions (FAQ)
Can I run MCP servers across different programming languages?
Yes! One of the greatest strengths of MCP is language interoperability. Your client can be written in Python while connecting to MCP servers written in TypeScript, Go, Rust, or C#.
Does MCP support streaming output?
Yes. MCP supports progress notifications and streaming chunk responses across long-running data analysis and retrieval tasks.
9. Summary & Actionable Checklist
- Implement custom tools: Use
mcp.server.fastmcp.FastMCPwith@mcp.tool()decorators. - Configure Claude Desktop: Add your server definition to
claude_desktop_config.jsonwith absolute paths. - Build programmatic Python agents: Connect via
stdio_clientandClientSessionto dynamically forward tools to Claude or custom LLM runtimes. - Implement safety guardrails: Enforce strict query parsing and path validation before executing system operations.
📦 Download the Production Code & Architecture Checklist
Get the complete, runnable GitHub repository for this tutorial, including Docker Compose configs, environment templates, and CI/CD pipelines.




Leave a Reply