On this page
To create an MCP server, use an official SDK, define a few narrow tools with input schemas, run it locally over stdio, and test it with the MCP Inspector. That takes minutes. The work that matters comes after: validating inputs, choosing the transport, adding sign-in for anything remote, and giving each tool the least access that works.
The code below follows the official Build an MCP server tutorial, and the security rules are quoted from the current MCP specification (2026-07-28).
1. Pick a language and SDK
The official tutorial covers Python, TypeScript, Java, Kotlin and C#. Python and TypeScript have the most examples. Two version notes from the tutorial:
- Python: "You must use the Python MCP SDK 2.0.0 or higher." It uses the
MCPServerclass. - TypeScript: the server package is
@modelcontextprotocol/server, withzodfor input schemas.
2. A minimal server in Python
Set up a project with uv:
uv init my-server
cd my-server
uv venv
uv add "mcp[cli]"
Then server.py, following the tutorial's pattern: the decorator turns a typed
function and its docstring into a tool definition.
from mcp.server import MCPServer
mcp = MCPServer("my-server")
@mcp.tool()
async def get_ticket_status(ticket_id: str) -> str:
"""Get the status of a support ticket.
Args:
ticket_id: The ticket ID, e.g. T-1042
"""
# Look the ticket up in your own system here, with a read-only credential.
return f"Ticket {ticket_id}: open"
if __name__ == "__main__":
mcp.run(transport="stdio")
Run it with uv run server.py.
3. A minimal server in TypeScript
npm init -y
npm install @modelcontextprotocol/server zod
npm install -D @types/node typescript
The tutorial builds the server inside a createServer() function and hands it
to serveStdio:
import { McpServer } from "@modelcontextprotocol/server";
import { serveStdio } from "@modelcontextprotocol/server/stdio";
import { z } from "zod";
function createServer() {
const server = new McpServer({ name: "my-server", version: "1.0.0" });
server.registerTool(
"get_ticket_status",
{
description: "Get the status of a support ticket",
inputSchema: z.object({
ticketId: z.string().regex(/^T-\d{1,8}$/).describe("Ticket ID, e.g. T-1042"),
}),
},
async ({ ticketId }) => ({
content: [{ type: "text", text: `Ticket ${ticketId}: open` }],
}),
);
return server;
}
serveStdio(createServer, {
onerror: (error) => console.error("Server error:", error),
});
The tutorial's package.json sets "type": "module" and builds with tsc.
One rule for both languages. For stdio servers, the tutorial is blunt:
"Never write to stdout. Writing to stdout will corrupt the JSON-RPC messages and
break your server." Log to stderr instead: Python's logging module, or
console.error() in TypeScript.
4. Test it with the MCP Inspector
The MCP Inspector is
the reference testing tool. It needs Node 22.19.0 or newer and runs with
npx:
npx @modelcontextprotocol/inspector node build/index.js
npx @modelcontextprotocol/inspector uv --directory /path/to/my-server run server.py
Its CLI mode is useful in CI, for example to check your tool list:
npx @modelcontextprotocol/inspector --cli node build/index.js --method tools/list
Call each tool with bad input on purpose: wrong types, empty strings, very long
values, ../ paths. That's the input an injected model will send.
5. Connect it to Claude
Claude Code: the documented syntax is
claude mcp add [options] <name> -- <command> [args...], for example:
claude mcp add --transport stdio my-server -- uv --directory /path/to/my-server run server.py
Claude Desktop: add it under mcpServers in claude_desktop_config.json,
with an absolute path:
{
"mcpServers": {
"my-server": {
"command": "uv",
"args": ["--directory", "/ABSOLUTE/PATH/TO/my-server", "run", "server.py"]
}
}
}
6. The security checklist
This is the part that decides whether your server is safe to share. Each item links back to the spec rule behind it.
- Start on stdio. For local servers, the spec's security best practices say to "Use the stdio transport to limit access to just the MCP client."
- If you serve HTTP, lock the door. The
Streamable HTTP rules:
servers "MUST validate the
Originheader on all incoming connections to prevent DNS rebinding attacks", "SHOULD bind only to localhost (127.0.0.1)" when running locally, and "SHOULD implement proper authentication for all connections." Several popular servers have shipped CVEs for exactly this; see our MCP authentication guide. - Never pass tokens through. "MCP servers MUST NOT accept any tokens that were not explicitly issued for the MCP server." Don't forward the client's token to a downstream API.
- Validate every input. The spec's tool rules start with "Validate all tool inputs". Use strict schemas (formats, lengths, ranges, enums), never pass model input to a shell, and for file access: "Servers MUST sanitize file paths to prevent directory traversal attacks".
- Keep tools few and narrow. One tool per job, read-only by default, and write or delete actions as separate tools so clients can ask before they run. Set honest tool annotations, and don't change tool descriptions silently between versions.
- Treat what you return as untrusted. If a tool returns text other people wrote (tickets, emails, web pages), it can carry prompt injection. The spec says servers "MUST" "Sanitize tool outputs". See MCP prompt injection.
- Rate limit and log. "Rate limit tool invocations" is another server MUST. Log calls, but not secrets.
- Ask for the smallest scopes. The spec recommends "a progressive, least-privilege scope model": start with read operations and ask for more only when a privileged tool is first used.
- Keep secrets out of config files. Read API keys from the environment,
not from a committed
.mcp.json. See keeping API keys out of MCP config. - Return useful errors. Report tool failures as tool results with
isError: trueand a message the model can act on. See MCP tool errors.
How we would grade it
We grade every listed server on four public checks (how we score). Building to them from day one is the easiest way to earn an A:
- Login: require sign-in for anything reachable over a network.
- Upkeep: release regularly and respond to issues.
- Known bugs: fix advisories fast and publish them.
- Connection: remote over HTTPS with sign-in, or local stdio.
When it's ready, submit it to the directory. Would you rather not build it yourself? Tell us what it should connect to and we'll build it with these checks done.
Frequently asked questions
What language should I use to build an MCP server?
Use the one your team knows. The official tutorial covers Python, TypeScript, Java, Kotlin and C#. Python (SDK 2.0+) and TypeScript have the most examples and community servers to learn from.
Should my MCP server use stdio or HTTP?
Stdio for a server that runs on the user's own machine for one client; the spec recommends it for local servers. HTTP for a shared or hosted server, and then you need Origin validation, authentication, and HTTPS.
Does my MCP server need OAuth?
Not for a local stdio server. A remote HTTP server should authenticate every connection, and MCP's authorization spec is built on OAuth. Whatever you use, only accept tokens issued for your server.
How do I test an MCP server?
With the MCP Inspector: npx @modelcontextprotocol/inspector plus the command
that starts your server. Its --cli mode lists tools and calls them from
scripts, which makes it easy to add to CI.
Why does my stdio MCP server break when I add logging?
Because something writes to stdout. On stdio, stdout carries the protocol's JSON-RPC messages, so any stray print corrupts them. Log to stderr instead.
More: MCP security best practices · MCP resources vs tools · Is this MCP server safe to install?