Rcarmo/umcp: A micro MCP core (asyncio and synchronous) Rui Carmo released umcp, a zero-dependency Python implementation of the Model Context Protocol (MCP) that supports stdio, Streamable HTTP, legacy SSE, and raw TCP transports. The runtime consists of three files and requires Python 3.10+, with no third-party dependencies. It automatically generates JSON Schema from type hints and infers MCP annotations from naming conventions, enabling dynamic discovery of tools, prompts, and resources. A lightweight, zero-dependency implementation of the Model Context Protocol MCP https://modelcontextprotocol.io in pure Python -- inspired by the original bash implementation by Muthukumaran Navaneethakrishnan. Why? I found the idea of an MCP server written as a shell script fascinating, and wanted to see what the same idea looked like in Python with proper introspection -- type hints to JSON Schema, naming conventions to MCP annotations, no decorator boilerplate, and the whole thing readable in an afternoon. The runtime is three files: umcp.py , aioumcp.py , and the shared umcp shared.py . It has no third-party runtime dependencies, supports stdio, stateless Streamable HTTP, legacy HTTP+SSE, and raw TCP, and ships with a handful of runnable examples in examples/ /rcarmo/umcp/blob/main/examples . Features -features Requirements -requirements Installation -installation Quick start -quick-start Transports -transports Architecture %EF%B8%8F-architecture Getting started tutorial -getting-started-tutorial Examples -examples Prompt templates -prompt-templates Resources -resources API reference -api-reference Testing -testing Development %EF%B8%8F-development Integration VS Code, Claude Desktop -integration Limitations -limitations Deployment notes -deployment-notes Troubleshooting -troubleshooting Further reading -further-reading License -license - ✅ Full JSON-RPC 2.0 protocol over stdio, SSE, streamable HTTP, or TCP - ✅ Complete MCP protocol implementation tools, prompts, resources , annotations - ✅ Dynamic discovery via function naming convention tool , prompt , resource , resource template - ✅ Runtime tool/prompt/resource registration with list-changed notifications - ✅ Stable sorted discovery with optional cursor pagination for list endpoints - ✅ Complete introspection of function signatures, including Literal , Union , and TypedDict - ✅ MCP inputSchema generated automatically from type hints - ✅ Automatic readOnlyHint / destructiveHint / openWorldHint annotations from naming conventions - ✅ Strict argument validation additionalProperties: false , unknown-arg rejection, type coercion for stringy clients - ✅ Prompt templates for reusable, structured interactions - ✅ Both synchronous and asynchronous implementations -- pick by I/O shape local disk vs. network - ✅ Zero third-party dependencies - Python 3.10+ both bases use PEP 604 unions and types.UnionType git clone https://github.com/rcarmo/umcp cd umcp python -m py compile umcp.py aioumcp.py umcp shared.py No additional packages required -- umcp uses only the Python standard library. echo '{"jsonrpc": "2.0", "method": "tools/call", "params": {"name": "get movies"}, "id": 1}' \ | python ./examples/movie server.py echo '{"jsonrpc": "2.0", "method": "tools/list", "id": 1}' \ | python ./examples/movie server.py echo '{"jsonrpc": "2.0", "method": "tools/call", "params": {"name": "add", "arguments": {"a": 5, "b": 3}}, "id": 1}' \ | python ./examples/calculator server.py stdio remains the default and is the right choice when the MCP host launches the server locally. Plain --port N deliberately retains the old SSE behaviour for compatibility; new network deployments should select Streamable HTTP explicitly. | Transport | Invocation | Intended use | |---|---|---| | stdio | python server.py | Local child-process integrations | | Streamable HTTP | python server.py --port 9000 --http | New network services; stateless POST /mcp | | legacy SSE | python server.py --port 9000 --sse | Existing HTTP+SSE clients; deprecated, with no removal date | | raw TCP | python server.py --port 9000 --tcp | Legacy compatibility only | The equivalent explicit form is --transport stdio|streamable-http|sse|tcp . Network transports also accept --host , --endpoint , --max-request-bytes , and repeatable --allowed-origin options. Conflicting aliases and network transports without --port are rejected. For compatibility with existing lightweight clients and command-line use, the dispatcher does not keep a connection-level "initialized" flag; it will answer methods such as tools/list before initialize . Standards-compliant MCP clients should still perform the normal initialize handshake. Streamable HTTP accepts one JSON-RPC object per POST . Requests return 200 application/json ; notifications and client responses return 202 . The initial initialize negotiates either 2025-03-26 or 2024-11-05 . Later requests must send a supported value in MCP-Protocol-Version . Request targets are matched on the URL path, so /mcp?trace=1 is accepted and treated the same as /mcp . HTTP/1.1 requires exactly one Host header; HTTP/1.0 may omit it. Duplicate Host , Authorization , Origin , Accept , Content-Type , MCP-Protocol-Version , Content-Length , or Transfer-Encoding headers are rejected with 400 , and any Transfer-Encoding is rejected. Stateless GET and DELETE return 405 . Browser preflight is only enabled for an allowed Origin , and only on the configured endpoint path. ┌─────────────┐ ┌───────────────┐ │ MCP Host │ │ MCP Server │ │ AI System │◄──────► │ myserver.py │ └─────────────┘ stdio └───────────────┘ │ ┌─────────┴──────────┐────────────────────┐ ▼ ▼ ▼ ┌────────────────┐ ┌────────────────┐ ┌────────────────────┐ │ Protocol Layer │ │ Business Logic │ │ Prompt Templates │ │ umcp.py │ │ tool methods │ │ prompt methods │ └────────────────┘ └────────────────┘ └────────────────────┘ │ │ ▼ ▼ ┌───────────────┐ ┌───────────────┐ │ Introspection │ │ External │ └───────────────┘ │ Services/APIs │ └───────────────┘ For the design details -- transports, sync vs. async rationale, schema generation, annotation inference, what's deliberately not included -- see docs/ARCHITECTURE.md /rcarmo/umcp/blob/main/docs/ARCHITECTURE.md . Create a file my server.py : python /usr/bin/env python3 from umcp import MCPServer class MyServer MCPServer : """A simple example MCP server.""" def tool greet self, name: str = "World" - str: """Greet someone by name. Args: name: The name to greet Returns: A friendly greeting message """ return f"Hello, {name} " def tool add numbers self, a: float, b: float - float: """Add two numbers together. Args: a: First number b: Second number Returns: The sum of the two numbers """ return a + b if name == " main ": server = MyServer server.run chmod +x my server.py echo '{"jsonrpc": "2.0", "method": "tools/call", "params": {"name": "greet", "arguments": {"name": "Alice"}}, "id": 1}' | ./my server.py echo '{"jsonrpc": "2.0", "method": "tools/call", "params": {"name": "add numbers", "arguments": {"a": 10, "b": 5}}, "id": 2}' | ./my server.py Use AsyncMCPServer from aioumcp instead, and make tool methods async def . Pick async when your tools talk to the network; pick sync when they talk to the local filesystem or run subprocesses. The architecture doc /rcarmo/umcp/blob/main/docs/ARCHITECTURE.md two-implementations-one-shape explains why both exist. python /usr/bin/env python3 import asyncio from aioumcp import AsyncMCPServer class AsyncMyServer AsyncMCPServer : """An async example MCP server.""" async def tool fetch data self, url: str - dict: """Simulate fetching data from a URL.""" await asyncio.sleep 0.1 return {"url": url, "status": "success", "data": "mock response"} if name == " main ": server = AsyncMyServer server.run The runnable examples live under examples/ /rcarmo/umcp/blob/main/examples : -- CRUD over an in-memory store, parameter validation, prompt templates. examples/movie server.py -- pure compute, error handling, type-safe parameters. examples/calculator server.py -- static and templated MCP resources, plus tools that mutate them and emit examples/resource server.py notifications/resources/updated .-- async version of the movie server. examples/async movie server.py -- async version of the calculator. examples/async calculator server.py -- async version of the resource server. examples/async resource server.py Synchronous python examples/movie server.py python examples/calculator server.py Asynchronous python examples/async movie server.py python examples/async calculator server.py For a real, sizeable MCP server built on umcp , see rcarmo/python-office-mcp-server https://github.com/rcarmo/python-office-mcp-server -- a Word/Excel/PowerPoint server with 100+ tools, structured workflow discovery, mutation diagnostics, and the chaining patterns documented in . It's the canonical worked example for what a production deployment of /rcarmo/umcp/blob/main/docs/CHAINING.md docs/CHAINING.md umcp looks like. umcp supports reusable prompt templates using the same naming convention as tools: methods named prompt