Skip to content

Commit d48d1aa

Browse files
committed
Initial commit
0 parents  commit d48d1aa

File tree

11 files changed

+1452
-0
lines changed

11 files changed

+1452
-0
lines changed

.gitignore

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
# Python-generated files
2+
__pycache__/
3+
*.py[oc]
4+
build/
5+
dist/
6+
wheels/
7+
*.egg-info
8+
9+
# Virtual environments
10+
.venv

.python-version

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
3.11

README.md

Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
# zeromcp
2+
3+
**Minimal MCP server implementation in pure Python.**
4+
5+
A lightweight, handcrafted implementation of the [Model Context Protocol](https://modelcontextprotocol.io/) focused on what most users actually need: exposing tools with clean Python type annotations.
6+
7+
## Features
8+
9+
-**Zero dependencies** - Pure Python, standard library only
10+
- 🎯 **Type-safe** - Native Python type annotations for everything
11+
- 🚀 **Fast** - Minimal overhead, maximum performance
12+
- 🛠️ **Handcrafted** - Written by a human, verified against the spec
13+
- 🌐 **HTTP/SSE transport** - Streamable responses (stdio planned)
14+
- 📦 **Tiny** - Less than 1,000 lines of code
15+
16+
## Installation
17+
18+
```bash
19+
pip install zeromcp
20+
```
21+
22+
Or with uv:
23+
24+
```bash
25+
uv add zeromcp
26+
```
27+
28+
## Quick Start
29+
30+
```python
31+
from typing import Annotated
32+
from zeromcp import McpServer
33+
34+
mcp = McpServer("my-server")
35+
36+
@mcp.tool
37+
def greet(
38+
name: Annotated[str, "Name to greet"],
39+
age: Annotated[int | None, "Age of person"] = None
40+
) -> str:
41+
"""Generate a greeting message"""
42+
if age:
43+
return f"Hello, {name}! You are {age} years old."
44+
return f"Hello, {name}!"
45+
46+
if __name__ == "__main__":
47+
mcp.start("127.0.0.1", 8000)
48+
```
49+
50+
Then manually test your MCP server with the [inspector](https://github.com/modelcontextprotocol/inspector):
51+
52+
```bash
53+
npx -y @modelcontextprotocol/inspector
54+
```
55+
56+
Once things are working you can configure the `mcp.json`:
57+
58+
```json
59+
{
60+
"mcpServers": {
61+
"my-server": {
62+
"type": "http",
63+
"url": "http://127.0.0.1/mcp"
64+
}
65+
}
66+
}
67+
```
68+
69+
## Type Annotations
70+
71+
zeromcp uses native Python `Annotated` types for schema generation:
72+
73+
```python
74+
from typing import Annotated, Optional, TypedDict, NotRequired
75+
76+
class GreetingResponse(TypedDict):
77+
message: Annotated[str, "Greeting message"]
78+
name: Annotated[str, "Name that was greeted"]
79+
age: Annotated[NotRequired[int], "Age if provided"]
80+
81+
@mcp.tool
82+
def greet(
83+
name: Annotated[str, "Name to greet"],
84+
age: Annotated[Optional[int], "Age of person"] = None
85+
) -> GreetingResponse:
86+
"""Generate a greeting message"""
87+
if age is not None:
88+
return {
89+
"message": f"Hello, {name}! You are {age} years old.",
90+
"name": name,
91+
"age": age
92+
}
93+
return {
94+
"message": f"Hello, {name}!",
95+
"name": name
96+
}
97+
```
98+
99+
## Union Types
100+
101+
Tools can accept multiple input types:
102+
103+
```python
104+
from typing import Annotated, TypedDict
105+
106+
class StructInfo(TypedDict):
107+
name: Annotated[str, "Structure name"]
108+
size: Annotated[int, "Structure size in bytes"]
109+
fields: Annotated[list[str], "List of field names"]
110+
111+
@mcp.tool
112+
def struct_get(
113+
names: Annotated[list[str], "Array of structure names"]
114+
| Annotated[str, "Single structure name"]
115+
) -> list[StructInfo]:
116+
"""Retrieve structure information by names"""
117+
return [
118+
{
119+
"name": name,
120+
"size": 128,
121+
"fields": ["field1", "field2", "field3"]
122+
}
123+
for name in (names if isinstance(names, list) else [names])
124+
]
125+
```
126+
127+
## Error Handling
128+
129+
```python
130+
from zeromcp import McpToolError
131+
132+
@mcp.tool
133+
def divide(
134+
numerator: Annotated[float, "Numerator"],
135+
denominator: Annotated[float, "Denominator"]
136+
) -> float:
137+
"""Divide two numbers"""
138+
if denominator == 0:
139+
raise McpToolError("Division by zero")
140+
return numerator / denominator
141+
```

examples/mcp_example.py

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
"""Example MCP server with test tools"""
2+
import time
3+
from typing import Annotated, Optional, TypedDict, NotRequired
4+
from zeromcp import McpToolError, McpServer
5+
6+
mcp = McpServer("example")
7+
8+
class SystemInfo(TypedDict):
9+
platform: Annotated[str, "Operating system platform"]
10+
python_version: Annotated[str, "Python version"]
11+
machine: Annotated[str, "Machine architecture"]
12+
timestamp: Annotated[float, "Current timestamp"]
13+
14+
class GreetingResponse(TypedDict):
15+
message: Annotated[str, "Greeting message"]
16+
name: Annotated[str, "Name that was greeted"]
17+
age: Annotated[NotRequired[int], "Age if provided"]
18+
19+
@mcp.tool
20+
def divide(
21+
numerator: Annotated[float, "Numerator"],
22+
denominator: Annotated[float, "Denominator"]
23+
) -> float:
24+
"""Divide two numbers (no zero check - tests natural exceptions)"""
25+
return numerator / denominator
26+
27+
@mcp.tool
28+
def greet(
29+
name: Annotated[str, "Name to greet"],
30+
age: Annotated[Optional[int], "Age of person"] = None
31+
) -> GreetingResponse:
32+
"""Generate a greeting message"""
33+
if age is not None:
34+
return {
35+
"message": f"Hello, {name}! You are {age} years old.",
36+
"name": name,
37+
"age": age
38+
}
39+
return {
40+
"message": f"Hello, {name}!",
41+
"name": name
42+
}
43+
44+
@mcp.tool
45+
def get_system_info() -> SystemInfo:
46+
"""Get system information"""
47+
import platform
48+
return {
49+
"platform": platform.system(),
50+
"python_version": platform.python_version(),
51+
"machine": platform.machine(),
52+
"timestamp": time.time()
53+
}
54+
55+
@mcp.tool
56+
def failing_tool(message: Annotated[str, "Error message to raise"]) -> str:
57+
"""Tool that always fails (for testing error handling)"""
58+
raise McpToolError(message)
59+
60+
class StructInfo(TypedDict):
61+
name: Annotated[str, "Structure name"]
62+
size: Annotated[int, "Structure size in bytes"]
63+
fields: Annotated[list[str], "List of field names"]
64+
65+
@mcp.tool
66+
def struct_get(
67+
names: Annotated[list[str], "Array of structure names"]
68+
| Annotated[str, "Single structure name"]
69+
) -> list[StructInfo]:
70+
"""Retrieve structure information by names"""
71+
return [
72+
StructInfo({
73+
"name": name,
74+
"size": 128, # Dummy size
75+
"fields": ["field1", "field2", "field3"] # Dummy fields
76+
})
77+
for name in (names if isinstance(names, list) else [names])
78+
]
79+
80+
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__}")
86+
87+
mcp.start("127.0.0.1", 5001)
88+
89+
print("\n" + "="*60)
90+
print("Server is running. Press Ctrl+C to stop.")
91+
print("="*60)
92+
93+
try:
94+
while True:
95+
time.sleep(1)
96+
except KeyboardInterrupt:
97+
print("\n\nStopping server...")
98+
mcp.stop()
99+
print("Server stopped.")

pyproject.toml

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
[project]
2+
name = "zeromcp"
3+
version = "0.1.0"
4+
description = "Zero-dependency MCP server implementation"
5+
readme = "README.md"
6+
requires-python = ">=3.11"
7+
dependencies = []
8+
license = "MIT"
9+
10+
[project.urls]
11+
Homepage = "https://github.com/mrexodia/zeromcp"
12+
Repository = "https://github.com/mrexodia/zeromcp"
13+
Issues = "https://github.com/mrexodia/zeromcp/issues"
14+
15+
[build-system]
16+
requires = ["hatchling"]
17+
build-backend = "hatchling.build"

src/zeromcp/__init__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
from .mcp import McpServer, McpToolError
2+
3+
__all__ = ["McpServer", "McpToolError"]

0 commit comments

Comments
 (0)