
Services
How to build an MCP server: a minimal Node.js MCP server and a Python MCP server, MCP server examples you can run, and how to test them in MCP Inspector before you add auth.

How to build an MCP server, the short version: one process that exposes a tool, a schema the model can read, and a transport a host can speak. Below are the two minimal servers I actually run when I am teaching this — a Node.js MCP server in TypeScript and a Python MCP server — checked against the official SDKs, plus how to hit them with MCP Inspector before you put auth in front.
An MCP server is a small program that advertises tools (and optionally resources) over the Model Context Protocol. A host such as Claude Desktop, Cursor, or your own agent connects, lists the tools, and calls them. You do not embed the tool inside each model SDK.
@modelcontextprotocol/server 2.3.1, McpServer.registerTool, served with serveStdio.mcp 2.3.0, MCPServer and @mcp.tool(), served with mcp.run() (stdio) or Streamable HTTP.What is an MCP server? It is the side of the Model Context Protocol that owns the tools. The model does not import your database client. It asks the server, over JSON-RPC, "what tools do you have?" and then "call this one with these arguments." The server validates the arguments, runs your function, and returns content blocks the model can read.
That split is the whole point. One notes server can serve a TypeScript agent and a Python agent without either of them copying the tool. If you are still deciding whether you need a server or a plain function call, the production guide to MCP has the comparison. If you are new to the loop the model runs around those tools, start with how to build an AI agent.
The examples below are intentionally boring. A notes tool that echoes a string is enough to prove the wiring. I checked them on October 12, 2026 against @modelcontextprotocol/server 2.3.1 and the Python package mcp 2.3.0. The TypeScript handler returned Saved: ship it for a valid call and an input-validation error when text was missing. The Python call_tool path returned the same string. The official walkthroughs are the TypeScript first-server guide and the Python SDK docs.
You need Node.js 20 or later. The SDK ships as ES modules only, so the package has to be "type": "module". tsx runs the file directly, which is what the official tutorial uses, so there is no compile step for the first version.
mkdir notes-mcp && cd notes-mcp
npm init -y
npm pkg set type=module
npm install @modelcontextprotocol/server zod tsx
mkdir src
Create src/index.ts. registerTool takes a name, a config, and a handler. inputSchema is a Zod schema. From that one schema the SDK builds the JSON Schema the model sees, checks arguments before your handler runs, and types the handler arguments.
import { McpServer } from '@modelcontextprotocol/server';
import { serveStdio } from '@modelcontextprotocol/server/stdio';
import * as z from 'zod/v4';
function createServer() {
const server = new McpServer({ name: 'notes', version: '1.0.0' });
server.registerTool(
'add-note',
{
description: 'Save a note and echo it back',
inputSchema: z.object({
text: z.string().describe('The note to save')
})
},
async ({ text }) => ({
content: [{ type: 'text', text: 'Saved: ' + text }]
})
);
return server;
}
void serveStdio(createServer);
console.error('notes MCP server running on stdio');
Run it with npx tsx src/index.ts. Nothing else happens. An stdio server waits on stdin for a client. Stop it with Ctrl+C.
Two details that bite people on the first afternoon. stdout is the protocol channel, so log with console.error. One console.log corrupts the JSON-RPC stream. And pass a factory to serveStdio, not a server you already connected. The SDK builds a fresh instance for the connection.
The current Python SDK is v2. FastMCP was renamed to MCPServer. Importing mcp.server.fastmcp raises ModuleNotFoundError. The install that matches the docs is pip install "mcp[cli]" or uv add "mcp[cli]". The [cli] extra gives you the mcp command, which is how you open Inspector. Python 3.11 or newer.
from mcp.server import MCPServer
mcp = MCPServer("notes")
@mcp.tool()
def add_note(text: str) -> str:
"""Save a note and echo it back."""
return "Saved: " + text
@mcp.resource("greeting://{name}")
def greeting(name: str) -> str:
"""Greet someone by name."""
return "Hello, " + name + "!"
if __name__ == "__main__":
# stdio. A host launches this process and owns its lifetime.
mcp.run()
You did not write a JSON Schema. text: str is the schema. The docstring is the description the model sees. I registered this exact tool on mcp 2.3.0 and list_tools returned add-note with a required string text. Calling it with {"text": "ship it"} returned Saved: ship it.
For a shared endpoint, switch the transport. Streamable HTTP is the one the SDK documents for anything that is not a local process. Defaults I read off MCPServer.run: host 127.0.0.1, port 8000, path /mcp.
if __name__ == "__main__":
mcp.run(transport="streamable-http", json_response=True)
Leave the host on loopback until something in front of it authenticates callers. Binding 0.0.0.0 on a public port with no auth is how a notes tool becomes an open proxy.
The notes tool above is the smallest MCP server example that still teaches the shape. Two more patterns are worth stealing, and then stop.
A tool that only reads — a forecast, a row lookup, a file — can return text and fail with isError: true when the upstream API fails. The official TypeScript tutorial does this with a weather-alert tool: check res.ok, and return an error content block instead of throwing a stack trace at the model. The model can read "HTTP 503" and decide to retry or tell the user. An uncaught exception is a dead session.
The Python greeting://{name} resource is the other official example. Tools are functions the model calls. Resources are data the host can read by URI. Use a resource when the client should pull context (a greeting, a policy, a file). Use a tool when the model should decide to act.
Do not copy the echo tool into a refund, an email send, or a CRM update and call it done. A write needs an idempotency key, a timeout, and a person in the loop when the action is hard to undo. That is the same rule as the ERP operations agent: approval before every write, then a read-back to confirm the system of record actually changed. The protocol will not add those for you.
MCP Inspector is a local UI that lists tools and calls them without a model in the way. Use it before you debug a prompt.
For the TypeScript server, from the project root, the command in the official guide is:
npx @modelcontextprotocol/inspector npx tsx src/index.ts
It opens a browser tab, launches your command, and connects over stdio. Click Connect, open Tools, choose add-note, pass a string, and run it. You should see a text block that starts with Saved:. Then call it with the text field empty. The SDK rejects it before your handler runs. On 2.3.1 that error reads like: Input validation error: Invalid arguments for tool add-note: text: Invalid input: expected string, received undefined.
For the Python file, the SDK's own CLI wraps Inspector:
uv run mcp dev server.py
mcp dev needs npx on your PATH, because Inspector is a Node app. In the Tools tab, call add_note with a string. The form (a required text field) is built from the type hint. Then open Resources and read greeting://World. If either of those fails, fix the server before you point a model at it.
Stdio has no network auth because there is no network. The host starts the process. That is the right transport for Claude Desktop, Cursor, and Inspector on your machine. It is the wrong transport for a server other people call.
On HTTP, createMcpHandler does not read the Authorization header and does not verify tokens. The type definition says so: authInfo is pass-through. You verify the bearer token, then pass the result into handler.fetch. The factory receives it as authInfo and can refuse to register tools for an anonymous caller. This is the shape from the SDK's serving guide:
import { createMcpHandler, McpServer } from '@modelcontextprotocol/server';
const handler = createMcpHandler(({ authInfo }) => {
const server = new McpServer({ name: 'notes', version: '1.0.0' });
server.registerTool(
'whoami',
{ description: 'Name the authenticated caller' },
async () => ({
content: [{ type: 'text', text: (authInfo && authInfo.clientId) || 'anonymous' }]
})
);
return server;
});
The SDK also exports requireBearerAuth. The documented gate is: verify the request, and if the result is a Response, return that challenge; otherwise call handler.fetch(request, { authInfo }). I am not pasting a full OAuth verifier here. Token verification, resource indicators, and scope checks are where teams get this wrong, and they deserve the longer treatment in the MCP production guide.
Three rules I will not budge on, even for a first internal server:
If the server is going to sit behind a real agent — several tools, a pause for approval, evals — that is a build, not a tutorial. I do that as an AI Product Sprint. The framework choice around the server, if you are also picking a runtime, is covered in LangChain vs LangGraph.
An MCP server is a program that exposes tools, and optionally resources and prompts, over the Model Context Protocol. A host connects, lists what the server offers, and calls a tool with JSON arguments. The server validates those arguments and returns content the model can read. It is a protocol boundary, not a chatbot framework.
Install @modelcontextprotocol/server, zod, and tsx in an ES-module package. Create an McpServer, register one tool with a Zod inputSchema, and pass a factory to serveStdio. Run it with npx tsx src/index.ts and exercise it with MCP Inspector before you connect a model. Log to stderr, because stdout is the protocol.
Install mcp[cli] (v2, tested here at 2.3.0). Construct MCPServer from mcp.server, decorate a typed function with @mcp.tool(), and call mcp.run() for stdio. Use mcp.run(transport="streamable-http", json_response=True) when you need an HTTP endpoint. uv run mcp dev server.py opens Inspector.
The smallest useful example is one tool that takes a string and returns text, which is the notes server on this page. The official TypeScript tutorial adds a weather-alert tool that calls a public API and returns isError: true on failure. The Python quickstart adds a greeting://{name} resource. Copy those, then read the production guide before you add a tool that writes.
Not the stdio one, and not the HTTP one until a bearer token is verified outside the tool. createMcpHandler will not do that for you. Start on loopback, confirm the tool in Inspector, then add auth, rate limits, and timeouts. The teams that skip that rebuild the server after the first incident.
How to build an MCP server is a one-file problem. How to run one that other systems trust is not. Get the TypeScript or Python example calling add-note from Inspector, including the case where the arguments are wrong. Then add auth on the HTTP transport and keep write tools behind a person. The rest of the failure modes — schema drift, retries that double-charge, one slow server stalling every agent — are written up in the MCP production guide.
If you want a second pair of eyes on where the server boundary should sit, I do a free 30-minute scoping call. For the wider map of what agents are for, the pillar is what AI agents are, and the catalog of jobs they actually do is the AI agent examples post.
Free 30-minute scoping call. We will sketch the tools, the transport, and where a person has to approve a write.
Book a Scoping CallTayyab is a freelance AI agent developer and founder of Workly. He does research, spec, architecture, UX, and the build — solo, no handoff failures. Ex-Principal PM behind a Fortune 500 AI contact center (40% CSAT lift). He helps founders and SMBs ship production-grade agentic systems end to end.