Create an MCP Server: The 20-Minute Path
Build one useful local tool first, verify the protocol boundary, and delay remote complexity until the server earns it.

To create an MCP server in 20 minutes, start with one read-only Python tool, run it over stdio, inspect its schema and result, then register the exact launch command in your client. Do not begin with OAuth, a database writer, or remote hosting. First prove that discovery, input validation, execution, and errors all work locally.
- The 60-second version
- Pick your SDK
- Write the server
- Wire it into a client
- Test it
- The mistakes that cost an afternoon
- Ship it
The 60-second version
The shortest honest path is a local stdio server with one deterministic tool. It needs a name, a typed function, a transport, and a client entry—nothing else.
- Install Python 3.10+,
uv, and MCP SDK 2.0+. - Create a project with
uv init twenty-minute-mcp. - Add the SDK with
uv add "mcp[cli]". - Put the server code below in
server.py. - Run
uv run server.py, then test it with Inspector. - Add the launch command to your MCP client and restart the client.
This server exposes a single MCP tool, not a chatbot. MCP defines tools as schema-described functions that a client discovers with tools/list and invokes with tools/call; your host still owns the model conversation. The official server quickstart confirms the current Python requirement and stdio pattern.
Pick your SDK
Choose Python unless your existing service is TypeScript. The language decision rarely affects protocol capability; it affects deployment fit, validation style, and how quickly your team can debug the wrapper around its real business logic.
| Choice | Best when | Trade-off |
|---|---|---|
| Python SDK | You wrap data scripts, internal APIs, or Python libraries | Concise decorators and inferred schemas; packaging a local runtime can be awkward |
| TypeScript SDK | You already ship Node services or need its web middleware ecosystem | Explicit schemas and build output; more setup for a one-file prototype |
The current Python SDK derives tool definitions from type hints and docstrings. The current TypeScript SDK uses McpServer, registerTool, and a schema library such as Zod; its official repository also documents runtime adapters. I would not switch languages merely because an old tutorial has more stars. If this prototype will become a maintained integration, follow the longer build-an-MCP-server guide after proving the call path.
Write the server
Write one bounded, read-only operation with types and a precise description. This example is intentionally boring: it proves MCP wiring without hiding failures behind a third-party API.
from mcp.server import MCPServer
mcp = MCPServer("text-utilities")
@mcp.tool()
def word_count(text: str) -> dict[str, int]:
"""Count whitespace-separated words and Unicode characters in text."""
return {
"words": len(text.split()),
"characters": len(text),
}
if __name__ == "__main__":
mcp.run(transport="stdio")
Save it as server.py and run uv run server.py. A stdio server appearing to sit silently is normal: the client launches it and exchanges newline-delimited JSON-RPC over stdin and stdout. The transport specification says stdout may contain only valid MCP messages, while logs belong on stderr; one stray print() can corrupt the session (MCP transports specification).
The return type matters. Typed, structured data is easier for clients to validate and for models to use than a decorative sentence. Keep domain logic in ordinary functions and make the decorated handler thin; then you can unit-test behavior without starting an MCP session.

Wire it into a client
Configure the client to launch exactly the command that worked in your terminal, using absolute paths. For Claude Desktop on macOS or Windows, add this documented mcpServers entry and replace both placeholders:
{
"mcpServers": {
"text-utilities": {
"command": "/ABSOLUTE/PATH/TO/uv",
"args": [
"--directory",
"/ABSOLUTE/PATH/TO/twenty-minute-mcp",
"run",
"server.py"
]
}
}
}
Find uv with which uv on macOS/Linux or where uv on Windows. Validate the edited file with the MCP config validator, fully quit the client, and reopen it. The official local-server connection guide documents Claude Desktop’s config location, restart requirement, and server indicator.
Confirmation is not “the process exists.” Open the client’s connectors or tools UI and verify that text-utilities appears and exposes word_count with one string input. Then ask it to count a known phrase and compare the result manually. If the tool is absent, inspect client logs before changing code.
Test it
Test discovery, boundaries, failures, and side effects—not just the happy-path answer. MCP Inspector is the fastest neutral check because it shows what the server advertises before a model decides whether to call anything.
Run:
npx @modelcontextprotocol/inspector uv --directory /ABSOLUTE/PATH/TO/twenty-minute-mcp run server.py
In the UI, connect, list tools, select word_count, and call it with hello MCP world. Expect words to be 3 and characters to be 15. The official Inspector guide covers direct commands plus inspection of tools, resources, prompts, notifications, and logs.
Before trust, check these cases:
- Empty input and a very large string.
- Newlines, emoji, and non-English text.
- The advertised input schema matches the handler.
- Errors are useful but do not reveal secrets or stack traces.
- Repeated calls return consistent results and do not leak state.
For a real integration, add unit tests around domain logic, an integration test that lists and calls tools, timeouts for every network request, and explicit maximum input/output sizes. A passing manual call is a smoke test, not production evidence.
The mistakes that cost an afternoon
Most “MCP failures” are process-launch and framing failures, not protocol mysteries. Check the boring edges first; rebuilding the tool usually wastes time.
- Logging to stdout. With stdio, use Python’s
loggingmodule or stderr. stdout is the protocol channel. - Relative paths. GUI clients may start servers from an unexpected working directory. The official debugging guide recommends absolute executable, project, and environment-file paths.
- A missing runtime environment. A command that works in your shell may fail in a GUI with a smaller
PATH. Use the absoluteuvpath and declare required environment values in client configuration. - Editing without restarting. Many clients keep the subprocess and cached tool list alive. Fully quit and reopen after code or config changes.
- Vague tool descriptions. “Process data” gives the model no reliable selection signal. Name the action, inputs, output, and limits.
- Catching every exception. Returning “something went wrong” erases diagnosis. Catch expected upstream failures, attach safe context, and let programming bugs surface in server logs.
- Starting with write access. A delete-or-send tool multiplies the cost of every schema and authorization mistake. Prove a read path first.
My hard rule: never let an MCP handler become the authorization boundary by accident. Validate identity and permission in the underlying service too, because tool metadata guides a model; it does not enforce business policy. The MCP project's security guidance recommends sandboxing local servers, restricting filesystem and network access, and minimizing authorization scopes.
Ship it
Keep stdio for one user on one machine; move to Streamable HTTP when multiple clients need a centrally operated service. Remote is an architectural change, not a transport toggle you deploy casually.
| Local stdio | Remote Streamable HTTP |
|---|---|
| Client starts one subprocess | Server runs independently for many clients |
| OS user permissions dominate | Authentication and per-user authorization are mandatory |
| No listening network port | HTTPS endpoint, origin validation, rate limits, and operations are required |
| Simple secrets in local config | Central secret storage and token lifecycle are needed |
The protocol’s architecture overview describes local stdio as a typical one-client connection and remote HTTP as a typical many-client service. For HTTP, the transport specification requires Origin validation and recommends localhost binding for local use plus authentication for connections.
I would ship locally until user distribution or uptime becomes the real problem. For remote, add OAuth or another client-supported authentication flow, tenant isolation, audit logs, request deadlines, rate limits, health checks, and graceful shutdown before publishing a URL. Do not expose a development endpoint directly to the internet.
Finally, version your tool behavior, document permissions, and make destructive actions conspicuous. Compare established patterns in our best MCP servers, but keep the first release small: one dependable tool beats ten ambiguous ones.