MCP & Agent Skills
Building MCP Servers
4 min read
Building an MCP server lets you expose any functionality—APIs, databases, custom logic—to AI models. Let's build one from scratch.
Minimal MCP Server (Python)
# server.py
from mcp.server import Server
from mcp.types import Tool, TextContent
app = Server("my-tools")
@app.list_tools()
async def list_tools():
return [
Tool(
name="greet",
description="Generate a greeting",
inputSchema={
"type": "object",
"properties": {
"name": {"type": "string", "description": "Name to greet"}
},
"required": ["name"]
}
)
]
@app.call_tool()
async def call_tool(name: str, arguments: dict):
if name == "greet":
return [TextContent(type="text", text=f"Hello, {arguments['name']}!")]
raise ValueError(f"Unknown tool: {name}")
if __name__ == "__main__":
import asyncio
from mcp.server.stdio import stdio_server
asyncio.run(stdio_server(app))
Running Your Server
# Install MCP SDK
pip install mcp
# Run directly
python server.py
# Or use with Claude Desktop (claude_desktop_config.json)
{
"mcpServers": {
"my-tools": {
"command": "python",
"args": ["server.py"]
}
}
}
Adding Resources
Expose data the model can read:
from mcp.types import Resource
@app.list_resources()
async def list_resources():
return [
Resource(
uri="config://settings",
name="App Settings",
mimeType="application/json"
)
]
@app.read_resource()
async def read_resource(uri: str):
if uri == "config://settings":
return json.dumps({"theme": "dark", "language": "en"})
raise ValueError(f"Unknown resource: {uri}")
Adding Prompts
Reusable prompt templates:
from mcp.types import Prompt, PromptArgument
@app.list_prompts()
async def list_prompts():
return [
Prompt(
name="explain_code",
description="Explain code in simple terms",
arguments=[
PromptArgument(name="language", description="Programming language"),
PromptArgument(name="level", description="Expertise level")
]
)
]
@app.get_prompt()
async def get_prompt(name: str, arguments: dict):
if name == "explain_code":
return {
"messages": [{
"role": "user",
"content": f"Explain this {arguments['language']} code for a {arguments['level']} developer."
}]
}
Real-World Example: Database MCP Server
import asyncpg
from mcp.server import Server
from mcp.types import Tool, TextContent
app = Server("postgres-tools")
pool = None
async def get_pool():
global pool
if pool is None:
pool = await asyncpg.create_pool("postgresql://localhost/mydb")
return pool
@app.list_tools()
async def list_tools():
return [
Tool(
name="query_database",
description="Execute a read-only SQL query",
inputSchema={
"type": "object",
"properties": {
"sql": {"type": "string", "description": "SQL SELECT query"}
},
"required": ["sql"]
}
)
]
@app.call_tool()
async def call_tool(name: str, arguments: dict):
if name == "query_database":
sql = arguments["sql"]
# Security: Only allow SELECT
if not sql.strip().upper().startswith("SELECT"):
return [TextContent(type="text", text="Error: Only SELECT queries allowed")]
pool = await get_pool()
async with pool.acquire() as conn:
rows = await conn.fetch(sql)
result = [dict(row) for row in rows]
return [TextContent(type="text", text=json.dumps(result, indent=2))]
TypeScript Alternative
// server.ts
import { Server } from "@modelcontextprotocol/sdk/server";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio";
const server = new Server({
name: "my-tools",
version: "1.0.0"
});
server.setRequestHandler("tools/list", async () => ({
tools: [{
name: "calculate",
description: "Perform calculations",
inputSchema: {
type: "object",
properties: {
expression: { type: "string" }
}
}
}]
}));
server.setRequestHandler("tools/call", async (request) => {
if (request.params.name === "calculate") {
const result = eval(request.params.arguments.expression);
return { content: [{ type: "text", text: String(result) }] };
}
});
new StdioServerTransport(server).start();
Nerd Note: Keep MCP servers focused. One server for database, another for file ops. Composability beats monoliths.
Next: Building extensible agent skills. :::