MCP Server Development

Protocols & Interoperability intermediate Building with AI 13 min read

Overview

Building an MCP server means creating a service that exposes tools, resources, and prompts to AI assistants through the standardized Model Context Protocol. An MCP server is the bridge between an AI model and the outside world—it wraps your databases, APIs, file systems, or any other external system behind a protocol that any MCP-compatible AI host can consume.

The official SDKs for Python and TypeScript handle the protocol plumbing (JSON-RPC encoding, transport management, capability negotiation), so server developers can focus on business logic: what tools to expose, what data to surface, and what prompt templates to offer. A basic MCP server can be built and running in under 30 minutes.

MCP server development is a critical skill for AI engineers because it determines what an AI assistant can actually do. The quality, security, and design of your MCP servers directly shapes the capabilities and reliability of every AI application that connects to them.

How It Works

Official SDKs

The MCP ecosystem provides two official SDKs that handle all protocol-level concerns:

Python SDK

Install with pip install mcp. Includes the FastMCP high-level framework that uses Python type hints and docstrings to automatically generate tool schemas. Supports stdio, SSE, and Streamable HTTP transports. Ideal for data-heavy servers, ML pipeline integrations, and rapid prototyping.

TypeScript SDK

Install with npm install @modelcontextprotocol/sdk. Published as @modelcontextprotocol/sdk, it runs on Node.js, Bun, and Deno. Provides MCP server libraries for tools, resources, prompts, Streamable HTTP, stdio, and auth helpers. Ideal when your stack is already JavaScript/TypeScript.

Building Tools

Tools are the most commonly implemented primitive—they let the AI model execute actions. Each tool has a name, a description (which the LLM reads to decide when to use it), and a JSON Schema defining its input parameters.

Here’s a complete Python example using FastMCP:

from mcp.server.fastmcp import FastMCP

# Create a server instance
mcp = FastMCP("weather-server")

@mcp.tool()
def get_weather(city: str, units: str = "celsius") -> str:
    """Get the current weather for a city.

    Args:
        city: The city name to look up weather for
        units: Temperature units - 'celsius' or 'fahrenheit'
    """
    # FastMCP auto-generates the JSON Schema from type hints
    # The docstring becomes the tool description the LLM sees
    weather_data = fetch_weather_api(city, units)
    return f"Weather in {city}: {weather_data['temp']}°, {weather_data['condition']}"

@mcp.tool()
def get_forecast(city: str, days: int = 5) -> str:
    """Get a multi-day weather forecast for a city."""
    forecast = fetch_forecast_api(city, days)
    return format_forecast(forecast)

The equivalent in TypeScript:

import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { z } from "zod";

const server = new McpServer({
  name: "weather-server",
  version: "1.0.0"
});

server.tool(
  "get_weather",
  "Get the current weather for a city",
  {
    city: z.string().describe("The city name"),
    units: z.enum(["celsius", "fahrenheit"]).default("celsius")
  },
  async ({ city, units }) => {
    const data = await fetchWeatherAPI(city, units);
    return {
      content: [{
        type: "text",
        text: `Weather in ${city}: ${data.temp}°, ${data.condition}`
      }]
    };
  }
);

Exposing Resources

Resources provide read-only data that clients can surface to the model. Unlike tools (which the model invokes), resources are typically listed and selected by the host application or user. Each resource has a URI, a name, and a MIME type.

@mcp.resource("config://app/settings")
def get_app_settings() -> str:
    """Current application configuration settings."""
    return json.dumps(load_settings(), indent=2)

@mcp.resource("users://{user_id}/profile")
def get_user_profile(user_id: str) -> str:
    """User profile information."""
    profile = db.get_user(user_id)
    return json.dumps(profile)

Resource URIs can be static (config://app/settings) or templated (users://{user_id}/profile). Templated resources allow clients to request specific instances by filling in the URI parameters.

Defining Prompts

Prompts are reusable message templates that standardize common interaction patterns. They can accept arguments and return structured message sequences.

@mcp.prompt()
def code_review(language: str, code: str) -> str:
    """Generate a structured code review for the given code."""
    return f"""Please review the following {language} code for:
1. Bugs and potential errors
2. Security vulnerabilities
3. Performance issues
4. Code style and best practices

Code to review:
```{language}
\{code\}
```

Provide specific, actionable feedback with line references."""

Transport Configuration

Servers must be configured to use the appropriate transport for their deployment scenario:

# Local server via stdio (for development / local tools)
if __name__ == "__main__":
    mcp.run(transport="stdio")

# Remote server via Streamable HTTP (for production)
if __name__ == "__main__":
    mcp.run(transport="streamable-http", host="0.0.0.0", port=8080)

For local development, stdio is simplest—the AI host spawns your server as a child process. For production deployments where the server runs on a remote machine, use streamable-http, which exposes a single HTTP endpoint supporting both POST requests and optional SSE streaming.

Testing with MCP Inspector

The MCP ecosystem provides the MCP Inspector, a development tool that lets you test your server’s tools, resources, and prompts without needing a full AI client. After each change, you can verify that capabilities register correctly, test tool invocations with sample inputs, and inspect the JSON-RPC messages flowing between client and server.

# Install and run the inspector
npx @modelcontextprotocol/inspector

# Or test with Claude Desktop by adding to config:
# ~/.config/claude/claude_desktop_config.json
{
  "mcpServers": {
    "weather": {
      "command": "python",
      "args": ["weather_server.py"]
    }
  }
}

Error Handling and Logging

MCP servers should return structured errors using the JSON-RPC error format. The SDK provides helpers for common error types. Servers can also emit log messages at different severity levels that clients can capture for debugging:

@mcp.tool()
def query_database(sql: str) -> str:
    """Execute a read-only SQL query."""
    if not sql.strip().upper().startswith("SELECT"):
        raise ValueError("Only SELECT queries are allowed")

    try:
        results = db.execute(sql)
        return json.dumps(results)
    except DatabaseError as e:
        # MCP SDK converts exceptions to proper JSON-RPC errors
        raise RuntimeError(f"Database query failed: {e}")

Practical Applications

Database Query Server

One of the most common MCP servers wraps a database, exposing read-only query capabilities as tools and table schemas as resources. The AI can inspect the schema to understand the data model, then write and execute SQL queries—all through the standardized MCP interface.

Internal Knowledge Base Server

An MCP server can wrap a company’s Confluence wiki, Notion workspace, or internal documentation system. Tools provide search capabilities, resources expose document contents, and prompts offer templates for common queries like “summarize the onboarding docs” or “find the runbook for service X.”

DevOps Tooling Server

MCP servers that wrap GitHub, Jira, PagerDuty, and monitoring systems let AI assistants participate in development workflows—creating issues, reviewing PRs, checking deployment status, and querying metrics dashboards.

Multi-Server Composition

In production, AI hosts typically connect to multiple MCP servers simultaneously. A developer assistant might connect to servers for GitHub, the project database, CI/CD pipelines, and documentation—each server independently maintained and deployed, all accessible through the same protocol.

Key Takeaways

Further Reading