zeromcp 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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
@@ -0,0 +1 @@
1
+ 3.11
zeromcp-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,152 @@
1
+ Metadata-Version: 2.4
2
+ Name: zeromcp
3
+ Version: 0.1.0
4
+ Summary: Zero-dependency MCP server implementation
5
+ Project-URL: Homepage, https://github.com/mrexodia/zeromcp
6
+ Project-URL: Repository, https://github.com/mrexodia/zeromcp
7
+ Project-URL: Issues, https://github.com/mrexodia/zeromcp/issues
8
+ License-Expression: MIT
9
+ Requires-Python: >=3.11
10
+ Description-Content-Type: text/markdown
11
+
12
+ # zeromcp
13
+
14
+ **Minimal MCP server implementation in pure Python.**
15
+
16
+ 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.
17
+
18
+ ## Features
19
+
20
+ - ✨ **Zero dependencies** - Pure Python, standard library only
21
+ - 🎯 **Type-safe** - Native Python type annotations for everything
22
+ - 🚀 **Fast** - Minimal overhead, maximum performance
23
+ - 🛠️ **Handcrafted** - Written by a human, verified against the spec
24
+ - 🌐 **HTTP/SSE transport** - Streamable responses (stdio planned)
25
+ - 📦 **Tiny** - Less than 1,000 lines of code
26
+
27
+ ## Installation
28
+
29
+ ```bash
30
+ pip install zeromcp
31
+ ```
32
+
33
+ Or with uv:
34
+
35
+ ```bash
36
+ uv add zeromcp
37
+ ```
38
+
39
+ ## Quick Start
40
+
41
+ ```python
42
+ from typing import Annotated
43
+ from zeromcp import McpServer
44
+
45
+ mcp = McpServer("my-server")
46
+
47
+ @mcp.tool
48
+ def greet(
49
+ name: Annotated[str, "Name to greet"],
50
+ age: Annotated[int | None, "Age of person"] = None
51
+ ) -> str:
52
+ """Generate a greeting message"""
53
+ if age:
54
+ return f"Hello, {name}! You are {age} years old."
55
+ return f"Hello, {name}!"
56
+
57
+ if __name__ == "__main__":
58
+ mcp.start("127.0.0.1", 8000)
59
+ ```
60
+
61
+ Then manually test your MCP server with the [inspector](https://github.com/modelcontextprotocol/inspector):
62
+
63
+ ```bash
64
+ npx -y @modelcontextprotocol/inspector
65
+ ```
66
+
67
+ Once things are working you can configure the `mcp.json`:
68
+
69
+ ```json
70
+ {
71
+ "mcpServers": {
72
+ "my-server": {
73
+ "type": "http",
74
+ "url": "http://127.0.0.1/mcp"
75
+ }
76
+ }
77
+ }
78
+ ```
79
+
80
+ ## Type Annotations
81
+
82
+ zeromcp uses native Python `Annotated` types for schema generation:
83
+
84
+ ```python
85
+ from typing import Annotated, Optional, TypedDict, NotRequired
86
+
87
+ class GreetingResponse(TypedDict):
88
+ message: Annotated[str, "Greeting message"]
89
+ name: Annotated[str, "Name that was greeted"]
90
+ age: Annotated[NotRequired[int], "Age if provided"]
91
+
92
+ @mcp.tool
93
+ def greet(
94
+ name: Annotated[str, "Name to greet"],
95
+ age: Annotated[Optional[int], "Age of person"] = None
96
+ ) -> GreetingResponse:
97
+ """Generate a greeting message"""
98
+ if age is not None:
99
+ return {
100
+ "message": f"Hello, {name}! You are {age} years old.",
101
+ "name": name,
102
+ "age": age
103
+ }
104
+ return {
105
+ "message": f"Hello, {name}!",
106
+ "name": name
107
+ }
108
+ ```
109
+
110
+ ## Union Types
111
+
112
+ Tools can accept multiple input types:
113
+
114
+ ```python
115
+ from typing import Annotated, TypedDict
116
+
117
+ class StructInfo(TypedDict):
118
+ name: Annotated[str, "Structure name"]
119
+ size: Annotated[int, "Structure size in bytes"]
120
+ fields: Annotated[list[str], "List of field names"]
121
+
122
+ @mcp.tool
123
+ def struct_get(
124
+ names: Annotated[list[str], "Array of structure names"]
125
+ | Annotated[str, "Single structure name"]
126
+ ) -> list[StructInfo]:
127
+ """Retrieve structure information by names"""
128
+ return [
129
+ {
130
+ "name": name,
131
+ "size": 128,
132
+ "fields": ["field1", "field2", "field3"]
133
+ }
134
+ for name in (names if isinstance(names, list) else [names])
135
+ ]
136
+ ```
137
+
138
+ ## Error Handling
139
+
140
+ ```python
141
+ from zeromcp import McpToolError
142
+
143
+ @mcp.tool
144
+ def divide(
145
+ numerator: Annotated[float, "Numerator"],
146
+ denominator: Annotated[float, "Denominator"]
147
+ ) -> float:
148
+ """Divide two numbers"""
149
+ if denominator == 0:
150
+ raise McpToolError("Division by zero")
151
+ return numerator / denominator
152
+ ```
@@ -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
+ ```
@@ -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.")
@@ -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"
@@ -0,0 +1,3 @@
1
+ from .mcp import McpServer, McpToolError
2
+
3
+ __all__ = ["McpServer", "McpToolError"]
@@ -0,0 +1,223 @@
1
+ import json
2
+ import types
3
+ import inspect
4
+ import traceback
5
+ from typing import Any, Callable, get_type_hints, get_origin, get_args, Union, TypedDict, TypeAlias, NotRequired
6
+
7
+ JsonRpcId: TypeAlias = str | int | float | None
8
+ JsonRpcParams: TypeAlias = dict[str, Any] | list[Any] | None
9
+
10
+ class JsonRpcRequest(TypedDict):
11
+ jsonrpc: str
12
+ method: str
13
+ params: NotRequired[JsonRpcParams]
14
+ id: NotRequired[JsonRpcId]
15
+
16
+ class JsonRpcError(TypedDict):
17
+ code: int
18
+ message: str
19
+ data: NotRequired[Any]
20
+
21
+ class JsonRpcResponse(TypedDict):
22
+ jsonrpc: str
23
+ result: NotRequired[Any]
24
+ error: NotRequired[JsonRpcError]
25
+ id: JsonRpcId
26
+
27
+ class JsonRpcException(Exception):
28
+ def __init__(self, code: int, message: str, data: Any = None):
29
+ self.code = code
30
+ self.message = message
31
+ self.data = data
32
+
33
+ class JsonRpcRegistry:
34
+ def __init__(self):
35
+ self.methods: dict[str, Callable] = {}
36
+
37
+ def method(self, func: Callable, name: str | None = None) -> Callable:
38
+ self.methods[name or func.__name__] = func # type: ignore
39
+ return func
40
+
41
+ def dispatch(self, request: dict | str | bytes | bytearray) -> JsonRpcResponse | None:
42
+ try:
43
+ if not isinstance(request, dict):
44
+ request = json.loads(request)
45
+ if not isinstance(request, dict):
46
+ return self._error(None, -32600, "Invalid request: must be a JSON object")
47
+ except Exception as e:
48
+ return self._error(None, -32700, "JSON parse error", str(e))
49
+
50
+ if request.get("jsonrpc") != "2.0":
51
+ return self._error(None, -32600, "Invalid request: 'jsonrpc' must be '2.0'")
52
+
53
+ method = request.get("method")
54
+ if method is None:
55
+ return self._error(None, -32600, "Invalid request: 'method' is required")
56
+ if not isinstance(method, str):
57
+ return self._error(None, -32600, "Invalid request: 'method' must be a string")
58
+
59
+ request_id: JsonRpcId = request.get("id")
60
+ is_notification = "id" not in request
61
+ params: JsonRpcParams = request.get("params")
62
+ try:
63
+ result = self._call(method, params)
64
+ if is_notification:
65
+ return None
66
+ return {
67
+ "jsonrpc": "2.0",
68
+ "result": result,
69
+ "id": request_id,
70
+ }
71
+ except JsonRpcException as e:
72
+ if is_notification:
73
+ return None
74
+ return self._error(request_id, e.code, e.message, e.data)
75
+ except Exception as e:
76
+ if is_notification:
77
+ return None
78
+ error = self.map_exception(e)
79
+ return self._error(request_id, error["code"], error["message"], error.get("data"))
80
+
81
+ def map_exception(self, e: Exception) -> JsonRpcError:
82
+ return {
83
+ "code": -32603,
84
+ "message": "\n".join(traceback.format_exception(e)).strip() + "\n\nPlease report a bug!",
85
+ }
86
+
87
+ def _call(self, method: str, params: Any) -> Any:
88
+ if method not in self.methods:
89
+ raise JsonRpcException(-32601, f"Method '{method}' not found")
90
+
91
+ func = self.methods[method]
92
+ sig = inspect.signature(func)
93
+ hints = get_type_hints(func)
94
+ hints.pop("return", None)
95
+
96
+ # Determine required vs optional parameters
97
+ required_params = []
98
+ for param_name, param in sig.parameters.items():
99
+ if param.default is inspect.Parameter.empty:
100
+ required_params.append(param_name)
101
+
102
+ # Handle None params
103
+ if params is None:
104
+ if len(required_params) == 0:
105
+ return func()
106
+ else:
107
+ raise JsonRpcException(-32602, "Missing required params")
108
+
109
+ # Convert list params to dict by parameter names
110
+ if isinstance(params, list):
111
+ if len(params) < len(required_params):
112
+ raise JsonRpcException(
113
+ -32602,
114
+ f"Invalid params: expected at least {len(required_params)} arguments, got {len(params)}"
115
+ )
116
+ if len(params) > len(hints):
117
+ raise JsonRpcException(
118
+ -32602,
119
+ f"Invalid params: expected at most {len(hints)} arguments, got {len(params)}"
120
+ )
121
+ params = dict(zip(hints.keys(), params))
122
+
123
+ # Validate dict params
124
+ if isinstance(params, dict):
125
+ # Check all required params are present
126
+ missing = set(required_params) - set(params.keys())
127
+ if missing:
128
+ raise JsonRpcException(
129
+ -32602,
130
+ f"Invalid params: missing required parameters: {list(missing)}"
131
+ )
132
+
133
+ # Check no extra params
134
+ extra = set(params.keys()) - set(hints.keys())
135
+ if extra:
136
+ raise JsonRpcException(
137
+ -32602,
138
+ f"Invalid params: unexpected parameters: {list(extra)}"
139
+ )
140
+
141
+ validated_params = {}
142
+ for param_name, expected_type in hints.items():
143
+ if param_name not in params:
144
+ continue # Skip optional params not provided
145
+
146
+ value = params[param_name]
147
+
148
+ # Inline type validation
149
+ origin = get_origin(expected_type)
150
+ args = get_args(expected_type)
151
+
152
+ # Handle None/null
153
+ if value is None:
154
+ if expected_type is not type(None):
155
+ # Check if None is allowed in a Union
156
+ if not (origin is Union and type(None) in args):
157
+ raise JsonRpcException(-32602, f"Invalid params: {param_name} cannot be null")
158
+ validated_params[param_name] = None
159
+ continue
160
+
161
+ # Handle Union types (int | str, Optional[int], etc.)
162
+ if origin is Union or (hasattr(types, 'UnionType') and origin is types.UnionType):
163
+ type_matched = False
164
+ for arg_type in args:
165
+ if arg_type is type(None):
166
+ continue
167
+
168
+ arg_origin = get_origin(arg_type)
169
+ check_type = arg_origin if arg_origin is not None else arg_type
170
+
171
+ if isinstance(value, check_type):
172
+ type_matched = True
173
+ break
174
+
175
+ if not type_matched:
176
+ raise JsonRpcException(-32602, f"Invalid params: {param_name} has invalid type")
177
+ validated_params[param_name] = value
178
+ continue
179
+
180
+ # Handle generic types (list[X], dict[K,V])
181
+ if origin is not None:
182
+ if not isinstance(value, origin):
183
+ raise JsonRpcException(
184
+ -32602,
185
+ f"Invalid params: {param_name} expected {origin.__name__}, got {type(value).__name__}"
186
+ )
187
+ validated_params[param_name] = value
188
+ continue
189
+
190
+ # Handle basic types
191
+ if isinstance(expected_type, type):
192
+ # Allow int -> float conversion
193
+ if expected_type is float and isinstance(value, int):
194
+ validated_params[param_name] = float(value)
195
+ continue
196
+ if not isinstance(value, expected_type):
197
+ raise JsonRpcException(
198
+ -32602,
199
+ f"Invalid params: {param_name} expected {expected_type.__name__}, got {type(value).__name__}"
200
+ )
201
+ validated_params[param_name] = value
202
+ continue
203
+
204
+ # Fallback for Any or unknown
205
+ validated_params[param_name] = value
206
+
207
+ return func(**validated_params)
208
+
209
+ else:
210
+ raise JsonRpcException(-32602, "Invalid params: must be array or object")
211
+
212
+ def _error(self, request_id: JsonRpcId, code: int, message: str, data: Any = None) -> JsonRpcResponse | None:
213
+ error: JsonRpcError = {
214
+ "code": code,
215
+ "message": message,
216
+ }
217
+ if data is not None:
218
+ error["data"] = data
219
+ return {
220
+ "jsonrpc": "2.0",
221
+ "error": error,
222
+ "id": request_id,
223
+ }