MCP (Model Context Protocol): Building Tool Integrations for AI
Model Context Protocol (MCP) is an open standard protocol developed by Anthropic for connecting AI assistants with external data sources and tools. Think of MCP as "USB-C for AI" — a single universal standard that allows any AI model to connect with any tool without requiring custom integrations for each combination.
In this tutorial, we will explore MCP architecture in depth, build MCP servers and clients from scratch, and create a practical example of a database query server that enables AI assistants to query PostgreSQL safely.
Why MCP Matters
Before MCP, every integration between AI and external tools required a custom implementation. If you had 5 AI models and 10 tools, you would need 50 different integrations. MCP solves this problem by providing a single standard protocol.
Key benefits of MCP:
- Standardization: One protocol for all AI-tool integrations
- Security: Granular access control over data and operations
- Modularity: Servers and clients can be developed independently
- Ecosystem: A growing community with many ready-to-use servers
- Vendor-agnostic: Not tied to any single AI provider
MCP Architecture
MCP uses a client-server architecture with three main components:
1. Host
The host is the AI application used by the end user, such as Claude Desktop, Claude Code, or an IDE with AI features. The host manages connections and provides the user interface.
2. Client
The client is a component within the host that manages a connection to one MCP server. Each client has a 1:1 connection with a single server. The client handles capability negotiation and message routing.
3. Server
The server is a program that exposes resources, tools, and prompts to clients. Servers can run locally or remotely and communicate through supported transport layers.
┌─────────────────────────────────────┐
│ HOST │
│ (Claude Desktop / Claude Code) │
│ │
│ ┌──────────┐ ┌──────────┐ │
│ │ Client 1 │ │ Client 2 │ │
│ └────┬─────┘ └────┬─────┘ │
└───────┼───────────────┼─────────────┘
│ │
┌────▼─────┐ ┌────▼─────┐
│ Server A │ │ Server B │
│ (DB) │ │ (Files) │
└──────────┘ └──────────┘
Core MCP Primitives
MCP defines three core primitives that servers can expose:
Resources
Resources are data that AI can read, such as files, database records, or API responses. Resources are identified by URIs and are read-only.
Tools
Tools are functions that AI can call to perform actions. Tools have parameters defined with JSON Schema and return results in a structured format.
Prompts
Prompts are reusable templates for AI interactions. Prompts help users accomplish common tasks in a structured way.
Installation
Python SDK
# Install using pip
pip install mcp
Or using uv (recommended)
uv add mcp
For development
uv add "mcp[cli]"
TypeScript SDK
# Install using npm
npm install @modelcontextprotocol/sdk
Or using pnpm
pnpm add @modelcontextprotocol/sdk
MCP Inspector (for testing)
# Run MCP Inspector
npx @modelcontextprotocol/inspector
Building an MCP Server with Python
Let's start by building a simple MCP server that exposes resources, tools, and prompts.
Basic Server
# server.py
from mcp.server.fastmcp import FastMCP
Initialize server
mcp = FastMCP("Demo Server")
Define a resource
@mcp.resource("config://app")
def getconfig() -> str:
"""Returns application configuration."""
return "App Configuration: debug=true, version=1.0"
Define a tool
@mcp.tool()
def calculatebmi(weightkg: float, heightm: float) -> str:
"""Calculate Body Mass Index (BMI).
Args:
weightkg: Weight in kilograms
heightm: Height in meters
"""
bmi = weightkg / (heightm * 2)
category = ""
if bmi < 18.5:
category = "Underweight"
elif bmi < 25:
category = "Normal"
elif bmi < 30:
category = "Overweight"
else:
category = "Obese"
return f"BMI: {bmi:.1f} ({category})"
Define a prompt
@mcp.prompt()
def reviewcode(code: str) -> str:
"""Prompt for code review."""
return f"Please review the following code and provide suggestions:\n\n{code}"
if name == "main":
mcp.run()
Running the Server
# Using stdio transport (default)
python server.py
Using SSE transport
mcp run server.py --transport sse
Using streamable HTTP transport
mcp run server.py --transport streamable-http
Tool Definition with Parameters
Tools are the most powerful feature in MCP. Here is how to define more complex tools:
from mcp.server.fastmcp import FastMCP
from typing import Optional
import json
mcp = FastMCP("Advanced Tools Server")
@mcp.tool()
def searchproducts(
query: str,
category: Optional[str] = None,
minprice: Optional[float] = None,
maxprice: Optional[float] = None,
limit: int = 10
) -> str:
"""Search products by query and filters.
Args:
query: Search keywords
category: Product category (optional)
minprice: Minimum price (optional)
maxprice: Maximum price (optional)
limit: Maximum number of results (default: 10)
"""
results = {
"query": query,
"filters": {
"category": category,
"minprice": minprice,
"maxprice": maxprice
},
"limit": limit,
"results": [
{"name": "Product A", "price": 29.99, "category": "electronics"},
{"name": "Product B", "price": 49.99, "category": "fashion"}
]
}
return json.dumps(results, indent=2)
@mcp.tool()
def createinvoice(
customername: str,
items: list[dict],
taxrate: float = 0.10
) -> str:
"""Create a new invoice.
Args:
customername: Customer name
items: List of items [{name, quantity, price}]
taxrate: Tax rate (default 10%)
"""
subtotal = sum(item["price"] item["quantity"] for item in items)
tax = subtotal taxrate
total = subtotal + tax
invoice = {
"customer": customername,
"items": items,
"subtotal": subtotal,
"tax": tax,
"total": total
}
return json.dumps(invoice, indent=2)
Building an MCP Client
Here is how to build an MCP client that connects to a server:
# client.py
import asyncio
from mcp import ClientSession, StdioServerParameters
from mcp.client.stdio import stdioclient
async def main():
# Configure connection to server
serverparams = StdioServerParameters(
command="python",
args=["server.py"],
env=None
)
async with stdioclient(serverparams) as (read, write):
async with ClientSession(read, write) as session:
# Initialize connection
await session.initialize()
# List all available tools
tools = await session.listtools()
print("Available tools:")
for tool in tools.tools:
print(f" - {tool.name}: {tool.description}")
# List all resources
resources = await session.listresources()
print("\nAvailable resources:")
for resource in resources.resources:
print(f" - {resource.uri}: {resource.name}")
# Call a tool
result = await session.calltool(
"calculatebmi",
arguments={"weightkg": 70, "heightm": 1.75}
)
print(f"\nBMI Result: {result.content[0].text}")
# Read a resource
resourcecontent = await session.readresource("config://app")
print(f"\nConfig: {resourcecontent.contents[0].text}")
if name == "main":
asyncio.run(main())
Transport Types
MCP supports several transport types for communication between client and server:
1. Stdio (Standard Input/Output)
The simplest default transport. The client runs the server as a subprocess and communicates through stdin/stdout.
# Client using stdio
serverparams = StdioServerParameters(
command="python",
args=["server.py"]
)
Pros: Simple, no network configuration needed
Cons: Local connections only
2. SSE (Server-Sent Events)
An HTTP-based transport that enables remote connections.
# Server with SSE transport
from mcp.server.fastmcp import FastMCP
mcp = FastMCP("SSE Server")
Run with SSE
mcp run server.py --transport sse --port 8080
# Client using SSE
from mcp.client.sse import sseclient
async with sseclient("http://localhost:8080/sse") as (read, write):
async with ClientSession(read, write) as session:
await session.initialize()
3. Streamable HTTP
The newest transport that is more efficient, supporting both stateless and stateful connections.
# Server with streamable HTTP
from mcp.server.fastmcp import FastMCP
mcp = FastMCP("HTTP Server", statelesshttp=True)
Run with streamable HTTP
mcp run server.py --transport streamable-http --port 8080
Integration with Claude Desktop
To use an MCP server with Claude Desktop, edit the configuration file:
MacOS:~/Library/Application Support/Claude/claudedesktopconfig.json
Windows: %APPDATA%\Claude\claudedesktopconfig.json
Linux: ~/.config/Claude/claudedesktopconfig.json
{
"mcpServers": {
"demo-server": {
"command": "python",
"args": ["/path/to/server.py"],
"env": {
"DATABASEURL": "postgresql://user:pass@localhost/mydb"
}
},
"another-server": {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-filesystem", "/home/user/documents"]
}
}
}
After saving the configuration, restart Claude Desktop. You will see a tools icon (hammer) at the bottom of the chat input indicating available tools.
Integration with Claude Code
Claude Code supports MCP servers natively. You can add servers through the CLI:
# Add MCP server to project scope
claude mcp add db-server -s project -- python /path/to/dbserver.py
Add with environment variables
claude mcp add db-server -s project \
-e DATABASEURL=postgresql://user:pass@localhost/mydb \
-- python /path/to/dbserver.py
Add remote SSE/HTTP server
claude mcp add remote-server -s user --transport http \
http://api.example.com/mcp
List all MCP servers
claude mcp list
Remove an MCP server
claude mcp remove db-server
Configuration is stored in the .mcp.json file at the project root:
{
"mcpServers": {
"db-server": {
"command": "python",
"args": ["/path/to/dbserver.py"],
"env": {
"DATABASEURL": "postgresql://user:pass@localhost/mydb"
}
}
}
}
Testing with MCP Inspector
MCP Inspector is a visual tool for testing and debugging MCP servers:
# Run Inspector
npx @modelcontextprotocol/inspector
Inspector will open browser at http://localhost:6274
With Inspector, you can:
- View all resources, tools, and prompts exposed by the server
- Call tools with custom parameters
- Read resources
- Inspect server responses in real-time
- Debug connection issues
You can also run Inspector directly with a server:
# Directly test a specific server
npx @modelcontextprotocol/inspector python server.py
Test server with environment variables
DATABASEURL=postgresql://localhost/mydb \
npx @modelcontextprotocol/inspector python server.py
Security Considerations
Security is critical when building MCP servers. Here are the best practices:
1. Input Validation
import re
@mcp.tool()
def querydata(tablename: str, limit: int = 10) -> str:
"""Query data from a specific table."""
# Validate table name - only alphanumeric and underscores
if not re.match(r'^[a-zA-Z][a-zA-Z0-9]