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.
The MCP ecosystem provides two official SDKs that handle all protocol-level concerns:
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.
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.
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}`
}]
};
}
);
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.
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."""
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.
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"]
}
}
}
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}")
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.
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.”
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.
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.
pip install mcp) and TypeScript (@modelcontextprotocol/sdk) handle all protocol plumbing so you can focus on business logic.FastMCP auto-generates tool schemas from type hints and docstrings—the fastest path to a working server.stdio transport for local development and streamable-http for production remote deployments.