MCP Servers Explained: Build AI Tool Integrations
Model Context Protocol — what it is, why it matters & build your first MCP server
Model Context Protocol (MCP) has become the standard way AI tools connect to external services. If you have ever wanted Claude to read your database, Cursor to access your company's API, or any AI tool to interact with a custom service, MCP is how you make it happen.
This tutorial takes you from understanding what MCP servers are to building and running your own. No prior MCP experience needed — just basic Python or TypeScript knowledge.
What You Will Learn
- What MCP is and why it was created
- The three capabilities MCP servers can expose: tools, resources, and prompts
- How to build a working MCP server in Python
- How to build a working MCP server in TypeScript
- How to connect your server to Claude Desktop, Claude Code, and Cursor
- Debugging and testing your MCP server
- Real-world MCP server patterns
For a conceptual overview of MCP before diving into code, read our What is MCP guide.
Why MCP Matters
Before MCP, every AI tool had its own plugin system. ChatGPT had custom GPTs with Actions. GitHub Copilot had extensions. Each required learning a different API, different authentication, different deployment. Building one integration for one tool was manageable. Building integrations for every AI tool your team uses was impractical.
MCP solved this with a universal protocol. You build one MCP server, and it works with every MCP-compatible AI client — Claude, Cursor, VS Code Copilot, Windsurf, and more.
Think of it like this:
| Before MCP | After MCP | |-----------|----------| | Custom plugin per AI tool | One server, all AI tools | | Each tool has a different API | One standard protocol | | Updates break integrations | Protocol handles compatibility | | Vendor lock-in | Tool-agnostic | | N tools x M services = N*M integrations | N tools + M servers = N+M integrations |
MCP Architecture
An MCP system has three components:
1. MCP Host — The AI application where the user interacts (Claude Desktop, Cursor, VS Code)
2. MCP Client — A protocol handler inside the host that manages communication with MCP servers. You do not build this — it is part of the host application.
3. MCP Server — The program you build. It exposes capabilities to the AI through three types:
- Tools — Functions the AI can call. Example:
search_database(query),send_email(to, subject, body),create_invoice(data) - Resources — Data the AI can read. Example: file contents, database records, API responses
- Prompts — Pre-built prompt templates the user can invoke. Example:
/analyze-code,/summarize-meeting
User ↔ MCP Host (Claude Desktop)
↔ MCP Client (built into host)
↔ MCP Server (your code)
↔ External Service (database, API, filesystem)
The communication happens over stdio (standard input/output) for local servers or SSE (Server-Sent Events) over HTTP for remote servers.
Building Your First MCP Server (Python)
Let us build a practical MCP server that provides a tool for looking up Indian pin codes and their associated city/state information.
Step 1: Set Up the Project
# Create project directory
mkdir mcp-pincode-server
cd mcp-pincode-server
# Create virtual environment
python -m venv venv
source venv/bin/activate # On Windows: venv\Scripts\activate
# Install the MCP SDK
pip install mcp[cli]
Step 2: Create the Server
Create a file called server.py:
from mcp.server import Server
from mcp.server.stdio import stdio_server
from mcp.types import Tool, TextContent
import json
import urllib.request
# Initialize the MCP server
app = Server("pincode-lookup")
# Sample data for offline use (subset of Indian pincodes)
PINCODE_DATA = {
"110001": {"city": "New Delhi", "state": "Delhi", "district": "Central Delhi"},
"400001": {"city": "Mumbai", "state": "Maharashtra", "district": "Mumbai"},
"560001": {"city": "Bengaluru", "state": "Karnataka", "district": "Bangalore Urban"},
"600001": {"city": "Chennai", "state": "Tamil Nadu", "district": "Chennai"},
"700001": {"city": "Kolkata", "state": "West Bengal", "district": "Kolkata"},
"500001": {"city": "Hyderabad", "state": "Telangana", "district": "Hyderabad"},
"380001": {"city": "Ahmedabad", "state": "Gujarat", "district": "Ahmedabad"},
"411001": {"city": "Pune", "state": "Maharashtra", "district": "Pune"},
"302001": {"city": "Jaipur", "state": "Rajasthan", "district": "Jaipur"},
"226001": {"city": "Lucknow", "state": "Uttar Pradesh", "district": "Lucknow"},
}
@app.list_tools()
async def list_tools() -> list[Tool]:
"""List available tools."""
return [
Tool(
name="lookup_pincode",
description="Look up an Indian PIN code to get city, state, and district information. Provide a 6-digit Indian PIN code.",
inputSchema={
"type": "object",
"properties": {
"pincode": {
"type": "string",
"description": "A 6-digit Indian PIN code (e.g., '110001' for New Delhi)",
}
},
"required": ["pincode"],
},
),
Tool(
name="find_pincode",
description="Find the PIN code for an Indian city. Provide the city name to search.",
inputSchema={
"type": "object",
"properties": {
"city": {
"type": "string",
"description": "Name of the Indian city (e.g., 'Mumbai', 'Delhi')",
}
},
"required": ["city"],
},
),
]
@app.call_tool()
async def call_tool(name: str, arguments: dict) -> list[TextContent]:
"""Handle tool calls."""
if name == "lookup_pincode":
pincode = arguments.get("pincode", "").strip()
if len(pincode) != 6 or not pincode.isdigit():
return [TextContent(
type="text",
text=f"Invalid PIN code: '{pincode}'. Indian PIN codes are exactly 6 digits."
)]
# Check local data first
if pincode in PINCODE_DATA:
data = PINCODE_DATA[pincode]
return [TextContent(
type="text",
text=json.dumps({
"pincode": pincode,
"city": data["city"],
"state": data["state"],
"district": data["district"],
"source": "local_database"
}, indent=2)
)]
# Try the public API for pincodes not in local data
try:
url = f"https://api.postalpincode.in/pincode/{pincode}"
req = urllib.request.Request(url, headers={"User-Agent": "MCP-Server/1.0"})
with urllib.request.urlopen(req, timeout=5) as response:
result = json.loads(response.read().decode())
if result[0]["Status"] == "Success":
post_office = result[0]["PostOffice"][0]
return [TextContent(
type="text",
text=json.dumps({
"pincode": pincode,
"city": post_office.get("Block", post_office.get("Name", "N/A")),
"state": post_office.get("State", "N/A"),
"district": post_office.get("District", "N/A"),
"source": "postal_api"
}, indent=2)
)]
except Exception:
pass
return [TextContent(
type="text",
text=f"PIN code {pincode} not found in database."
)]
elif name == "find_pincode":
city = arguments.get("city", "").strip().lower()
results = []
for code, data in PINCODE_DATA.items():
if city in data["city"].lower():
results.append({"pincode": code, **data})
if results:
return [TextContent(
type="text",
text=json.dumps(results, indent=2)
)]
return [TextContent(
type="text",
text=f"No PIN codes found for city: '{city}'. Try a major city name."
)]
return [TextContent(type="text", text=f"Unknown tool: {name}")]
async def main():
"""Run the MCP server."""
async with stdio_server() as (read_stream, write_stream):
await app.run(read_stream, write_stream, app.create_initialization_options())
if __name__ == "__main__":
import asyncio
asyncio.run(main())
Step 3: Test the Server
# Test using the MCP CLI inspector
mcp dev server.py
The MCP inspector opens a web interface where you can test your tools interactively.
Building an MCP Server (TypeScript)
If you prefer TypeScript, here is the same server using the TypeScript SDK.
Step 1: Set Up the Project
mkdir mcp-pincode-ts
cd mcp-pincode-ts
npm init -y
npm install @modelcontextprotocol/sdk
npm install -D typescript @types/node
npx tsc --init
Step 2: Create the Server
Create src/index.ts:
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import {
CallToolRequestSchema,
ListToolsRequestSchema,
} from "@modelcontextprotocol/sdk/types.js";
interface PincodeInfo {
city: string;
state: string;
district: string;
}
const PINCODE_DATA: Record<string, PincodeInfo> = {
"110001": { city: "New Delhi", state: "Delhi", district: "Central Delhi" },
"400001": { city: "Mumbai", state: "Maharashtra", district: "Mumbai" },
"560001": { city: "Bengaluru", state: "Karnataka", district: "Bangalore Urban" },
"600001": { city: "Chennai", state: "Tamil Nadu", district: "Chennai" },
"700001": { city: "Kolkata", state: "West Bengal", district: "Kolkata" },
};
const server = new Server(
{ name: "pincode-lookup", version: "1.0.0" },
{ capabilities: { tools: {} } }
);
server.setRequestHandler(ListToolsRequestSchema, async () => ({
tools: [
{
name: "lookup_pincode",
description: "Look up an Indian PIN code to get city, state, and district information.",
inputSchema: {
type: "object" as const,
properties: {
pincode: {
type: "string",
description: "A 6-digit Indian PIN code",
},
},
required: ["pincode"],
},
},
],
}));
server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
if (name === "lookup_pincode") {
const pincode = (args?.pincode as string)?.trim();
if (!pincode || pincode.length !== 6 || !/^\d{6}$/.test(pincode)) {
return {
content: [
{
type: "text" as const,
text: `Invalid PIN code: '${pincode}'. Must be exactly 6 digits.`,
},
],
};
}
const data = PINCODE_DATA[pincode];
if (data) {
return {
content: [
{
type: "text" as const,
text: JSON.stringify({ pincode, ...data }, null, 2),
},
],
};
}
return {
content: [
{ type: "text" as const, text: `PIN code ${pincode} not found.` },
],
};
}
return {
content: [{ type: "text" as const, text: `Unknown tool: ${name}` }],
};
});
async function main() {
const transport = new StdioServerTransport();
await server.connect(transport);
console.error("Pincode MCP server running on stdio");
}
main().catch(console.error);
Step 3: Build and Test
npx tsc
node dist/index.js # Test that it starts without errors
Connecting to AI Tools
Claude Desktop
Edit ~/Library/Application Support/Claude/claude_desktop_config.json (macOS) or %APPDATA%\Claude\claude_desktop_config.json (Windows):
{
"mcpServers": {
"pincode-lookup": {
"command": "python",
"args": ["/absolute/path/to/server.py"]
}
}
}
Restart Claude Desktop. You should see a hammer icon indicating available tools.
Claude Code
Add to your project's .mcp.json file:
{
"mcpServers": {
"pincode-lookup": {
"command": "python",
"args": ["server.py"],
"cwd": "/path/to/mcp-pincode-server"
}
}
}
For more on Claude Code configuration, see our Claude Code skills guide.
Cursor
In Cursor settings, add your MCP server under the MCP section. Cursor supports the same configuration format as Claude Desktop.
VS Code (GitHub Copilot)
VS Code supports MCP servers through the settings.json configuration. Add your server configuration under the MCP settings section.
Adding Resources to Your Server
Beyond tools (functions the AI calls), MCP servers can expose resources (data the AI can read). Here is how to add a resource to the Python server:
from mcp.types import Resource
@app.list_resources()
async def list_resources() -> list[Resource]:
"""List available resources."""
return [
Resource(
uri="pincode://statistics",
name="PIN Code Database Statistics",
description="Current statistics about the PIN code database",
mimeType="application/json",
)
]
@app.read_resource()
async def read_resource(uri: str) -> str:
"""Read a resource by URI."""
if uri == "pincode://statistics":
return json.dumps({
"total_pincodes": len(PINCODE_DATA),
"states_covered": len(set(d["state"] for d in PINCODE_DATA.values())),
"last_updated": "2026-03-24",
})
raise ValueError(f"Unknown resource: {uri}")
Resources are useful for providing context that the AI can reference during conversations — configuration files, database schemas, documentation, or any structured data.
Real-World MCP Server Patterns
Pattern 1: Database Query Server
Expose read-only database access so AI can query your data:
- Tool:
run_query(sql)with safety constraints (SELECT only, row limit) - Resource: Database schema for context
- Use case: "What were our top 10 customers last quarter?"
Pattern 2: API Gateway Server
Bridge any REST API for AI access:
- Tool:
api_request(endpoint, method, body)with authentication handled server-side - Resource: API documentation and available endpoints
- Use case: Connect AI to your company's internal APIs
Pattern 3: Document Search Server
Give AI access to your knowledge base:
- Tool:
search_documents(query)using vector similarity or full-text search - Resource: Document index and categories
- Use case: "Find our policy on remote work" — This pattern connects naturally to RAG architectures
Pattern 4: Notification and Action Server
Let AI take actions on your behalf:
- Tool:
send_slack_message(channel, text),create_jira_ticket(title, description) - Use case: AI agent that can communicate with your team
Debugging MCP Servers
Common issues and solutions:
| Issue | Cause | Fix | |-------|-------|-----| | Server not appearing in Claude | Config path wrong | Check absolute paths in config JSON | | "Tool not found" error | Server crashed silently | Run server manually to see error output | | Timeout on tool calls | External API slow | Add timeout handling and fallback responses | | JSON parse errors | Invalid output format | Ensure all tool responses return valid TextContent | | Permission errors | File access blocked | Check file paths and permissions |
Debug mode: Run your server with the MCP inspector for interactive testing:
# Python
mcp dev server.py
# TypeScript
npx @modelcontextprotocol/inspector node dist/index.js
The inspector shows every message exchanged between client and server, making it easy to identify issues.
Best Practices
- Validate all inputs — Never trust arguments from the AI. Validate types, ranges, and formats.
- Handle errors gracefully — Return helpful error messages, not stack traces. The AI uses your error messages to retry.
- Keep tools focused — One tool should do one thing well. Prefer multiple small tools over one complex tool.
- Document your tools — The
descriptionfield in your tool definition is what the AI reads to decide when and how to use your tool. Write clear, specific descriptions. - Add timeout handling — External API calls should have timeouts so the AI is not left waiting.
- Security first — MCP servers run with your system permissions. Never expose write access to sensitive systems without authentication and authorization.
- Start local, go remote later — Build and test with stdio transport first. Move to SSE/HTTP transport when you need remote access.
Next Steps
Now that you can build MCP servers, explore these directions:
- Browse existing MCP servers at the MCP server registry — there are hundreds of community-built servers you can install and learn from
- Build an MCP server for your work — Think about what external data or tools would make your AI assistant most useful
- Learn about AI agents that use tools — Our AI agents tutorial covers how AI models decide when and how to use tools like MCP servers
- Explore the MCP specification — The official MCP documentation covers advanced features like sampling, roots, and transport options
- Read our guide on building MCP servers — For additional patterns and advanced configurations, see Build MCP Servers
Community Questions
0No questions yet. Be the first to ask!