Lesson 10 of 20

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. :::

Quiz

Module 3: MCP & Agent Skills

Take Quiz