Skip to content

Commit 15d8bac

Browse files
committed
Add stdio transport support
1 parent 07dd1b2 commit 15d8bac

File tree

3 files changed

+90
-18
lines changed

3 files changed

+90
-18
lines changed

README.md

Lines changed: 51 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@ A lightweight, handcrafted implementation of the [Model Context Protocol](https:
1010
- 🎯 **Type-safe** - Native Python type annotations for everything
1111
- 🚀 **Fast** - Minimal overhead, maximum performance
1212
- 🛠️ **Handcrafted** - Written by a human, verified against the spec
13-
- 🌐 **HTTP/SSE transport** - Streamable responses (stdio planned)
13+
- 🌐 **HTTP/SSE transport** - Streamable responses
14+
- 📡 **Stdio transport** - For legacy clients
1415
- 📦 **Tiny** - Less than 1,000 lines of code
1516

1617
## Installation
@@ -66,6 +67,37 @@ Once things are working you can configure the `mcp.json`:
6667
}
6768
```
6869

70+
## Stdio Transport
71+
72+
For MCP clients that only support stdio transport:
73+
74+
```python
75+
from zeromcp import McpServer
76+
77+
mcp = McpServer("my-server")
78+
79+
@mcp.tool
80+
def greet(name: str) -> str:
81+
"""Generate a greeting"""
82+
return f"Hello, {name}!"
83+
84+
if __name__ == "__main__":
85+
mcp.stdio()
86+
```
87+
88+
Then configure in `mcp.json` (different for every client):
89+
90+
```json
91+
{
92+
"mcpServers": {
93+
"my-server": {
94+
"command": "python",
95+
"args": ["path/to/server.py"]
96+
}
97+
}
98+
}
99+
```
100+
69101
## Type Annotations
70102

71103
zeromcp uses native Python `Annotated` types for schema generation:
@@ -139,3 +171,21 @@ def divide(
139171
raise McpToolError("Division by zero")
140172
return numerator / denominator
141173
```
174+
175+
## Supported clients
176+
177+
The following clients have been tested:
178+
179+
- [Claude Code](https://code.claude.com/docs/en/mcp#installing-mcp-servers)
180+
- [Claude Desktop](https://modelcontextprotocol.io/docs/develop/connect-local-servers#installing-the-filesystem-server) (_stdio only_)
181+
- [Visual Studio Code](https://code.visualstudio.com/docs/copilot/customization/mcp-servers)
182+
- [Roo Code](https://docs.roocode.com/features/mcp/using-mcp-in-roo) / [Cline](https://docs.cline.bot/mcp/configuring-mcp-servers) / [Kilo Code](https://kilocode.ai/docs/features/mcp/using-mcp-in-kilo-code)
183+
- [LM Studio](https://lmstudio.ai/docs/app/mcp)
184+
- [Jan](https://www.jan.ai/docs/desktop/mcp#configure-and-use-mcps-within-jan)
185+
- [Gemini CLI](https://geminicli.com/docs/tools/mcp-server/#how-to-set-up-your-mcp-server)
186+
- [Cursor](https://cursor.com/docs/context/mcp)
187+
- [Windsurf](https://docs.windsurf.com/windsurf/cascade/mcp)
188+
- [Zed](https://zed.dev/docs/ai/mcp) (_stdio only_)
189+
- [Warp](https://docs.warp.dev/knowledge-and-collaboration/mcp#adding-an-mcp-server)
190+
191+
_Note_: generally the `/mcp` endpoint is preferred, but not all clients support it correctly.

examples/mcp_example.py

Lines changed: 23 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
"""Example MCP server with test tools"""
22
import time
3+
import argparse
34
from typing import Annotated, Optional, TypedDict, NotRequired
45
from zeromcp import McpToolError, McpServer
56

@@ -78,22 +79,28 @@ def struct_get(
7879
]
7980

8081
if __name__ == "__main__":
81-
print("Starting MCP Example Server...")
82-
print("\nAvailable tools:")
83-
for name in mcp.tools.methods.keys():
84-
func = mcp.tools.methods[name]
85-
print(f" - {name}: {func.__doc__}")
82+
parser = argparse.ArgumentParser(description="MCP Example Server")
83+
parser.add_argument("--stdio", action="store_true", help="Run MCP server over stdio")
84+
args = parser.parse_args()
85+
if args.stdio:
86+
mcp.stdio()
87+
else:
88+
print("Starting MCP Example Server...")
89+
print("\nAvailable tools:")
90+
for name in mcp.tools.methods.keys():
91+
func = mcp.tools.methods[name]
92+
print(f" - {name}: {func.__doc__}")
8693

87-
mcp.start("127.0.0.1", 5001)
94+
mcp.serve("127.0.0.1", 5001)
8895

89-
print("\n" + "="*60)
90-
print("Server is running. Press Ctrl+C to stop.")
91-
print("="*60)
96+
print("\n" + "="*60)
97+
print("Server is running. Press Ctrl+C to stop.")
98+
print("="*60)
9299

93-
try:
94-
while True:
95-
time.sleep(1)
96-
except KeyboardInterrupt:
97-
print("\n\nStopping server...")
98-
mcp.stop()
99-
print("Server stopped.")
100+
try:
101+
while True:
102+
time.sleep(1)
103+
except KeyboardInterrupt:
104+
print("\n\nStopping server...")
105+
mcp.stop()
106+
print("Server stopped.")

src/zeromcp/mcp.py

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,9 @@
33
import json
44
import threading
55
import traceback
6+
import sys
67
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
7-
from typing import Any, Callable, get_type_hints, Annotated
8+
from typing import Any, Callable, get_type_hints, Annotated, BinaryIO
89
from urllib.parse import urlparse, parse_qs
910
from io import BufferedIOBase
1011

@@ -244,6 +245,20 @@ def stop(self):
244245

245246
print("[MCP] Server stopped")
246247

248+
def stdio(self, stdin: BinaryIO = sys.stdin.buffer, stdout: BinaryIO = sys.stdout.buffer):
249+
while True:
250+
try:
251+
request = stdin.readline()
252+
if not request: # EOF
253+
break
254+
255+
response = self.mcp_registry.dispatch(request)
256+
if response is not None:
257+
stdout.write(json.dumps(response).encode("utf-8") + b"\n")
258+
stdout.flush()
259+
except (BrokenPipeError, KeyboardInterrupt): # Client disconnected
260+
break
261+
247262
def _run_server(self, host: str, port: int):
248263
"""Run the HTTP server main loop using ThreadingHTTPServer"""
249264
# Set the MCPServer instance on the handler class

0 commit comments

Comments
 (0)