{"slug": "rcarmo-umcp-a-micro-mcp-core-asyncio-and-synchronous", "title": "Rcarmo/umcp: A micro MCP core (asyncio and synchronous)", "summary": "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.", "body_md": "A lightweight, zero-dependency implementation of the [Model Context\nProtocol (MCP)](https://modelcontextprotocol.io) in pure Python --\ninspired by the original `bash`\n\nimplementation by Muthukumaran\nNavaneethakrishnan.\n\n**Why?** I found the idea of an MCP server written as a shell script\nfascinating, and wanted to see what the same idea looked like in Python\nwith proper introspection -- type hints to JSON Schema, naming\nconventions to MCP annotations, no decorator boilerplate, and the whole\nthing readable in an afternoon.\n\nThe runtime is three files: `umcp.py`\n\n, `aioumcp.py`\n\n, and the shared\n`umcp_shared.py`\n\n. It has no third-party runtime dependencies, supports\nstdio, stateless Streamable HTTP, legacy HTTP+SSE, and raw TCP, and ships\nwith a handful of runnable examples in [ examples/](/rcarmo/umcp/blob/main/examples).\n\n[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)\n\n- ✅ Full JSON-RPC 2.0 protocol over stdio, SSE, streamable HTTP, or TCP\n- ✅ Complete MCP protocol implementation (tools, prompts,\n**resources**, annotations) - ✅ Dynamic discovery via function naming convention (\n`tool_*`\n\n,`prompt_*`\n\n,`resource_*`\n\n,`resource_template_*`\n\n) - ✅ Runtime tool/prompt/resource registration with list-changed notifications\n- ✅ Stable sorted discovery with optional cursor pagination for list endpoints\n- ✅ Complete introspection of function signatures, including\n`Literal`\n\n,`Union`\n\n, and`TypedDict`\n\n- ✅ MCP\n`inputSchema`\n\ngenerated automatically from type hints - ✅ Automatic\n`readOnlyHint`\n\n/`destructiveHint`\n\n/`openWorldHint`\n\nannotations from naming conventions - ✅ Strict argument validation (\n`additionalProperties: false`\n\n, unknown-arg rejection, type coercion for stringy clients) - ✅ Prompt templates for reusable, structured interactions\n- ✅ Both synchronous and asynchronous implementations -- pick by I/O shape (local disk vs. network)\n- ✅ Zero third-party dependencies\n\n- Python 3.10+ (both bases use PEP 604 unions and\n`types.UnionType`\n\n)\n\n```\ngit clone https://github.com/rcarmo/umcp\ncd umcp\npython -m py_compile umcp.py aioumcp.py umcp_shared.py\n```\n\nNo additional packages required -- `umcp`\n\nuses only the Python\nstandard library.\n\n```\necho '{\"jsonrpc\": \"2.0\", \"method\": \"tools/call\", \"params\": {\"name\": \"get_movies\"}, \"id\": 1}' \\\n  | python ./examples/movie_server.py\necho '{\"jsonrpc\": \"2.0\", \"method\": \"tools/list\", \"id\": 1}' \\\n  | python ./examples/movie_server.py\necho '{\"jsonrpc\": \"2.0\", \"method\": \"tools/call\", \"params\": {\"name\": \"add\", \"arguments\": {\"a\": 5, \"b\": 3}}, \"id\": 1}' \\\n  | python ./examples/calculator_server.py\n```\n\nstdio remains the default and is the right choice when the MCP host launches\nthe server locally. Plain `--port N`\n\ndeliberately retains the old SSE\nbehaviour for compatibility; new network deployments should select\nStreamable HTTP explicitly.\n\n| Transport | Invocation | Intended use |\n|---|---|---|\n| stdio | `python server.py` |\nLocal child-process integrations |\n| Streamable HTTP | `python server.py --port 9000 --http` |\nNew network services; stateless `POST /mcp` |\n| legacy SSE | `python server.py --port 9000 --sse` |\nExisting HTTP+SSE clients; deprecated, with no removal date |\n| raw TCP | `python server.py --port 9000 --tcp` |\nLegacy compatibility only |\n\nThe equivalent explicit form is `--transport stdio|streamable-http|sse|tcp`\n\n.\nNetwork transports also accept `--host`\n\n, `--endpoint`\n\n,\n`--max-request-bytes`\n\n, and repeatable `--allowed-origin`\n\noptions. Conflicting\naliases and network transports without `--port`\n\nare rejected.\n\nFor compatibility with existing lightweight clients and command-line use,\nthe dispatcher does not keep a connection-level \"initialized\" flag; it will\nanswer methods such as `tools/list`\n\nbefore `initialize`\n\n. Standards-compliant\nMCP clients should still perform the normal initialize handshake.\n\nStreamable HTTP accepts one JSON-RPC object per `POST`\n\n. Requests return\n`200 application/json`\n\n; notifications and client responses return `202`\n\n.\nThe initial `initialize`\n\nnegotiates either `2025-03-26`\n\nor `2024-11-05`\n\n.\nLater requests must send a supported value in `MCP-Protocol-Version`\n\n.\nRequest targets are matched on the URL path, so `/mcp?trace=1`\n\nis accepted\nand treated the same as `/mcp`\n\n. HTTP/1.1 requires exactly one `Host`\n\nheader;\nHTTP/1.0 may omit it. Duplicate `Host`\n\n, `Authorization`\n\n, `Origin`\n\n, `Accept`\n\n,\n`Content-Type`\n\n, `MCP-Protocol-Version`\n\n, `Content-Length`\n\n, or\n`Transfer-Encoding`\n\nheaders are rejected with `400`\n\n, and any\n`Transfer-Encoding`\n\nis rejected. Stateless `GET`\n\nand `DELETE`\n\nreturn `405`\n\n.\nBrowser preflight is only enabled for an allowed `Origin`\n\n, and only on the\nconfigured endpoint path.\n\n```\n┌─────────────┐         ┌───────────────┐\n│ MCP Host    │         │ MCP Server    │\n│ (AI System) │◄──────► │ (myserver.py) │\n└─────────────┘ stdio   └───────────────┘\n                                │\n                      ┌─────────┴──────────┐────────────────────┐\n                      ▼                    ▼                    ▼\n              ┌────────────────┐  ┌────────────────┐  ┌────────────────────┐\n              │ Protocol Layer │  │ Business Logic │  │ Prompt Templates   │\n              │ (umcp.py)      │  │(tool_* methods)│  │ (prompt_* methods) │\n              └────────────────┘  └────────────────┘  └────────────────────┘\n                      │                    │\n                      ▼                    ▼\n              ┌───────────────┐    ┌───────────────┐\n              │ Introspection │    │ External      │\n              └───────────────┘    │ Services/APIs │\n                                   └───────────────┘\n```\n\nFor the design details -- transports, sync vs. async rationale, schema\ngeneration, annotation inference, what's deliberately *not* included --\nsee [ docs/ARCHITECTURE.md](/rcarmo/umcp/blob/main/docs/ARCHITECTURE.md).\n\nCreate a file `my_server.py`\n\n:\n\n``` python\n#!/usr/bin/env python3\nfrom umcp import MCPServer\n\nclass MyServer(MCPServer):\n    \"\"\"A simple example MCP server.\"\"\"\n\n    def tool_greet(self, name: str = \"World\") -> str:\n        \"\"\"Greet someone by name.\n\n        Args:\n            name: The name to greet\n\n        Returns:\n            A friendly greeting message\n        \"\"\"\n        return f\"Hello, {name}!\"\n\n    def tool_add_numbers(self, a: float, b: float) -> float:\n        \"\"\"Add two numbers together.\n\n        Args:\n            a: First number\n            b: Second number\n\n        Returns:\n            The sum of the two numbers\n        \"\"\"\n        return a + b\n\nif __name__ == \"__main__\":\n    server = MyServer()\n    server.run()\nchmod +x my_server.py\n\necho '{\"jsonrpc\": \"2.0\", \"method\": \"tools/call\", \"params\": {\"name\": \"greet\", \"arguments\": {\"name\": \"Alice\"}}, \"id\": 1}' | ./my_server.py\necho '{\"jsonrpc\": \"2.0\", \"method\": \"tools/call\", \"params\": {\"name\": \"add_numbers\", \"arguments\": {\"a\": 10, \"b\": 5}}, \"id\": 2}' | ./my_server.py\n```\n\nUse `AsyncMCPServer`\n\nfrom `aioumcp`\n\ninstead, and make tool methods\n`async def`\n\n. Pick async when your tools talk to the network; pick sync\nwhen they talk to the local filesystem or run subprocesses. The\n[architecture doc](/rcarmo/umcp/blob/main/docs/ARCHITECTURE.md#two-implementations-one-shape)\nexplains why both exist.\n\n``` python\n#!/usr/bin/env python3\nimport asyncio\nfrom aioumcp import AsyncMCPServer\n\nclass AsyncMyServer(AsyncMCPServer):\n    \"\"\"An async example MCP server.\"\"\"\n\n    async def tool_fetch_data(self, url: str) -> dict:\n        \"\"\"Simulate fetching data from a URL.\"\"\"\n        await asyncio.sleep(0.1)\n        return {\"url\": url, \"status\": \"success\", \"data\": \"mock response\"}\n\nif __name__ == \"__main__\":\n    server = AsyncMyServer()\n    server.run()\n```\n\nThe runnable examples live under [ examples/](/rcarmo/umcp/blob/main/examples):\n\n-- CRUD over an in-memory store, parameter validation, prompt templates.`examples/movie_server.py`\n\n-- pure compute, error handling, type-safe parameters.`examples/calculator_server.py`\n\n-- static and templated MCP resources, plus tools that mutate them and emit`examples/resource_server.py`\n\n`notifications/resources/updated`\n\n.-- async version of the movie server.`examples/async_movie_server.py`\n\n-- async version of the calculator.`examples/async_calculator_server.py`\n\n-- async version of the resource server.`examples/async_resource_server.py`\n\n```\n# Synchronous\npython examples/movie_server.py\npython examples/calculator_server.py\n\n# Asynchronous\npython examples/async_movie_server.py\npython examples/async_calculator_server.py\n```\n\nFor a real, sizeable MCP server built on `umcp`\n\n, see\n[ rcarmo/python-office-mcp-server](https://github.com/rcarmo/python-office-mcp-server) -- a Word/Excel/PowerPoint\nserver with 100+ tools, structured workflow discovery, mutation\ndiagnostics, and the chaining patterns documented in\n\n[. It's the canonical worked example for what a production deployment of](/rcarmo/umcp/blob/main/docs/CHAINING.md)\n\n`docs/CHAINING.md`\n\n`umcp`\n\nlooks like.`umcp`\n\nsupports reusable prompt templates using the same naming\nconvention as tools: methods named `prompt_<name>`\n\nare discovered and\nexposed via the MCP `prompts/list`\n\nand `prompts/get`\n\nmethods. You can\nalso register prompts at runtime with `register_prompt()`\n\n/\n`unregister_prompt()`\n\n, or use `register_prompt_and_notify()`\n\n/\n`unregister_prompt_and_notify()`\n\nas convenience wrappers. The plain\nregister/unregister APIs only mutate local state; if you use them\ndirectly, you must call `notify_prompt_list_changed()`\n\nyourself. See\n[ docs/PROMPTS.md](/rcarmo/umcp/blob/main/docs/PROMPTS.md) for the full reference.\n\nQuick example:\n\n``` python\nclass MyServer(MCPServer):\n    def prompt_code_review(self, filename: str, issues: int = 0) -> str:\n        \"\"\"Generate a focused code review instruction.\n        Categories: code, review\"\"\"\n        return f\"Please review '{filename}'. Assume ~{issues} pre-identified issues.\"\necho '{\"jsonrpc\": \"2.0\", \"method\": \"prompts/list\", \"id\": 1}' | python ./examples/movie_server.py\necho '{\"jsonrpc\": \"2.0\", \"method\": \"prompts/get\", \"params\": {\"name\": \"code_review\", \"arguments\": {\"filename\": \"main.py\"}}, \"id\": 2}' | python ./examples/movie_server.py\n```\n\n`umcp`\n\nalso exposes MCP `completion/complete`\n\nand `logging/setLevel`\n\n.\n`initialize`\n\nalways advertises `logging: {}`\n\nand only adds\n`completions: {}`\n\nwhen the server actually has something completable\n(prompt arguments, resource-template arguments, or registered completion\nproviders).\n\nCompletion values can come from:\n\n`Literal[...]`\n\nannotations`Enum`\n\nannotations- prompt\n`inputSchema`\n\nenums supplied to`register_prompt()`\n\n- registered completion providers via\n`register_completion_provider()`\n\nProviders receive `prefix`\n\n, `arguments`\n\n, `ref`\n\n, and `argument`\n\n, and may\nreturn either a plain list or `{ \"values\": [...], \"total\": N, \"hasMore\": bool }`\n\n. Results are prefix-filtered, deduplicated, and\ncapped at 100 values.\n\nFor runtime logs, call `notify_log_message()`\n\n/ `log_message()`\n\nwith one\nof the standard MCP levels (`debug`\n\n, `info`\n\n, `notice`\n\n, `warning`\n\n,\n`error`\n\n, `critical`\n\n, `alert`\n\n, `emergency`\n\n). Payloads are redacted\nrecursively by default for common secret-looking keys and bearer/token\nstrings; pass `sanitize=False`\n\nto opt out explicitly.\n\nProgress and cancellation are request-local. If a client supplies\n`params._meta.progressToken`\n\n, server code can read it with\n`get_progress_token()`\n\nand emit `notifications/progress`\n\nvia\n`notify_progress(...)`\n\nor the server instance method of the same name.\nNo token means no progress notification is sent. Cancellation arrives as\n`notifications/cancelled`\n\n; async handlers are cancelled actively when\npossible, while sync handlers are cooperative only and should call\n`is_request_cancelled()`\n\n/ `raise_if_cancelled()`\n\ninside long-running\nwork.\n\n`umcp`\n\nimplements the [MCP resources spec](https://modelcontextprotocol.io/specification/2025-11-25/server/resources): methods named\n`resource_<name>`\n\nare exposed as static resources, and methods named\n`resource_template_<name>`\n\nbecome parameterised resource templates whose\nsignature parameters fill in the URI placeholders.\n\nThe MCP capability set is declared automatically on `initialize`\n\n:\n\n`tools: {\"listChanged\": true}`\n\n`prompts: {\"get\": true, \"listChanged\": true}`\n\n`resources: {\"subscribe\": true, \"listChanged\": true}`\n\n`logging: {}`\n\n`completions: {}`\n\nwhen prompt/resource-template completion is available\n\nThe following methods are wired through:\n\n`tools/list`\n\n,`prompts/list`\n\n,`resources/list`\n\n, and`resources/templates/list`\n\nreturn stable sorted results. When called without pagination params they preserve the previous one-shot behaviour. When called with`pageSize`\n\n(or a returned`cursor`\n\n) they return a page plus`nextCursor`\n\n.`resources/list`\n\n-- returns every`resource_*`\n\nmethod, plus anything registered via`register_resource()`\n\n.`resources/templates/list`\n\n-- every`resource_template_*`\n\nmethod, plus`register_resource_template()`\n\n.`resources/read`\n\n-- looks up by URI; static resources match by exact URI, templates by`{placeholder}`\n\nregex. Returns`-32002`\n\nwith the URI in`data`\n\nfor unknown resources, per spec.`resources/subscribe`\n\n/`resources/unsubscribe`\n\n-- track URIs of interest.\n\nReturn types are normalised into MCP `contents`\n\nentries:\n\n`str`\n\n→ text content (`text`\n\n+`mimeType`\n\n, default`text/plain`\n\n).`bytes`\n\n→ binary content (base64-encoded`blob`\n\n, default`application/octet-stream`\n\n).`dict`\n\n→ a single content entry, with`uri`\n\nfilled in if missing.`list[dict]`\n\n→ multiple content entries.\n\nAttach `_mcp_resource = {...}`\n\n(or `_mcp_resource_template = {...}`\n\n) to a\nresource method to override the auto-generated `uri`\n\n/ `uri_template`\n\n,\nset a `title`\n\n, `description`\n\n, `mime_type`\n\n, `size`\n\n, or annotations\n(`audience`\n\n, `priority`\n\n, `lastModified`\n\n).\n\nMutating tools should call `notify_resource_updated(uri)`\n\n(only fires\nfor URIs the client has subscribed to) and/or\n`notify_resource_list_changed()`\n\nafter they change resource state. In\nthe async base these helpers are coroutines (`await self.notify_resource_updated(...)`\n\n).\n\nQuick example:\n\n``` php\nclass MyServer(MCPServer):\n    def resource_motd(self) -> str:\n        \"\"\"Message of the day.\"\"\"\n        return \"Hello!\"\n    resource_motd._mcp_resource = {\"mime_type\": \"text/plain\", \"title\": \"MOTD\"}\n\n    def resource_template_user(self, user_id: str) -> dict:\n        \"\"\"Synthesised user profile.\"\"\"\n        return {\"mimeType\": \"application/json\",\n                \"text\": f'{{\"id\": \"{user_id}\"}}'}\necho '{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"resources/list\"}' | python examples/resource_server.py\necho '{\"jsonrpc\":\"2.0\",\"id\":2,\"method\":\"resources/read\",\"params\":{\"uri\":\"umcp://ResourceServer/motd\"}}' | python examples/resource_server.py\necho '{\"jsonrpc\":\"2.0\",\"id\":3,\"method\":\"resources/read\",\"params\":{\"uri\":\"file:///hello.txt\"}}' | python examples/resource_server.py\n```\n\nBase class for synchronous MCP servers.\n\n`discover_tools()`\n\n-- finds all`tool_*`\n\nmethods on the subclass.`discover_prompts()`\n\n-- finds all`prompt_*`\n\nmethods on the subclass.`discover_resources()`\n\n/`discover_resource_templates()`\n\n-- finds all`resource_*`\n\nand`resource_template_*`\n\nmethods on the subclass.`register_tool(name, callable, ...)`\n\n/`unregister_tool(name)`\n\n-- runtime registration of tools. These are mutation-only; call`notify_tool_list_changed()`\n\nexplicitly afterwards, or use`register_tool_and_notify(...)`\n\n/`unregister_tool_and_notify(...)`\n\n.`register_tool()`\n\nalso accepts`output_schema=...`\n\n, and tool methods may expose`_mcp_output_schema`\n\nmetadata for`tools/list`\n\n.`register_prompt(name, callable, ...)`\n\n/`unregister_prompt(name)`\n\n-- runtime registration of prompts. These are mutation-only; call`notify_prompt_list_changed()`\n\nexplicitly afterwards, or use`register_prompt_and_notify(...)`\n\n/`unregister_prompt_and_notify(...)`\n\n.`register_resource(uri, callable, ...)`\n\n/`register_resource_template(uri_template, callable, ...)`\n\n-- runtime registration of resources/templates.`notify_tool_list_changed()`\n\n/`notify_prompt_list_changed()`\n\n/`notify_resource_updated(uri)`\n\n/`notify_resource_list_changed()`\n\n-- emit MCP notifications when the advertised catalogue changes. In`AsyncMCPServer`\n\n, these notification helpers are coroutines.`handle_tools_call()`\n\n-- dispatches a tool call. Mapping / structured tool results preserve the legacy text`content`\n\nblock and also expose`structuredContent`\n\nwhen possible; advertised`outputSchema`\n\nvalues are validated on the way out.`handle_prompt_get()`\n\n-- dispatches a prompt fetch.`get_config()`\n\n-- override to declare server name, version, capabilities.`get_instructions()`\n\n-- override to give the model session-level guidance.`run()`\n\n-- start the server on the configured transport (stdio by default; plain`--port N`\n\nretains legacy SSE,`--http`\n\nselects Streamable HTTP, and`--tcp`\n\nselects raw TCP).`authenticate_request()`\n\n/`authorize_request()`\n\n-- optional HTTP identity and policy hooks. The default principal is anonymous for compatibility.- Module-level\n`umcp.get_request_context()`\n\n-- return immutable request-local transport, protocol, principal, peer, and header metadata.\n\nThe same MCP feature surface, with coroutine entry points named\n`process_request_async()`\n\n, `run_socket_async()`\n\n, `run_sse_async()`\n\n, and\n`run_streamable_http_async()`\n\n; `tool_*`\n\nand `prompt_*`\n\nmethods may also be\n`async def`\n\n. Use this when your tools are network-bound; use `MCPServer`\n\nwhen\nthey're local-disk or compute-bound.\n\nRemote services can validate HTTP credentials without turning identity into a model-controlled tool argument:\n\n``` python\nfrom typing import Mapping\nfrom umcp import MCPServer, MCPPrincipal, get_request_context\n\nclass PrivateServer(MCPServer):\n    def authenticate_request(\n        self, *, method: str, path: str,\n        headers: Mapping[str, str], peer: str | None,\n    ) -> MCPPrincipal | None:\n        if headers.get(\"authorization\") != \"Bearer expected-token\":\n            return None\n        return MCPPrincipal(name=\"alice\", roles=(\"reader\",))\n\n    def authorize_request(\n        self, principal: MCPPrincipal | None, *,\n        rpc_method: str | None, tool_name: str | None,\n    ) -> bool:\n        return principal is not None and (\n            tool_name != \"write\" or \"writer\" in principal.roles\n        )\n\n    def tool_whoami(self) -> str:\n        return get_request_context().principal or \"local\"\n```\n\nAuthentication failure is an HTTP `401`\n\n; authorization failure is `403`\n\n.\nThe context is reset in `finally`\n\nafter every dispatch and is isolated across\nthreads, asyncio tasks, and executor workers. `MCPPrincipal.metadata`\n\nand\n`MCPRequestContext.headers`\n\nare defensive immutable copies.\n\n``` php\ndef tool_<name>(self, param1: type1, param2: type2 = default) -> return_type:\n    \"\"\"Tool description (first line becomes the summary).\n\n    Args:\n        param1: Description of parameter 1\n        param2: Description of parameter 2\n\n    Returns:\n        Description of return value\n    \"\"\"\nphp\ndef prompt_<name>(self, param1: type1, param2: type2 = default) -> return_type:\n    \"\"\"Prompt description.\n    Categories: category1, category2\"\"\"\n```\n\nFor schema generation rules (`Literal`\n\n, `Union`\n\n, `Optional`\n\n, `TypedDict`\n\n)\nand annotation inference, see\n[ docs/ARCHITECTURE.md](/rcarmo/umcp/blob/main/docs/ARCHITECTURE.md).\n\nThe project uses `pytest`\n\nand ships a `Makefile`\n\nplus a GitHub Actions\nworkflow that runs the suite on Python 3.10, 3.11, and 3.12.\n\n```\n# Make targets (preferred)\nmake test            # full suite\nmake test-fast       # -x -q\nmake coverage        # coverage report on stdout\nmake coverage-html   # writes htmlcov/index.html\nmake clean           # remove caches and log files\n\n# Or pytest directly\npython -m pytest tests/\n\n# Or with uv (hermetic, no global pytest required)\nuv run --with pytest --with pytest-asyncio --python 3.12 \\\n  python -m pytest tests/\n\n# Specific files / verbose\npython -m pytest tests/test_resources.py -v\n```\n\n| File | Area |\n|---|---|\n`test_introspection.py` |\nend-to-end tool discovery via subprocess |\n`test_movieserver.py` |\nworked-example end-to-end test |\n`test_protocol_errors.py` |\nJSON-RPC error paths -- parse/version/method/args/raise |\n`test_schema_generation.py` |\ntype hint -> JSON Schema mapping (primitives, `Optional` , `Union` , `Literal` , `TypedDict` , `list` /`dict` ) |\n`test_schema_fallbacks.py` |\nexotic-type schema fallback behaviour |\n`test_annotations.py` |\n`readOnlyHint` / `destructiveHint` / `openWorldHint` inference + overrides |\n`test_coercion.py` |\nstringy-client argument coercion (str -> int / float / bool) |\n`test_prompts.py` / `test_async_prompts.py` |\nprompt discovery and dispatch |\n`test_resources.py` |\nresources/list, /templates/list, /read, subscribe/unsubscribe, dynamic registration, capability declaration |\n`test_notifications.py` |\n`notifications/resources/list_changed` and `notifications/resources/updated` emission and subscription gating |\n`test_async_servers.py` |\nasync stdio subprocess round-trips |\n`test_transports.py` |\nsync stdio, TCP, legacy SSE, and Streamable HTTP subprocess tests |\n`test_shared_negotiation_context.py` |\nprotocol negotiation and immutable request/principal context |\n`test_streamable_http_sync_async.py` |\nmatching sync/async Streamable HTTP dispatch |\n`test_streamable_http_regressions.py` |\nauth, Origin, CORS, limits, context isolation, CLI, and remote-safe errors |\n`simple_async_test.py` |\nsmoke tests for the async base |\n\nThe suite covers both bases and runs on Python 3.10, 3.11, and 3.12.\n\n```\ngit clone https://github.com/rcarmo/umcp\ncd umcp\npython -m pytest tests/\n```\n\n- Explicit imports only.\n- Functional style; short, single-responsibility functions.\n- Type hints on all parameters and returns.\n- Double quotes for strings; triple-double-quote docstrings.\n`snake_case`\n\nmethod naming.- f-strings only when needed.\n- Logging over print statements.\n\n- Fork the repository.\n- Create a feature branch:\n`git checkout -b feature-name`\n\n. - Make your changes following the code style.\n- Add tests for new functionality.\n- Ensure all tests pass:\n`python -m pytest tests/`\n\n. - Submit a pull request.\n\n```\numcp/\n├── umcp.py                -- sync MCPServer base class\n├── aioumcp.py             -- async AsyncMCPServer base class\n├── umcp_shared.py         -- versions, principals, and request context\n├── scripts/               -- optional compatibility smoke tests\n├── examples/              -- runnable example servers\n│   ├── movie_server.py\n│   ├── async_movie_server.py\n│   ├── calculator_server.py\n│   ├── async_calculator_server.py\n│   ├── resource_server.py\n│   └── async_resource_server.py\n├── tests/                 -- pytest suite\n├── docs/\n│   ├── ARCHITECTURE.md    -- design, transports, schema generation\n│   ├── CHAINING.md        -- chaining patterns for MCP server authors\n│   └── PROMPTS.md         -- prompt template reference\n├── readme.md              -- this file\n└── LICENSE\n\"mcp\": {\n    \"servers\": {\n        \"my-weather-server\": {\n            \"type\": \"stdio\",\n            \"command\": \"/path/to/your/server.py\",\n            \"args\": [],\n            \"env\": {\n                \"MCP_API_KEY\": \"anything_you_need\"\n            }\n        }\n    }\n}\n```\n\nThen `/mcp my-weather-server get weather for New York`\n\nfrom Copilot Chat.\n\n```\n{\n  \"mcpServers\": {\n    \"my-server\": {\n      \"command\": \"python\",\n      \"args\": [\"/path/to/your/server.py\"],\n      \"env\": {}\n    }\n  }\n}\n```\n\n- No streaming responses -- partial results aren't supported.\n- No built-in authentication backend or policy engine. Streamable HTTP\nexposes\n`authenticate_request()`\n\nand`authorize_request()`\n\nhooks, and reverse proxies can supply trusted identity, but the application owns credentials, roles, and policy. - No concurrency in the synchronous version -- one request at a time.\nUse\n`AsyncMCPServer`\n\nif you need overlapping I/O.\n\nFor most AI-assistant / local-tool use cases, none of these are\nblocking. See [ docs/ARCHITECTURE.md](/rcarmo/umcp/blob/main/docs/ARCHITECTURE.md#what-umcp-deliberately-doesnt-do)\nfor the rationale.\n\nFor local hosts (Claude Desktop, VS Code, Copilot, Piclaw, etc.), prefer\n**stdio**. `umcp`\n\nnow tags stdio/file requests as local request contexts,\nkeeps request metadata immutable, and avoids exposing internal exception\nstrings over remote transports.\n\nIf you need network access, prefer **streamable HTTP** over legacy SSE/TCP:\n\n- bind to loopback unless you have a real reason not to;\n- implement\n`authenticate_request()`\n\nand`authorize_request()`\n\nfor remote use; hook exceptions are logged server-side and returned as generic`500`\n\ns; - require a supported\n`MCP-Protocol-Version`\n\non non-`initialize`\n\nrequests; - set\n`--allowed-origin`\n\nexplicitly for browser clients, or rely on loopback-only Origin handling for local web UIs; - expect\n`OPTIONS`\n\npreflight only when the browser sends a valid`Origin`\n\n.\n\nA typical reverse-proxy setup should preserve `Host`\n\n, `Authorization`\n\n,\n`Origin`\n\n, `Accept`\n\n, `Content-Type`\n\n, and `MCP-Protocol-Version`\n\nheaders unchanged,\nand should not inject duplicates or `Transfer-Encoding`\n\n.\nIf the proxy terminates TLS or auth, keep the upstream `umcp`\n\nlistener on\nlocalhost.\n\nMinimal `systemd`\n\nservice sketch:\n\n```\n[Unit]\nDescription=umcp HTTP server\nAfter=network.target\n\n[Service]\nExecStart=/usr/bin/python /srv/umcp/server.py --port 9000 --http --host 127.0.0.1 --allowed-origin https://ui.example\nWorkingDirectory=/srv/umcp\nRestart=on-failure\nUser=umcp\nGroup=umcp\n\n[Install]\nWantedBy=multi-user.target\n```\n\nContainer deployments should follow the same pattern: keep the container port private, expose it through a reverse proxy, and do not publish raw SSE/TCP unless you have a separate auth story. Session IDs, if stateful HTTP support is added later, are routing identifiers rather than credentials.\n\nPiclaw's `pi-mcp-adapter`\n\nuses the official MCP SDK's\n`StreamableHTTPClientTransport`\n\nbefore considering its legacy SSE fallback.\nThe repository includes a repeatable smoke test for that exact transport:\n\n```\n# Terminal 1\npython examples/calculator_server.py --port 9000 --http\n\n# Terminal 2 -- point this at Piclaw's bundled SDK directory\nMCP_URL=http://127.0.0.1:9000/mcp \\\nMCP_SDK_ROOT=/path/to/piclaw/node_modules/@modelcontextprotocol/sdk \\\n  bun scripts/piclaw_streamable_http_smoke.ts\n```\n\nA successful run connects without SSE, discovers the calculator tools, and\ncalls `add(2, 3)`\n\n. This is an optional compatibility check; Bun and the MCP\nSDK are not runtime dependencies of `umcp`\n\n.\n\n**Server doesn't respond to JSON-RPC requests.** Check the JSON is\nvalid and the server is running. Try a `tools/list`\n\nrequest first --\nit has no arguments and exercises the protocol path.\n\n**Tools not showing up in tools/list.** Ensure the methods are\nnamed\n\n`tool_*`\n\nand have proper type hints. Check `mcpserver.log`\n\nnext\nto the script for introspection errors.**Async server seems slow.** The async examples use `asyncio.sleep()`\n\nto simulate I/O. Remove these in real applications.\n\n**Permission denied on the script.** `chmod +x your_server.py`\n\n.\n\n**Debug logging.**\n\n```\nif __name__ == \"__main__\":\n    server = MyServer()\n    server.logger.setLevel(\"DEBUG\")\n    server.run()\n```\n\n-- design notes: transports, sync vs. async rationale, schema generation, annotation inference, what's deliberately not included.`docs/ARCHITECTURE.md`\n\n-- how language models actually chain MCP tool calls in practice, with`docs/CHAINING.md`\n\nas the worked example. Read this before building anything non-trivial; it's where the real reliability work lives.`python-office-mcp-server`\n\n-- prompt template reference.`docs/PROMPTS.md`\n\n-- production-grade MCP server built on`python-office-mcp-server`\n\n`umcp`\n\n, with 100+ tools for Word/Excel/PowerPoint editing.\n\nMIT -- see [ LICENSE](/rcarmo/umcp/blob/main/LICENSE).\n\n- Inspired by the original\n`bash`\n\nMCP implementation by Muthukumaran Navaneethakrishnan. - Built against the\n[Model Context Protocol](https://modelcontextprotocol.io)specification.", "url": "https://wpnews.pro/news/rcarmo-umcp-a-micro-mcp-core-asyncio-and-synchronous", "canonical_source": "https://github.com/rcarmo/umcp", "published_at": "2026-08-01 22:03:22+00:00", "updated_at": "2026-08-01 22:22:25.039029+00:00", "lang": "en", "topics": ["developer-tools", "ai-infrastructure"], "entities": ["Rui Carmo", "umcp", "Model Context Protocol", "Muthukumaran Navaneethakrishnan"], "alternates": {"html": "https://wpnews.pro/news/rcarmo-umcp-a-micro-mcp-core-asyncio-and-synchronous", "markdown": "https://wpnews.pro/news/rcarmo-umcp-a-micro-mcp-core-asyncio-and-synchronous.md", "text": "https://wpnews.pro/news/rcarmo-umcp-a-micro-mcp-core-asyncio-and-synchronous.txt", "jsonld": "https://wpnews.pro/news/rcarmo-umcp-a-micro-mcp-core-asyncio-and-synchronous.jsonld"}}