Building Your Own MCP Server
Connecting a ready-made server takes just a few commands. But sooner or later, the tool you need will exist only inside a corporate system, a legacy REST API, or a custom script. That's when building your own MCP server stops being optional and becomes a necessity.
This article is a journey from a blank slate to a working server. We'll cover FastMCP (Python), the official TypeScript SDK, debugging with MCP Inspector, and the code execution pattern that reshapes the architecture of agent systems.
When You Need Your Own Server
There are already plenty of ready-made servers for GitHub, Postgres, Slack, and dozens of other services — we covered those in the next article. You need your own server when:
- The system is internal (a corporate API, a legacy backend, an internal database)
- A ready-made server exists but doesn't give you the level of control you need (custom authorization, custom data transformation)
- You need business logic on the server side — aggregating from multiple sources, caching, filtering sensitive fields
Choosing a Stack: FastMCP or TypeScript SDK
For Python projects the choice is clear — FastMCP. It's a high-level framework that joined the official Python MCP SDK in 2024 and, according to its authors, now underlies ~70% of all MCP servers. With the @mcp.tool decorator you get automatic JSON Schema generation from type annotations, parameter validation, and documentation — without a single line of boilerplate.
For TypeScript projects, use the official @modelcontextprotocol/sdk from Anthropic. A bit more code to write by hand, but full control over the protocol and native integration with the Node.js ecosystem.
flowchart TD
A[Integration idea] --> B{Ready-made\nserver exists?}
B -- Yes --> C[claude mcp add]
B -- No --> D[Create a server]
D --> E{Stack?}
E -- Python --> F[FastMCP\npip install fastmcp]
E -- TypeScript --> G[@modelcontextprotocol/sdk\nnpm install]
F --> H[Define @mcp.tool\n@mcp.resource @mcp.prompt]
G --> H2[Define registerTool\nwith Zod schema]
H --> I[MCP Inspector\nnpx @modelcontextprotocol/inspector]
H2 --> I
I -- Errors --> H
I -- OK --> J[claude mcp add\nor .mcp.json]
J --> K[Server available in Claude Code]FastMCP: From Zero to a Working Server
Installation is a single line:
pip install fastmcp
# or via uv (recommended for isolation)
uv add fastmcpA minimal server with one tool:
from fastmcp import FastMCP
mcp = FastMCP("my-server")
@mcp.tool
def search_docs(query: str, limit: int = 10) -> list[dict]:
"""Searches documents in the internal knowledge base."""
# Real logic goes here — a DB query, Elasticsearch, etc.
return [{"id": 1, "title": f"Result for '{query}'", "score": 0.95}]
if __name__ == "__main__":
mcp.run() # stdio by defaultNotice that query: str and limit: int = 10 are all you need to generate a schema. FastMCP extracts types from annotations, and the docstring becomes the tool description that the model reads.
#### Tools
Tools are the primary primitive for actions. Rules for a good tool:
- Name — use a verb:
search_,create_,update_,delete_ - Docstring — what it does and when to use it (this is read by Claude, not a human)
- Types — be precise:
str,int,list[str],Literal["asc", "desc"] - Return value — must be JSON-serializable (dict, list, str, int)
from typing import Literal
from pydantic import Field
@mcp.tool
def query_database(
sql: str = Field(description="SELECT query. Read-only."),
timeout_ms: int = Field(default=5000, ge=100, le=30000)
) -> dict:
"""Executes a SQL query against the analytics DB (read-only).
Use this to retrieve aggregated data and reports.
Does not support INSERT/UPDATE/DELETE — SELECT only.
"""
# Implementation
return {"rows": [], "count": 0}Field from Pydantic lets you add a description to each parameter individually — this is especially important for tools with non-obvious semantics.
#### Resources
Resources are read-only data that Claude can read by URI. Unlike tools, they don't perform actions — they only return content.
import json
# Static resource — fixed URI
@mcp.resource("config://app/settings")
def get_settings() -> str:
"""Current application settings in JSON format."""
return json.dumps({"env": "production", "version": "2.1.0"})
# Dynamic resource — parameter in URI
@mcp.resource("user://{user_id}/profile")
def get_user_profile(user_id: str) -> str:
"""User profile by ID."""
# Database query
return json.dumps({"id": user_id, "name": "Alice"})When should you use a resource instead of a tool? A simple rule: if the operation is reading persistently existing data (a config, a profile, a document), use a resource. If the operation changes state or requires parameters that affect execution logic, use a tool.
#### Prompts
Prompts are message templates that the server offers to clients. They're useful for standardizing requests to your system:
from fastmcp import FastMCP
from fastmcp.prompts import Message
@mcp.prompt
def analyze_ticket(ticket_id: str, language: str = "en") -> list[Message]:
"""Template for analyzing a support ticket."""
return [
Message(role="user", content=f"Analyze ticket #{ticket_id} in {language}")
]#### Transport: stdio vs HTTP
# stdio — for local claude mcp add
mcp.run() # or mcp.run(transport="stdio")
# HTTP — for cloud deployment
mcp.run(transport="streamable-http", host="0.0.0.0", port=8000)To connect a stdio server to Claude Code:
claude mcp add --transport stdio my-server -- python /path/to/server.pyFor HTTP:
claude mcp add --transport http my-server http://localhost:8000/mcpTypeScript SDK: An Alternative for Node.js Stacks
If your project is already on TypeScript, the official SDK provides native integration:
npm install @modelcontextprotocol/sdkimport { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
const server = new McpServer({ name: "my-ts-server", version: "1.0.0" });
server.registerTool(
"fetch_metrics",
{
description: "Fetch service metrics for a given period",
inputSchema: z.object({
service: z.string().describe("Service name"),
period: z.enum(["1h", "24h", "7d"]).default("24h")
})
},
async ({ service, period }) => {
// Logic for querying Prometheus, Datadog, etc.
return { content: [{ type: "text", text: JSON.stringify({ service, p99: 45 }) }] };
}
);
const transport = new StdioServerTransport();
await server.connect(transport);Zod is used for validation — the same ecosystem found in most TypeScript projects. The schema is generated automatically from the Zod object.
MCP Inspector: Debugging Before Connecting to Claude
Before connecting your server to Claude Code, you need to make sure it works correctly. MCP Inspector is an interactive debugger launched via npx with no installation required:
# For a Python server
npx @modelcontextprotocol/inspector uv run python server.py
# For a TypeScript server
npx @modelcontextprotocol/inspector node dist/index.js
# For a PyPI package
npx @modelcontextprotocol/inspector uvx my-mcp-serverInspector opens a browser UI with several tabs:
- Tools — a list of all tools, their schemas, a form for manual invocation, and the result. Use this to verify schema correctness and behavior on edge cases
- Resources — a list of resources with the ability to read each one's content
- Prompts — a list of templates, invocable with parameters
- Notifications — all server logs in real time, including protocol errors
A typical debugging workflow:
1. Launch Inspector with the server
2. Open the Tools tab and call each tool with test data
3. Check the schema: correct types, descriptions, required fields
4. Submit invalid input data — make sure the server returns clean errors rather than crashing
5. Only then connect via claude mcp add
Inspector is especially useful when working with template resources: you can pass user://alice/profile directly and see exactly what the server returns.
Code Execution with MCP: A Different Level
This is an architectural pattern that Anthropic describes in a dedicated article. The idea: instead of calling each MCP tool one by one through an agent, give the agent a code execution tool — and let it write programs that orchestrate the calls itself.
Standard approach (N tool calls):
[Claude] → tool_call: get_transcript(meeting_id) → [150k tokens response]
[Claude] → tool_call: create_salesforce_record(data) → ...Code execution approach (2 calls):
[Claude] → tool_call: execute_code("""
import servers.google_drive as drive
import servers.salesforce as sf
transcript = drive.get_transcript(meeting_id)
summary = transcript[:500] # filter BEFORE returning to context
sf.create_record(summary=summary, ...)
""")The result reported by Anthropic: a real-world workflow involving transcript transfer and writing to Salesforce reduced token consumption from ~150,000 to ~2,000 — a 98.7% saving. Intermediate data never leaves the execution environment and never pollutes the context window.
To implement this, the MCP server exposes an execute_code tool that accepts a Python/TypeScript string and runs it in an isolated environment. The other servers are organized as modules (a ./servers/google-drive/, ./servers/salesforce/ folder structure) with importable functions. The agent can progressively discover them — reading directories and loading only the definitions it needs.
This pattern isn't universally applicable: it requires an isolated execution environment (sandbox), explicit control over what code is permitted, and serious security work. But for internal corporate agents handling large data volumes, it represents a fundamental shift in the model.
The Path to Production
Once the server has been verified through Inspector and is working locally, the next steps are:
Dockerizing and deploying:
FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install -r requirements.txt
COPY server.py .
EXPOSE 8000
CMD ["python", "server.py"]Team access via .mcp.json:
{
"mcpServers": {
"my-server": {
"type": "streamable-http",
"url": "${MY_SERVER_URL:-http://localhost:8000}/mcp"
}
}
}The URL via an environment variable lets each developer substitute their own address (local or staging), while the config is committed to git. This is the same pattern from the previous article.
For production authentication, OAuth 2.0 (with the server implementing the PKCE flow) or static API keys via Authorization: Bearer ${TOKEN} in headers is recommended. FastMCP has built-in support for both options.
Logging and monitoring: MCP Inspector shows logs in real time during development. In production, log every tool call with its parameters and execution time — this will help you debug agent behavior, which is otherwise very difficult to reproduce.
See also
- Model Context Protocol: архитектура и основы — the tools/resources/prompts primitives and transports at the specification level
- Подключение MCP-серверов в Claude Code — how to register your own server via
claude mcp addand.mcp.json - Практика: GitHub, базы данных и веб-API через MCP — real-world integrations and security patterns
- Субагенты и контекстная изоляция — when using a subagent is the right choice instead of MCP
- Claude Agent SDK: программная сборка агентов — how the Agent SDK interacts with MCP servers programmatically
- Промпт-кэширование, батчи и оптимизация затрат — code execution with MCP as a way to reduce token costs