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.

mcpsdktoolsserverpythontypescriptFact-checked 2026-08-22
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.

FeatureWho selects itUse it forAvoid
ToolsThe modelQueries and actions with typed argumentsOne giant execute-anything tool or hidden destructive behavior
ResourcesThe applicationRead-only data addressed by URISmuggling write operations into a read contract
PromptsThe personNamed instruction templates surfaced by the hostBusiness logic that belongs in a tool or server policy
Pick the smallest server surface that represents the job.

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

  1. Create an ESM project

    Use Node.js 20 or later, initialize package.json, set type=module, and create a src directory.

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

  3. Build a server factory

    Create one function that returns a new McpServer and registers every capability. Reuse that factory for stdio, HTTP, and tests.

  4. Serve it over stdio

    Pass the factory to serveStdio. The SDK owns transport wiring, version negotiation, discovery, modern result envelopes, and legacy-client compatibility.

Set up the TypeScript project
… scroll to run this session
src/index.ts — minimal stable-v2 stdio server
typescript
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.

Add these registrations inside createServer
typescript
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

  1. Create the environment

    Run uv init weather-python, enter the directory, and run uv add "mcp[cli]". The CLI extra installs the mcp dev, mcp run, mcp install, and mcp version commands used below; the unbounded package resolves to the stable 2.x line.

  2. Import MCPServer

    Use from mcp.server import MCPServer. The old mcp.server.fastmcp import was removed in v2.

  3. Decorate plain functions

    MCPServer derives tool and prompt schemas from type hints and docstrings. Resource URI parameters map to function parameters.

  4. Run the server

    Call mcp.run() for stdio, or mcp.run(transport="streamable-http") for a development HTTP server.

server.py — minimal stable-v2 Python server
python
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()
CommandPurpose
uv run mcp dev server.pyImport the MCPServer object and open the Inspector for interactive testing
uv run mcp run server.pyRun the high-level server directly
uv run mcp install server.pyRegister the server with Claude Desktop; use each other host's own config for that host
uv run mcp versionPrint the installed SDK version
Python v2 development commands.

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.

Serversrc/server.ts
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();});
Setup & runnpm + tsx
npm init -ynpm install @modelcontextprotocol/server zodnpm install -D typescript tsx @types/nodenpx tsx src/server.ts

TypeScript MCP server exposing Tools over stdio.

Choose a language, capability set, and transport, then compare the scaffold with the stable-v2 patterns above.

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.

Smoke-test both examples
… scroll to run this session

A useful test ladder

  1. Schema test

    List the tool and confirm its name, description, required fields, constraints, annotations, and output schema.

  2. Happy-path test

    Call the tool with valid input and assert on structuredContent when one is declared.

  3. Failure-path test

    Send invalid input, simulate an upstream failure, and confirm the model receives an actionable isError result without secrets.

  4. Transport test

    Exercise the exact stdio command or HTTP endpoint the host will use, including environment, working directory, auth, and timeouts.

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

HostRegistrationVerify
Claude Codeclaude mcp add --transport stdio weather -- npx tsx /absolute/path/src/index.tsclaude mcp list and the in-session /mcp screen
Codex CLIcodex mcp add weather -- npx tsx /absolute/path/src/index.tscodex mcp list
Gemini CLIAdd command, args, env, cwd, timeout, and trust under mcpServers in settings.jsonThe in-session /mcp screen
Local stdio registration in three common CLI hosts.

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.

Areav1Stable v2
TypeScript package@modelcontextprotocol/sdk@modelcontextprotocol/server and, when needed, @modelcontextprotocol/client
TypeScript stdioStdioServerTransport plus server.connectserveStdio(serverFactory)
TypeScript input schemaMap of Zod fields in common examplesZod object schema
Python serverFastMCP from mcp.server.fastmcpMCPServer from mcp.server
Wire target2025 session-era protocol2026 stateless core with compatibility support
The most visible v1 to v2 changes.

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.