Module 9: MCP — Building Your Own Servers
Building your own MCP servers.
What is MCP?
The Model Context Protocol (MCP) is an open standard created by Anthropic for connecting AI agents to external tools and data sources. Before MCP, every AI application had to build custom integrations for every tool — a Slack integration for one app, a completely different Slack integration for another. MCP standardises these connections so that a tool integration built once works with any MCP-compatible host.
Think of it as USB-C for AI. Just as USB-C provides a universal connector between devices and peripherals, MCP provides a universal protocol between AI agents and their tools. Anthropic donated MCP to the Linux Foundation in December 2025, and it has grown to over 97 million monthly SDK downloads across all languages.
MCP matters because it solves the N x M integration problem. Without MCP, N AI applications need M custom integrations each, yielding N*M total integrations. With MCP, each tool builds one server and each application builds one client, yielding N+M total integrations.
Before USB, every printer, scanner, and keyboard had a different connector. You needed a specific cable and driver for each combination. USB unified this — any device works with any computer. MCP does the same for AI tools: build one MCP server for your database, and it works with Claude Desktop, VS Code, Cursor, and any other MCP host.
| Without MCP | With MCP |
|---|---|
| Custom integration per app per tool | One server per tool, works everywhere |
| Different APIs, formats, auth methods | Standardised protocol and message format |
| Tight coupling between agent and tools | Loose coupling via protocol |
| Hard to share tools between projects | Community registry of reusable servers |
Architecture
MCP follows a client-server architecture with three distinct roles. Understanding these roles is key to building and integrating MCP servers correctly.
MCP Host
The AI application the user interacts with. Examples: Claude Desktop, VS Code with Copilot, Cursor, or your own custom agent application. The host manages one or more MCP clients.
MCP Client
A protocol client (provided by the MCP SDK) that maintains a 1:1 connection with a single MCP server. The host creates one client per server it connects to.
MCP Server
A lightweight programme that exposes capabilities via the MCP protocol. Each server provides some combination of tools, resources, and prompts.
The communication flow is: User → Host → Client → Server → External System. The LLM inside the host decides which tools to call, the client sends the request to the appropriate server, the server executes the action and returns results, and the client passes them back to the host.
MCP Primitives
| Primitive | Description | Control | Example |
|---|---|---|---|
| Tools | Functions the model can call to take actions | Model-controlled (LLM decides when to call) | Send email, query database, create file |
| Resources | Read-only data the application can access | Application-controlled (host decides when to read) | File contents, database schemas, API docs |
| Prompts | Reusable prompt templates with arguments | User-controlled (user selects which prompt) | "Summarise this document", "Review this PR" |
MCP supports two transport mechanisms: stdio (standard input/output, for local servers) and SSE (Server-Sent Events over HTTP, for remote servers). Stdio is simpler and more common for local development; SSE enables hosting MCP servers as web services.
Building an MCP Server
The Python MCP SDK provides FastMCP, a high-level API that makes building servers as easy as writing decorated Python functions. Each decorated function becomes a tool, resource, or prompt that any MCP host can discover and use.
pip install mcp[cli]
A Complete Example Server
from mcp.server.fastmcp import FastMCP
import json
import sqlite3
from datetime import datetime
# Create the MCP server
mcp = FastMCP("Company Tools", version="1.0.0")
# --- TOOLS (model-controlled) ---
@mcp.tool()
def get_stock_price(symbol: str) -> str:
"""Get the current stock price for a given ticker symbol.
Args:
symbol: Stock ticker symbol, e.g., 'AAPL', 'GOOGL', 'MSFT'
Returns:
Current price and daily change as a formatted string.
"""
# In production, call a real API like Alpha Vantage or Yahoo Finance
prices = {
"AAPL": {"price": 227.50, "change": +1.2},
"GOOGL": {"price": 178.30, "change": -0.8},
"MSFT": {"price": 445.20, "change": +2.1},
}
data = prices.get(symbol.upper())
if not data:
return f"Unknown symbol: {symbol}"
sign = "+" if data["change"] >= 0 else ""
return f"{symbol.upper()}: ${data['price']:.2f} ({sign}{data['change']}%)"
@mcp.tool()
def query_employees(department: str = "", role: str = "") -> str:
"""Search the employee directory by department and/or role.
Args:
department: Filter by department name (optional)
role: Filter by job role (optional)
Returns:
JSON array of matching employees with name, role, department, email.
"""
conn = sqlite3.connect("company.db")
conn.row_factory = sqlite3.Row
query = "SELECT name, role, department, email FROM employees WHERE 1=1"
params = []
if department:
query += " AND department LIKE ?"
params.append(f"%{department}%")
if role:
query += " AND role LIKE ?"
params.append(f"%{role}%")
rows = conn.execute(query, params).fetchall()
conn.close()
return json.dumps([dict(r) for r in rows], indent=2)
@mcp.tool()
def create_calendar_event(
title: str, date: str, time: str, duration_minutes: int = 60
) -> str:
"""Create a new calendar event.
Args:
title: Event title
date: Date in YYYY-MM-DD format
time: Start time in HH:MM format (24-hour)
duration_minutes: Duration in minutes (default: 60)
"""
# In production, call Google Calendar or Outlook API
return json.dumps({
"status": "created",
"event": {"title": title, "date": date, "time": time,
"duration": duration_minutes}
})
# Run the server
if __name__ == "__main__":
mcp.run(transport="stdio")
Write detailed docstrings for every tool. The MCP SDK extracts these as the tool description that the LLM reads. Include argument descriptions, expected formats, and return value documentation. The quality of your docstrings directly determines how well the LLM uses your tools.
MCP tools must return strings. If your function returns a dict or list, serialise it to JSON first. The MCP protocol communicates via text, so non-string return values will cause errors.
Resources and Prompts
Beyond tools, MCP servers expose resources (read-only data) and prompts (reusable templates). Resources let the host application browse and read data without the LLM deciding to do so. Prompts provide pre-built interaction patterns that users can trigger.
Resources
@mcp.resource("company://docs/{doc_name}")
def get_document(doc_name: str) -> str:
"""Read a company document by name.
Exposes documents like:
company://docs/vacation-policy
company://docs/expense-guidelines
"""
docs_dir = "./company_docs"
path = f"{docs_dir}/{doc_name}.md"
try:
with open(path, "r") as f:
return f.read()
except FileNotFoundError:
return f"Document '{doc_name}' not found."
@mcp.resource("company://metrics/dashboard")
def get_dashboard_metrics() -> str:
"""Get current company metrics dashboard data."""
# In production, query your metrics database
return json.dumps({
"revenue_mtd": "$2.4M",
"active_users": 15420,
"uptime": "99.97%",
"open_tickets": 23
}, indent=2)
Prompts
from mcp.server.fastmcp import Context
@mcp.prompt()
def review_code(language: str, code: str) -> str:
"""Review code for bugs, style issues, and improvements."""
return f"""Please review the following {language} code for:
1. Bugs and potential errors
2. Style and readability issues
3. Performance improvements
4. Security concerns
Code to review:
```{language}
{code}
```
Provide specific, actionable feedback with code examples for each issue found."""
@mcp.prompt()
def summarise_meeting(notes: str) -> str:
"""Summarise meeting notes into action items and decisions."""
return f"""Analyse these meeting notes and produce:
1. A 2-3 sentence summary
2. Key decisions made
3. Action items with owners and deadlines
Meeting notes:
{notes}"""
The difference between tools, resources, and prompts is about who controls them. Tools are model-controlled (the LLM decides to call them). Resources are application-controlled (the host reads them as needed). Prompts are user-controlled (the user selects them from a menu).
Connecting to Claude Desktop
Claude Desktop is the most common MCP host for testing and personal use. To connect your server, you edit the Claude Desktop configuration file and add your server definition.
Configuration File Location
- macOS:
~/Library/Application Support/Claude/claude_desktop_config.json - Windows:
%APPDATA%\Claude\claude_desktop_config.json
Configuration Format
{
"mcpServers": {
"company-tools": {
"command": "python",
"args": ["/absolute/path/to/server.py"],
"env": {
"DATABASE_URL": "sqlite:///company.db",
"API_KEY": "your-api-key-here"
}
},
"github": {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-github"],
"env": {
"GITHUB_PERSONAL_ACCESS_TOKEN": "ghp_xxxxx"
}
}
}
}
After saving the configuration and restarting Claude Desktop, your tools will appear in the tools menu (the hammer icon). Claude will automatically discover all tools, resources, and prompts exposed by your server.
Use the MCP Inspector to debug your server before connecting it to Claude Desktop. Run mcp dev server.py to launch an interactive web UI that lets you test every tool, resource, and prompt directly.
Testing with the MCP Inspector
# Install the CLI tools
pip install "mcp[cli]"
# Launch the inspector (opens a web UI)
mcp dev server.py
# Or test from the command line
mcp run server.py
The MCP Ecosystem
The MCP ecosystem has grown rapidly since the protocol's release. There is a large library of pre-built servers for common integrations, SDKs in multiple languages, and a growing community of developers building and sharing servers.
Pre-Built Servers
GitHub, Slack, PostgreSQL, SQLite, filesystem, Google Drive, Brave Search, Puppeteer, and dozens more available from the official registry.
Multi-Language SDKs
Official SDKs for Python, TypeScript, Java, C#, Go, and Kotlin. Build MCP servers in your language of choice.
MCP Inspector
Built-in debugging tool. Interact with your server's tools, resources, and prompts through a web UI before connecting to a host.
Community Registry
Browse and share MCP servers at the official registry. Install community servers with a single command.
| Pre-Built Server | Capabilities | Install |
|---|---|---|
| Filesystem | Read, write, search files in allowed directories | npx @modelcontextprotocol/server-filesystem |
| GitHub | Repos, issues, PRs, code search | npx @modelcontextprotocol/server-github |
| PostgreSQL | Query databases, inspect schemas | npx @modelcontextprotocol/server-postgres |
| Brave Search | Web search via Brave API | npx @modelcontextprotocol/server-brave-search |
| Puppeteer | Browser automation, screenshots | npx @modelcontextprotocol/server-puppeteer |
When building production MCP servers, always validate inputs, handle errors gracefully, and consider security implications. An MCP server is essentially an API that an AI can call — apply the same security practices you would to any API (authentication, rate limiting, input sanitisation, principle of least privilege).