env.dev

How to Build an MCP Server: TypeScript & Python (2026)

Build a Model Context Protocol server in TypeScript or Python: SDK quickstart, tools vs resources vs prompts, stdio vs Streamable HTTP, Inspector debugging, security hardening, and Claude/Cursor/VS Code client config.

Last updated:

To build an MCP server, install the official Model Context Protocol SDK (@modelcontextprotocol/sdk on npm or mcp on PyPI), instantiate a server object, register tools with JSON Schema input definitions, and connect a transport — stdio for local clients like Claude Desktop or Streamable HTTP (spec 2025-06-18) for remote deployments. MCP is the de facto protocol for connecting LLMs to tools in 2026: the public registry tracks 13,000+ servers, the TypeScript SDK has crossed 150M downloads, and OpenAI, Google, and Microsoft now ship first-party MCP clients. Security is the hard part — researchers disclosed 30+ MCP-related CVEs in the first four months of 2026, including a CVSS 9.6 RCE in mcp-remote.

What Is an MCP Server?

The Model Context Protocol is a JSON-RPC 2.0 protocol that lets an LLM application (the host) load capabilities — data, actions, templates — from independent servers at runtime. Anthropic introduced MCP in late 2024 as "USB-C for AI apps." In 2026 it's supported by Claude Desktop, Claude Code, Cursor, VS Code, Windsurf, ChatGPT, Zed, and most agent frameworks.

RoleExamplesResponsibility
HostClaude Desktop, Claude Code, Cursor, VS CodeThe LLM app. Owns the model, UI, and consent.
ClientOne per connected server, inside the hostSpeaks JSON-RPC. Negotiates capabilities. Forwards tool calls.
ServerYour process — filesystem, database, API wrapperExposes tools / resources / prompts over a transport.

Servers can expose three kinds of capabilities, and clients can optionally offer three back to servers:

  • Tools — functions the model can call (model-controlled, like function calling).
  • Resources — URI-addressed read-only data the model consumes as context.
  • Prompts — reusable templates surfaced to the user (e.g., slash commands).
  • Sampling (client → server) — the server asks the host's model to complete text on its behalf.
  • Roots (client → server) — the host declares which URIs the server may operate on.
  • Elicitation (client → server) — the server asks the user for additional input mid-call.

Is MCP the Same as Function Calling?

No. Function calling is one feature of one model — a single API exposes the function schemas the host crafted for that request. MCP is a protocol between processes: one server can serve any MCP-capable host, and a host can connect to many servers simultaneously. Tools is only one of six MCP features, and servers are decoupled from the model vendor. In practice, the host translates MCP tool definitions into whatever function-calling format its model expects — OpenAI tools, Anthropic tools, Gemini function declarations — so you write a tool once and it works everywhere.

Which Transport Should You Use?

MCP defines two standard transports. The HTTP+SSE transport from spec version 2024-11-05 was deprecated in favor of Streamable HTTP in 2025-06-18 — don't build new servers on it.

TransportUse WhenNotes
stdioClient spawns your server as a subprocess — Claude Desktop, Claude Code, Cursor config filesJSON-RPC over stdin/stdout, newline-delimited. Never write non-protocol output to stdout.
Streamable HTTPRemote or multi-tenant server; serverless deploys; production SaaSSingle endpoint, HTTP POST for requests, optional SSE for server-initiated messages. Supports resumable streams and session IDs.
HTTP+SSE (legacy)Never for new code — only for backwards compatibility with 2024-11-05 clientsTwo endpoints, long-lived SSE. Replaced by Streamable HTTP.

Rule of thumb: start with stdio — it's zero network config and the Inspector speaks it natively. Ship Streamable HTTP only when you actually need remote access, SSO, or multi-user deployment.

How Do You Build an MCP Server in TypeScript?

Install the SDK and Zod (used for tool input schemas). Node.js 18+ is required; Node 20+ is recommended so you get native fetch and better stream ergonomics.

bash
pnpm add @modelcontextprotocol/sdk zod
pnpm add -D typescript tsx @types/node

The minimum viable server is ~15 lines. This one exposes a single greet tool over stdio:

src/index.ts
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import { z } from 'zod';

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

server.registerTool(
  'greet',
  {
    description: 'Greet someone by name',
    inputSchema: { name: z.string().describe('The person to greet') },
  },
  async ({ name }) => ({
    content: [{ type: 'text', text: `Hello, ${name}!` }],
  }),
);

const transport = new StdioServerTransport();
await server.connect(transport);

Run it directly — tsx src/index.ts — or compile to dist/ and ship a #!/usr/bin/env node shebang. Critical: never write to stdout outside the SDK — any stray console.log corrupts the JSON-RPC stream. Log to stderr (console.error) instead, which clients forward to their debug logs.

How Do You Build an MCP Server in Python?

The Python SDK ships a decorator-based API called FastMCP that reads type hints and docstrings to generate tool schemas. Install with uv (recommended) or pip:

bash
uv add "mcp[cli]"
# or: pip install "mcp[cli]"
server.py
from mcp.server.fastmcp import FastMCP

mcp = FastMCP("greeting-server")


@mcp.tool()
def greet(name: str) -> str:
    """Greet someone by name."""
    return f"Hello, {name}!"


@mcp.resource("greeting://{name}")
def greeting_resource(name: str) -> str:
    """A dynamic greeting resource addressed by name."""
    return f"Hello, {name}!"


if __name__ == "__main__":
    mcp.run(transport="stdio")

Swap transport="stdio" for transport="streamable-http" when you're ready to go remote. FastMCP accepts stateless_http=True and json_response=True for serverless-friendly deployments.

When Should You Use Tools vs Resources vs Prompts?

Each capability type has a distinct control model. Picking the wrong one produces servers the model won't invoke correctly.

CapabilityControlled ByUse For
ToolModel (with user approval)Actions with side effects or computation: search, write, call an API, run a query.
ResourceHost or user (pre-loaded as context)Read-only data addressable by URI: files, DB rows, doc pages, API responses.
PromptUser (invoked like a slash command)Templated workflows: "/summarize", "/review-pr" — multi-step chains users trigger.
SamplingServer calls back to host modelThe server needs LLM reasoning mid-call but doesn't host a model itself.
ElicitationServer asks the userMid-call input requests: "which project?", "confirm this destructive action?".

A practical heuristic: if the model needs to do something, it's a tool. If the model needs to read something, it's a resource. If the user wants a named shortcut, it's a prompt.

How Do You Expose a Remote Server over Streamable HTTP?

Streamable HTTP uses a single endpoint (typically /mcp) that handles both POST (client → server) and GET (optional server → client SSE stream). The SDK ships an Express adapter:

Streamable HTTP server (TypeScript)
import express from 'express';
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
import { randomUUID } from 'node:crypto';

const app = express();
app.use(express.json());

const server = new McpServer({ name: 'remote-server', version: '1.0.0' });
// ... server.registerTool(...) here

const transport = new StreamableHTTPServerTransport({
  sessionIdGenerator: () => randomUUID(),
  // REQUIRED for anything not running behind a trusted reverse proxy:
  allowedHosts: ['localhost:3000', 'mcp.example.com'],
  allowedOrigins: ['https://claude.ai', 'https://cursor.sh'],
  enableDnsRebindingProtection: true,
});

await server.connect(transport);

app.all('/mcp', (req, res) => transport.handleRequest(req, res, req.body));
app.listen(3000, '127.0.0.1');

Three non-negotiable defaults from the spec: validate the Origin header on every request (DNS rebinding attacks trivially pivot through localhost), bind to 127.0.0.1 for local servers rather than 0.0.0.0, and authenticate remote servers (the SDK supports OAuth 2.1 with PKCE via mcpAuthRouter).

What Are the Biggest MCP Security Risks?

MCP's threat model is different from REST: tool descriptions are read by the model, so they're a prompt-injection surface even before anyone calls a tool. Public CVE databases logged 30+ MCP-related disclosures in the first months of 2026, including a CVSS 9.6 command-injection RCE in mcp-remote (downloaded ~500K times) and a zero-interaction prompt injection in Windsurf (CVE-2026-30615).

  • Tool poisoning — a malicious server embeds hidden instructions in a tool's description or parameter names. The model reads those as user intent. Defense: only install servers from sources you trust, and treat the annotations field as untrusted per spec.
  • Command injection — interpolating client-supplied strings into shell commands or SQL. The mcp-remote RCE happened exactly this way. Defense: parameterize every shell or DB call; validate with Zod or Pydantic before dispatch; prefer execFile over shell invocations.
  • DNS rebinding — an attacker's page tricks a browser into hitting http://localhost:3000/mcp. Defense: the Origin/Host allowlist shown above plus localhost-only binding.
  • Over-scoped filesystem / secret access — the server runs as your user. Defense: accept an explicit roots allowlist; never read .env*, ~/.ssh/**, ~/.aws/**, or similar without a whitelist.
  • Confused-deputy / cross-server exfiltration — a poisoned server instructs the model to read data from another server and exfiltrate it. Defense: fine-grained tool approvals on the host side and least-privilege scopes per server.

How Do You Debug an MCP Server?

MCP Inspector is the official debugger — a React UI plus a proxy process that speaks any transport. No install:

bash
# Inspect a TypeScript server you're building
npx @modelcontextprotocol/inspector node dist/index.js

# Inspect a Python server
npx @modelcontextprotocol/inspector uv run server.py

# Inspect a published npm server
npx -y @modelcontextprotocol/inspector npx @modelcontextprotocol/server-filesystem ~/Documents

# Inspect a running Streamable HTTP server
npx @modelcontextprotocol/inspector --transport http --url http://localhost:3000/mcp

The UI opens on http://localhost:6274 with tabs for Tools, Resources, Prompts, and a Notifications pane that shows all JSON-RPC traffic — the single fastest way to verify schemas and catch capability-negotiation errors.

How Do You Install Your Server in a Client?

Client configs are nearly identical — the shape is the same JSON object:

Claude Desktop ~/Library/Application Support/Claude/claude_desktop_config.json (macOS) / %APPDATA%\Claude\claude_desktop_config.json (Windows):

json
{
  "mcpServers": {
    "greeting-server": {
      "command": "node",
      "args": ["/absolute/path/to/dist/index.js"]
    }
  }
}

Claude Code — add via CLI:

bash
claude mcp add greeting-server -- node /absolute/path/to/dist/index.js

# remote server
claude mcp add greeting-server --transport http --url https://mcp.example.com/mcp

Cursor .cursor/mcp.json (project) or ~/.cursor/mcp.json (global), same mcpServers shape as Claude Desktop.

VS Code .vscode/mcp.json:

json
{
  "servers": {
    "greeting-server": {
      "type": "stdio",
      "command": "node",
      "args": ["/absolute/path/to/dist/index.js"]
    }
  }
}

For stdio servers, always use absolute paths — clients spawn subprocesses from their own working directory. Quit and relaunch the client (not just close the window) after editing config.

Why You Shouldn't Build a 50-Tool Monolith

Agent quality degrades sharply once a single context carries too many tool definitions. Internal Anthropic evaluations and independent reproductions consistently show an inflection around 30–40 tools per context, after which models start mis-routing calls, picking overlapping tools, and losing task focus. Anthropic's current guidance is to scope each MCP server to a single coherent product area — a jira-server, a postgres-server, a stripe-server — not a "platform-server" with 80 tools. Name tools with verbs, keep descriptions under 1K tokens, and prefer one multi-arg tool over many near-duplicates.

Frequently Asked Questions

Is MCP the same as OpenAI function calling or Anthropic tool use?

No. Function calling is a per-request API feature of a single model. MCP is a transport-agnostic JSON-RPC protocol between processes. One MCP server works with Claude, GPT, Gemini, Cursor, VS Code, and any other MCP-capable host simultaneously — the host translates MCP tool definitions into whatever function-calling format its model needs.

Do I need MCP if my app already supports tool use?

Only if you want third-party servers or cross-client reuse. If your tools live inside your own app and only your model calls them, direct function calling is simpler. MCP pays off when you want users to plug in tools you didn't write, or when you want your tools to work in multiple clients without re-implementation.

Can an MCP server read my filesystem or run commands?

Yes, if it is built to. A stdio MCP server runs as a child process with your user's permissions, so it can do anything you can do on the command line. Only install servers from trusted authors, review the roots/paths you hand them, and configure filesystem servers with the narrowest directory list that still works.

What transport should I pick — stdio or Streamable HTTP?

Default to stdio. It requires zero network configuration, runs out of the box with the MCP Inspector, and is the transport all major clients prefer for local servers. Only move to Streamable HTTP when you need remote access, multi-tenant isolation, SSO, or serverless deployment.

Why does my MCP server connect but show no tools?

Almost always one of: (1) you wrote non-JSON output to stdout — move all logs to stderr; (2) the client spawned your command with a different working directory, so a relative path failed — use absolute paths in config; (3) your tool registration throws during initialization and the client silently skips the server. Run it through npx @modelcontextprotocol/inspector to see the error.

How do I add authentication to a remote MCP server?

Use OAuth 2.1 with PKCE. The TypeScript SDK ships mcpAuthRouter helpers that implement the MCP authorization spec (2025-06-18), including dynamic client registration and resource indicators. For internal deployments, a signed JWT in the Authorization header plus Origin allowlisting is acceptable; never ship a remote MCP server with no auth.

References

Was this helpful?

Frequently Asked Questions

Is MCP the same as OpenAI function calling or Anthropic tool use?

No. Function calling is a per-request API feature of a single model. MCP is a transport-agnostic JSON-RPC protocol between processes. One MCP server works with Claude, GPT, Gemini, Cursor, VS Code, and any other MCP-capable host simultaneously — the host translates MCP tool definitions into whatever function-calling format its model needs.

Do I need MCP if my app already supports tool use?

Only if you want third-party servers or cross-client reuse. If your tools live inside your own app and only your model calls them, direct function calling is simpler. MCP pays off when you want users to plug in tools you didn't write, or when you want your tools to work in multiple clients without re-implementation.

Can an MCP server read my filesystem or run commands?

Yes, if it is built to. A stdio MCP server runs as a child process with your user's permissions, so it can do anything you can do on the command line. Only install servers from trusted authors, review the roots/paths you hand them, and configure filesystem servers with the narrowest directory list that still works.

What transport should I pick — stdio or Streamable HTTP?

Default to stdio. It requires zero network configuration, runs out of the box with the MCP Inspector, and is the transport all major clients prefer for local servers. Only move to Streamable HTTP when you need remote access, multi-tenant isolation, SSO, or serverless deployment.

Why does my MCP server connect but show no tools?

Almost always one of: (1) you wrote non-JSON output to stdout — move all logs to stderr; (2) the client spawned your command with a different working directory, so a relative path failed — use absolute paths in config; (3) your tool registration throws during initialization and the client silently skips the server. Run it through npx @modelcontextprotocol/inspector to see the error.

How do I add authentication to a remote MCP server?

Use OAuth 2.1 with PKCE. The TypeScript SDK ships mcpAuthRouter helpers that implement the MCP authorization spec (2025-06-18), including dynamic client registration and resource indicators. For internal deployments, a signed JWT in the Authorization header plus Origin allowlisting is acceptable; never ship a remote MCP server with no auth.

Stay up to date

Get notified about new guides, tools, and cheatsheets.