First Principles · 16 min mission
Build an MCP Server From Scratch
Write a working MCP server in TypeScript or Python, test it with the Inspector, and register it with your agent.
On this page
An MCP server is a focused program that exposes tools, resources, and prompts through a standard JSON-RPC interface. Build the contract once and any compatible host can connect over a local subprocess or a remote HTTP endpoint.
This guide targets the final 2026-07-28 protocol and the stable v2 SDK lines. The TypeScript SDK is now split into scoped server and client packages. The Python high-level server is now MCPServer rather than FastMCP.
| Feature | Who selects it | Use it for | Avoid |
|---|---|---|---|
| Tools | The model | Queries and actions with typed arguments | One giant execute-anything tool or hidden destructive behavior |
| Resources | The application | Read-only data addressed by URI | Smuggling write operations into a read contract |
| Prompts | The person | Named instruction templates surfaced by the host | Business logic that belongs in a tool or server policy |
Choose the transport
The protocol has two standard transports. stdio is the fastest route for a local server: the host launches the process and exchanges newline-delimited messages over stdin and stdout. Streamable HTTP is the deployment route: each request is an HTTP POST to one endpoint, with a JSON response or a request-scoped SSE stream.
The 2026 protocol core has no mandatory initialize handshake or protocol session ID. Do not copy 2025-era examples that require Mcp-Session-Id, a GET event stream, or Last-Event-ID replay.
stdio vs. Streamable HTTP
stdio
Best for one local user and direct machine access. No listening port. The host owns process lifecycle and supplies environment variables. stdout is reserved for JSON-RPC; logs go to stderr.
Streamable HTTP
Best for shared or deployed services. One endpoint can serve many clients. Add authentication, authorization, Host/Origin validation, TLS, rate limits, and an external store for durable business state.
Build a TypeScript v2 server
Create an ESM project
Use Node.js 20 or later, initialize package.json, set type=module, and create a src directory.
Install the server SDK
Run npm install @modelcontextprotocol/server zod tsx, then npm install -D typescript @types/node. The stable v2 server package is ESM-only and accepts a Zod v4 object schema. TypeScript 6 projects must include node in compilerOptions.types.
Build a server factory
Create one function that returns a new McpServer and registers every capability. Reuse that factory for stdio, HTTP, and tests.
Serve it over stdio
Pass the factory to serveStdio. The SDK owns transport wiring, version negotiation, discovery, modern result envelopes, and legacy-client compatibility.
import { McpServer } from '@modelcontextprotocol/server';
import { serveStdio } from '@modelcontextprotocol/server/stdio';
import * as z from 'zod/v4';
function createServer(): McpServer {
const server = new McpServer({ name: 'weather', version: '1.0.0' });
server.registerTool(
'get-alerts',
{
description: 'Get active weather alerts for a US state',
inputSchema: z.object({
state: z.string().length(2).describe('Two-letter state code, for example CA'),
}),
},
async ({ state }) => {
const code = state.toUpperCase();
// Replace this line with a real API call.
return {
content: [{ type: 'text', text: 'Active alerts for ' + code }],
};
},
);
return server;
}
const handle = serveStdio(createServer);
console.error('Weather MCP server is listening on stdio');
process.on('SIGINT', () => {
void handle.close();
});Add a resource and a prompt
Register static or templated resources with registerResource, and named message templates with registerPrompt. A resource callback returns contents with text or a base64 blob. A prompt callback returns one or more user or assistant messages.
server.registerResource(
'config',
'config://app',
{
title: 'Application Config',
description: 'Public runtime configuration',
mimeType: 'text/plain',
},
async (uri) => ({
contents: [{ uri: uri.href, text: 'region=eu-west-1' }],
}),
);
server.registerPrompt(
'review-code',
{
title: 'Code Review',
description: 'Review code for correctness and risk',
argsSchema: z.object({
code: z.string().describe('Code to review'),
}),
},
({ code }) => ({
messages: [
{
role: 'user' as const,
content: { type: 'text' as const, text: 'Review this code:
' + code },
},
],
}),
);Build the same server with Python v2
Create the environment
Run
uv init weather-python, enter the directory, and runuv add "mcp[cli]". The CLI extra installs themcp dev,mcp run,mcp install, andmcp versioncommands used below; the unbounded package resolves to the stable 2.x line.Import MCPServer
Use from mcp.server import MCPServer. The old mcp.server.fastmcp import was removed in v2.
Decorate plain functions
MCPServer derives tool and prompt schemas from type hints and docstrings. Resource URI parameters map to function parameters.
Run the server
Call mcp.run() for stdio, or mcp.run(transport="streamable-http") for a development HTTP server.
import sys
from typing import Annotated
from mcp.server import MCPServer
from pydantic import Field
mcp = MCPServer('weather')
@mcp.tool()
async def get_alerts(
state: Annotated[
str,
Field(min_length=2, max_length=2, pattern=r'^[A-Za-z]{2}$'),
],
) -> str:
"""Get active weather alerts for a two-letter US state code."""
return f'Active alerts for {state.upper()}'
@mcp.resource('greeting://{name}')
def greeting(name: str) -> str:
"""Return a personalized greeting."""
return f'Hello, {name}!'
@mcp.prompt()
def review_code(code: str) -> str:
"""Create a code-review prompt."""
return f'Review this code for correctness and risk:
{code}'
if __name__ == '__main__':
print('Weather MCP server is listening on stdio', file=sys.stderr)
mcp.run()| Command | Purpose |
|---|---|
| uv run mcp dev server.py | Import the MCPServer object and open the Inspector for interactive testing |
| uv run mcp run server.py | Run the high-level server directly |
| uv run mcp install server.py | Register the server with Claude Desktop; use each other host's own config for that host |
| uv run mcp version | Print the installed SDK version |
Scaffold an MCP server
MCP server scaffold
Stand up a minimal MCP server. Pick a language, choose which primitives it exposes — tools, resources, prompts — and a transport. The entrypoint and setup commands regenerate together against the stable v2 SDKs and the final 2026-07-28 protocol.
Language
The SDK and entrypoint file differ — the scaffold adapts to your choice.
Primitives
What the server exposes to the host. Pick at least one — most servers start with tools.
Transport
How the host reaches the server. stdio launches it as a subprocess; Streamable HTTP serves it over a URL.
Local subprocess over stdin/stdout. The default for desktop hosts.
import { McpServer } from "@modelcontextprotocol/server";import { serveStdio } from "@modelcontextprotocol/server/stdio";import * as z from "zod/v4"; function createServer(): McpServer { const server = new McpServer({ name: "my-mcp-server", version: "1.0.0", }); // A tool: a function the model can call to take action. server.registerTool( "echo", { title: "Echo", description: "Echo a message back to the caller.", inputSchema: z.object({ message: z.string().describe("Text to echo"), }), }, async ({ message }) => ({ content: [{ type: "text", text: `You said: ${message}` }], }), ); return server;} const handle = serveStdio(createServer);// Log to stderr only — stdout carries the JSON-RPC protocol.console.error("MCP server running on stdio"); process.on("SIGINT", () => { void handle.close();});npm init -ynpm install @modelcontextprotocol/server zodnpm install -D typescript tsx @types/nodenpx tsx src/server.tsTypeScript MCP server exposing Tools over stdio.
Test the contract before registering a host
The official MCP Inspector is the fastest manual smoke test. It launches a stdio command, discovers the server, renders forms from schemas, and lets you list and invoke capabilities. For automated tests, both stable SDKs support an in-process client path, so you can assert on structured results without opening a port or mocking the protocol.
A useful test ladder
Schema test
List the tool and confirm its name, description, required fields, constraints, annotations, and output schema.
Happy-path test
Call the tool with valid input and assert on structuredContent when one is declared.
Failure-path test
Send invalid input, simulate an upstream failure, and confirm the model receives an actionable isError result without secrets.
Transport test
Exercise the exact stdio command or HTTP endpoint the host will use, including environment, working directory, auth, and timeouts.
Policy test
Verify allowlists, approval gates, idempotency keys, audit records, and tenant or user isolation before enabling writes.
Register the server with a host
A stdio registration needs a name, a launch command, its arguments, and any environment variables. A remote registration needs an HTTPS endpoint and its authorization flow. Host configuration commands are not protocol features, so verify each host's current syntax before automation.
| Host | Registration | Verify |
|---|---|---|
| Claude Code | claude mcp add --transport stdio weather -- npx tsx /absolute/path/src/index.ts | claude mcp list and the in-session /mcp screen |
| Codex CLI | codex mcp add weather -- npx tsx /absolute/path/src/index.ts | codex mcp list |
| Gemini CLI | Add command, args, env, cwd, timeout, and trust under mcpServers in settings.json | The in-session /mcp screen |
Move to HTTP only when the service boundary is real
In TypeScript v2, createMcpHandler takes your server factory and returns a web-standard handler; framework adapters connect that handler to Express, Fastify, Hono, or Node. The factory runs once per HTTP request, so it should be cheap and side-effect-free. Put pools and caches outside it, and pass caller identity through request context.
In Python v2, mcp.run(transport="streamable-http") is suitable for local development. For production, mount the ASGI application in your own server so TLS, worker count, timeouts, connection limits, and graceful shutdown remain application concerns.
Migrating a v1 server
TypeScript v2 replaces the monolithic package, map-shaped input schemas, and manual transport connection. Import McpServer from @modelcontextprotocol/server, use a Zod object schema, serve stdio with serveStdio, and serve HTTP with createMcpHandler.
Python v2 renames FastMCP to MCPServer and moves the import. Decorator-based servers otherwise keep the familiar tool, resource, and prompt shape. Run the official migration guides and keep only the legacy-client compatibility path you can test.
| Area | v1 | Stable v2 |
|---|---|---|
| TypeScript package | @modelcontextprotocol/sdk | @modelcontextprotocol/server and, when needed, @modelcontextprotocol/client |
| TypeScript stdio | StdioServerTransport plus server.connect | serveStdio(serverFactory) |
| TypeScript input schema | Map of Zod fields in common examples | Zod object schema |
| Python server | FastMCP from mcp.server.fastmcp | MCPServer from mcp.server |
| Wire target | 2025 session-era protocol | 2026 stateless core with compatibility support |
Knowledge check
A TypeScript v1 tutorial tells you to install @modelcontextprotocol/sdk and connect McpServer to a new StdioServerTransport. What is the stable-v2 starting point?
Reach the end and this star joins your charted sky.