anodize-mcp 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.
- anodize_mcp-0.1.0/LICENSE +21 -0
- anodize_mcp-0.1.0/PKG-INFO +279 -0
- anodize_mcp-0.1.0/README.md +249 -0
- anodize_mcp-0.1.0/pyproject.toml +76 -0
- anodize_mcp-0.1.0/setup.cfg +4 -0
- anodize_mcp-0.1.0/src/anodize_mcp/__init__.py +74 -0
- anodize_mcp-0.1.0/src/anodize_mcp/_asyncrun.py +62 -0
- anodize_mcp-0.1.0/src/anodize_mcp/_compat.py +55 -0
- anodize_mcp-0.1.0/src/anodize_mcp/_deferred.py +71 -0
- anodize_mcp-0.1.0/src/anodize_mcp/clientfeatures.py +107 -0
- anodize_mcp-0.1.0/src/anodize_mcp/content.py +230 -0
- anodize_mcp-0.1.0/src/anodize_mcp/context.py +233 -0
- anodize_mcp-0.1.0/src/anodize_mcp/exceptions.py +63 -0
- anodize_mcp-0.1.0/src/anodize_mcp/models.py +197 -0
- anodize_mcp-0.1.0/src/anodize_mcp/pagination.py +37 -0
- anodize_mcp-0.1.0/src/anodize_mcp/protocol.py +88 -0
- anodize_mcp-0.1.0/src/anodize_mcp/py.typed +0 -0
- anodize_mcp-0.1.0/src/anodize_mcp/schema.py +707 -0
- anodize_mcp-0.1.0/src/anodize_mcp/server.py +626 -0
- anodize_mcp-0.1.0/src/anodize_mcp/session.py +136 -0
- anodize_mcp-0.1.0/src/anodize_mcp/transports/__init__.py +1 -0
- anodize_mcp-0.1.0/src/anodize_mcp/transports/http.py +309 -0
- anodize_mcp-0.1.0/src/anodize_mcp/transports/stdio.py +84 -0
- anodize_mcp-0.1.0/src/anodize_mcp.egg-info/PKG-INFO +279 -0
- anodize_mcp-0.1.0/src/anodize_mcp.egg-info/SOURCES.txt +32 -0
- anodize_mcp-0.1.0/src/anodize_mcp.egg-info/dependency_links.txt +1 -0
- anodize_mcp-0.1.0/src/anodize_mcp.egg-info/requires.txt +4 -0
- anodize_mcp-0.1.0/src/anodize_mcp.egg-info/top_level.txt +1 -0
- anodize_mcp-0.1.0/tests/test_compat.py +147 -0
- anodize_mcp-0.1.0/tests/test_dispatch.py +261 -0
- anodize_mcp-0.1.0/tests/test_features.py +350 -0
- anodize_mcp-0.1.0/tests/test_http.py +190 -0
- anodize_mcp-0.1.0/tests/test_schema.py +292 -0
- anodize_mcp-0.1.0/tests/test_stdio.py +91 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Adam Munawar Rahman
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: anodize-mcp
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A lightweight, pure-Python MCP (Model Context Protocol) server framework with zero dependencies and no Rust toolchain required.
|
|
5
|
+
Author: Adam Munawar Rahman
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/msradam/anodize-mcp
|
|
8
|
+
Project-URL: Repository, https://github.com/msradam/anodize-mcp
|
|
9
|
+
Project-URL: Issues, https://github.com/msradam/anodize-mcp/issues
|
|
10
|
+
Keywords: mcp,model-context-protocol,llm,pure-python,no-rust,stdlib,zero-dependency
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Operating System :: OS Independent
|
|
15
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
21
|
+
Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
|
|
22
|
+
Classifier: Typing :: Typed
|
|
23
|
+
Requires-Python: >=3.9
|
|
24
|
+
Description-Content-Type: text/markdown
|
|
25
|
+
License-File: LICENSE
|
|
26
|
+
Provides-Extra: dev
|
|
27
|
+
Requires-Dist: ruff; extra == "dev"
|
|
28
|
+
Requires-Dist: mypy; extra == "dev"
|
|
29
|
+
Dynamic: license-file
|
|
30
|
+
|
|
31
|
+
# anodize-mcp
|
|
32
|
+
|
|
33
|
+
A lightweight, pure-Python implementation of the [Model Context Protocol](https://modelcontextprotocol.io) (MCP) server framework. Standard library only, zero third-party dependencies, and no Rust toolchain required.
|
|
34
|
+
|
|
35
|
+
The official MCP SDK and FastMCP both depend on `pydantic`, which depends on `pydantic-core` (compiled Rust). That dependency has no prebuilt wheel for many targets and cannot be compiled where a Rust toolchain is unavailable or disallowed. anodize fills that gap: it implements the same FastMCP-style API using only `json`, `http.server`, `threading`, `dataclasses`, and `typing` from the standard library. The server class is `AnodizeMCP`, also exported as `FastMCP` so switching later is a one-line import change.
|
|
36
|
+
|
|
37
|
+
## Why it exists
|
|
38
|
+
|
|
39
|
+
The barrier is specific: a Rust-based package with no prebuilt wheel for your platform and no way to build one because there is no Rust toolchain. `pydantic-core` (under both FastMCP and the official SDK) is the clearest case. anodize has no compiled dependencies at all, so it installs where those cannot:
|
|
40
|
+
|
|
41
|
+
- **z/OS** (the sharpest case): IBM's Open Enterprise SDK for Python bundles `cryptography` and `numpy`, but there is no `rustc` targeting z/OS, so `pydantic-core` cannot be built or installed. anodize uses only `json`, `http.server`, `threading`, `dataclasses`, and `typing`.
|
|
42
|
+
- **Linux on IBM Z (s390x), AIX, Solaris/illumos, the BSDs, Cygwin** where prebuilt wheels are often absent (on s390x Linux you can build from source, slowly; anodize skips the build).
|
|
43
|
+
- **Exotic or older CPU architectures**: ppc64le, riscv64, ARMv6/v7, mips, sparc.
|
|
44
|
+
- **WebAssembly** (Pyodide, PyScript) and **locked-down or air-gapped build environments** with no compiler, no network, or a no-Rust policy.
|
|
45
|
+
|
|
46
|
+
| | Third-party deps | Compiled deps | Installs without a build toolchain |
|
|
47
|
+
|---|---|---|---|
|
|
48
|
+
| Official `mcp` SDK | pydantic, anyio, httpx, starlette, uvicorn | pydantic-core (Rust) | no |
|
|
49
|
+
| FastMCP | pydantic + many | pydantic-core (Rust) | no |
|
|
50
|
+
| `pure-mcp` | pydantic, anyio, httpx, jsonschema | pydantic-core (Rust) | no |
|
|
51
|
+
| **anodize** | none | none | **yes** |
|
|
52
|
+
|
|
53
|
+
## Install
|
|
54
|
+
|
|
55
|
+
```sh
|
|
56
|
+
pip install anodize-mcp
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Requires Python 3.9 or newer. There are no other dependencies.
|
|
60
|
+
|
|
61
|
+
## Quickstart
|
|
62
|
+
|
|
63
|
+
```python
|
|
64
|
+
from anodize_mcp import AnodizeMCP
|
|
65
|
+
|
|
66
|
+
mcp = AnodizeMCP("demo", instructions="A small demo server.")
|
|
67
|
+
|
|
68
|
+
@mcp.tool
|
|
69
|
+
def add(a: int, b: int) -> int:
|
|
70
|
+
"Add two numbers."
|
|
71
|
+
return a + b
|
|
72
|
+
|
|
73
|
+
@mcp.resource("config://app")
|
|
74
|
+
def config() -> str:
|
|
75
|
+
return '{"theme": "dark"}'
|
|
76
|
+
|
|
77
|
+
@mcp.prompt
|
|
78
|
+
def review(code: str) -> str:
|
|
79
|
+
return f"Review this code:\n\n{code}"
|
|
80
|
+
|
|
81
|
+
if __name__ == "__main__":
|
|
82
|
+
mcp.run() # stdio transport
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Tool input schemas are generated from type hints. Supported types include the primitives, `Optional`/`Union`, `list`/`dict`/`set`/`tuple`, `Literal`, `Enum`, dataclasses, and stdlib types (`datetime`, `date`, `UUID`, `Decimal`). Arguments are validated and coerced at call time. Constraints come from `Annotated`:
|
|
86
|
+
|
|
87
|
+
```python
|
|
88
|
+
from typing import Annotated
|
|
89
|
+
from anodize_mcp import Field
|
|
90
|
+
|
|
91
|
+
@mcp.tool
|
|
92
|
+
def scale(factor: Annotated[float, Field(ge=0, le=10, description="0 to 10")]) -> float:
|
|
93
|
+
return factor * 2
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
A dataclass return value produces an `outputSchema` and `structuredContent` automatically.
|
|
97
|
+
|
|
98
|
+
## Drop-in compatibility with FastMCP
|
|
99
|
+
|
|
100
|
+
The intended workflow: build your server with AnodizeMCP today on a platform
|
|
101
|
+
where Rust is unavailable, and if Rust later becomes available, switch to
|
|
102
|
+
FastMCP by changing one import line.
|
|
103
|
+
|
|
104
|
+
The class is exported as `FastMCP`, and the decorator and `Context` APIs match
|
|
105
|
+
FastMCP's:
|
|
106
|
+
|
|
107
|
+
```python
|
|
108
|
+
from anodize_mcp import FastMCP, Context # later: from fastmcp import FastMCP
|
|
109
|
+
|
|
110
|
+
mcp = FastMCP("demo", instructions="...")
|
|
111
|
+
|
|
112
|
+
@mcp.tool
|
|
113
|
+
async def summarize(text: str, ctx: Context) -> str:
|
|
114
|
+
await ctx.info("summarizing")
|
|
115
|
+
result = await ctx.sample(text, system_prompt="Be concise.")
|
|
116
|
+
return result.text
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
To stay portable both directions, write FastMCP's async style: `async def`
|
|
120
|
+
handlers and `await ctx.*`. anodize's `Context` methods are awaitable for this
|
|
121
|
+
reason (they also work without `await`, as a convenience, but that sync-only
|
|
122
|
+
form does not port back to FastMCP).
|
|
123
|
+
|
|
124
|
+
This is checked against the official `mcp` reference client: the same client
|
|
125
|
+
driving a FastMCP server and an AnodizeMCP server (identical bodies, only the
|
|
126
|
+
import differs) sees matching tool descriptions, input schemas
|
|
127
|
+
(`additionalProperties: false`, parameter `default`s), and structured output
|
|
128
|
+
(scalar returns wrapped as `{"result": value}` with an `outputSchema`, like
|
|
129
|
+
FastMCP).
|
|
130
|
+
|
|
131
|
+
### What ports unchanged
|
|
132
|
+
|
|
133
|
+
- `FastMCP(name, instructions=..., version=...)`; `@mcp.tool`, `@mcp.resource`, `@mcp.prompt` with `name`/`title`/`description`/`annotations`/`tags`; `add_tool`/`add_resource`/`add_prompt`
|
|
134
|
+
- `ctx: Context` injection; `await ctx.debug/info/notice/warning/error(...)`, `ctx.log(message, level=...)`, `report_progress`, `read_resource`, `list_resources`, `list_prompts`, `get_prompt`, `get_state/set_state/delete_state`, `sample` (result `.text`), `elicit(message, dataclass)` (result `.action`/`.data`), `list_roots`; `ctx.session_id`/`client_id`/`request_id`
|
|
135
|
+
- Parameter types: primitives, `Optional`/`Union`/`Literal`/`Enum`, `list`/`dict`/`set`/`tuple`, `datetime`/`date`/`UUID`/`Decimal`, dataclasses, and constraints via either anodize's `Field` or **`pydantic.Field`/`annotated_types`** (`Annotated[int, Field(ge=0)]` validates)
|
|
136
|
+
- Return types: `str`, numbers, `dict`, `list`, dataclasses, `bytes`, `None`, and content blocks (`TextContent`, `ImageContent`, ...)
|
|
137
|
+
- `mcp.run(transport="stdio"|"http", host=..., port=...)`
|
|
138
|
+
|
|
139
|
+
### What does not port (use the alternative, or it is unsupported)
|
|
140
|
+
|
|
141
|
+
| FastMCP feature | On anodize |
|
|
142
|
+
|---|---|
|
|
143
|
+
| `pydantic.BaseModel` as a tool parameter | Use a `@dataclass` instead (BaseModel params are the one hard break) |
|
|
144
|
+
| `from fastmcp.exceptions import ToolError` | `from anodize_mcp import ToolError` (one import line) |
|
|
145
|
+
| `mcp.mount` / `import_server` / server composition | Not supported |
|
|
146
|
+
| `@mcp.custom_route`, middleware, auth providers | Not supported |
|
|
147
|
+
| `@mcp.tool(task=True)` background tasks | Not supported |
|
|
148
|
+
| `transport="sse"` (deprecated) | Raises a clear error; use `"http"` |
|
|
149
|
+
|
|
150
|
+
The other expected difference is the negotiated protocol revision: AnodizeMCP
|
|
151
|
+
implements `2025-06-18` and negotiates down gracefully if the client offers a
|
|
152
|
+
newer one.
|
|
153
|
+
|
|
154
|
+
## Protocol coverage
|
|
155
|
+
|
|
156
|
+
Implements MCP protocol revision `2025-06-18`.
|
|
157
|
+
|
|
158
|
+
| Area | Methods |
|
|
159
|
+
|---|---|
|
|
160
|
+
| Lifecycle | `initialize`, `notifications/initialized`, `ping` |
|
|
161
|
+
| Tools | `tools/list` (paginated), `tools/call`, `notifications/tools/list_changed` |
|
|
162
|
+
| Resources | `resources/list`, `resources/read`, `resources/templates/list`, `resources/subscribe`, `resources/unsubscribe`, `notifications/resources/updated`, `notifications/resources/list_changed` |
|
|
163
|
+
| Prompts | `prompts/list`, `prompts/get`, `notifications/prompts/list_changed` |
|
|
164
|
+
| Completions | `completion/complete` |
|
|
165
|
+
| Logging | `logging/setLevel`, `notifications/message` |
|
|
166
|
+
| Progress | `notifications/progress` |
|
|
167
|
+
| Sampling | `sampling/createMessage` (server to client) |
|
|
168
|
+
| Elicitation | `elicitation/create` (server to client) |
|
|
169
|
+
| Roots | `roots/list` (server to client) |
|
|
170
|
+
|
|
171
|
+
## Context
|
|
172
|
+
|
|
173
|
+
A handler receives a `Context` by declaring a parameter annotated as `Context`. It is excluded from the input schema and injected at call time.
|
|
174
|
+
|
|
175
|
+
```python
|
|
176
|
+
from anodize_mcp import Context
|
|
177
|
+
|
|
178
|
+
@mcp.tool
|
|
179
|
+
def review(code: str, ctx: Context) -> str:
|
|
180
|
+
ctx.info("starting review")
|
|
181
|
+
result = ctx.sample(f"Review:\n{code}", system_prompt="Be terse.")
|
|
182
|
+
return result.text
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
Context provides:
|
|
186
|
+
|
|
187
|
+
- Logging: `ctx.debug/info/notice/warning/error(...)`. The default level is `info`; the client narrows it with `logging/setLevel`.
|
|
188
|
+
- Progress: `ctx.report_progress(progress, total=..., message=...)`.
|
|
189
|
+
- Reading resources: `ctx.read_resource(uri)`.
|
|
190
|
+
- Sampling: `ctx.sample(messages, system_prompt=..., max_tokens=...)` asks the client's LLM. `messages` is a string, a single message dict, or a list of either.
|
|
191
|
+
- Elicitation: `ctx.elicit(message, schema)` asks the user, where `schema` is a JSON Schema dict or a dataclass.
|
|
192
|
+
- Roots: `ctx.list_roots()` returns the client's filesystem roots.
|
|
193
|
+
|
|
194
|
+
`sample`, `elicit`, and `list_roots` are server-to-client requests: the handler blocks until the client responds. They require the client to have declared the matching capability, otherwise they raise an error.
|
|
195
|
+
|
|
196
|
+
A tool can return a string (text), a dataclass (structured output), or content blocks built directly:
|
|
197
|
+
|
|
198
|
+
```python
|
|
199
|
+
from anodize_mcp import TextContent, ImageContent
|
|
200
|
+
|
|
201
|
+
@mcp.tool
|
|
202
|
+
def render() -> list:
|
|
203
|
+
return [TextContent(text="caption"), ImageContent.from_bytes(png_bytes, "image/png")]
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
## Transports
|
|
207
|
+
|
|
208
|
+
stdio (default), newline-delimited UTF-8 JSON:
|
|
209
|
+
|
|
210
|
+
```python
|
|
211
|
+
mcp.run() # or mcp.run("stdio")
|
|
212
|
+
mcp.run("stdio", max_workers=8) # thread pool size for concurrent handlers
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
Streamable HTTP, a single endpoint (default `/mcp`) on the standard-library HTTP server:
|
|
216
|
+
|
|
217
|
+
```python
|
|
218
|
+
mcp.run("http", host="127.0.0.1", port=8000) # serves POST/GET on /mcp
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
The HTTP transport validates the `Origin` header (localhost only by default), tracks sessions with `Mcp-Session-Id`, and serves server-to-client messages (progress, logging, sampling) over a GET SSE stream. A client that never opens that GET stream will not receive those notifications; queued ones are bounded and drop oldest-first. Options:
|
|
222
|
+
|
|
223
|
+
```python
|
|
224
|
+
mcp.run(
|
|
225
|
+
"http",
|
|
226
|
+
host="127.0.0.1",
|
|
227
|
+
port=8000,
|
|
228
|
+
endpoint="/mcp",
|
|
229
|
+
allowed_origins={"localhost", "127.0.0.1"}, # or {"*"} to disable the check
|
|
230
|
+
stateless=False, # True skips session tracking
|
|
231
|
+
)
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
## Completions
|
|
235
|
+
|
|
236
|
+
Register argument completers per prompt or resource template:
|
|
237
|
+
|
|
238
|
+
```python
|
|
239
|
+
@mcp.complete_prompt("review")
|
|
240
|
+
def complete(argument: str, value: str) -> list[str]:
|
|
241
|
+
if argument == "language":
|
|
242
|
+
return [x for x in ("python", "rust", "go") if x.startswith(value)]
|
|
243
|
+
return []
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
A completer may take a third `context` argument (the already-entered values) and may return a `CompletionResult(values=..., total=..., has_more=...)` for explicit totals.
|
|
247
|
+
|
|
248
|
+
## Dynamic changes
|
|
249
|
+
|
|
250
|
+
Registries can change at runtime. Removing an item or calling a notify method broadcasts the corresponding `list_changed` notification to connected clients:
|
|
251
|
+
|
|
252
|
+
```python
|
|
253
|
+
mcp.remove_tool("old_tool") # broadcasts notifications/tools/list_changed
|
|
254
|
+
mcp.notify_resource_updated(uri) # to clients subscribed to that uri
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
## Pagination
|
|
258
|
+
|
|
259
|
+
List endpoints page automatically when a registry exceeds `page_size`:
|
|
260
|
+
|
|
261
|
+
```python
|
|
262
|
+
mcp = AnodizeMCP("demo", page_size=100)
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
Clients receive a `nextCursor` and echo it back. The cursor is opaque.
|
|
266
|
+
|
|
267
|
+
## Development
|
|
268
|
+
|
|
269
|
+
```sh
|
|
270
|
+
uv venv && uv pip install -e ".[dev]"
|
|
271
|
+
python -m unittest discover -s tests
|
|
272
|
+
ruff format . && ruff check . && mypy
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
The test suite uses only the standard library `unittest`.
|
|
276
|
+
|
|
277
|
+
## License
|
|
278
|
+
|
|
279
|
+
MIT. See [LICENSE](LICENSE).
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
# anodize-mcp
|
|
2
|
+
|
|
3
|
+
A lightweight, pure-Python implementation of the [Model Context Protocol](https://modelcontextprotocol.io) (MCP) server framework. Standard library only, zero third-party dependencies, and no Rust toolchain required.
|
|
4
|
+
|
|
5
|
+
The official MCP SDK and FastMCP both depend on `pydantic`, which depends on `pydantic-core` (compiled Rust). That dependency has no prebuilt wheel for many targets and cannot be compiled where a Rust toolchain is unavailable or disallowed. anodize fills that gap: it implements the same FastMCP-style API using only `json`, `http.server`, `threading`, `dataclasses`, and `typing` from the standard library. The server class is `AnodizeMCP`, also exported as `FastMCP` so switching later is a one-line import change.
|
|
6
|
+
|
|
7
|
+
## Why it exists
|
|
8
|
+
|
|
9
|
+
The barrier is specific: a Rust-based package with no prebuilt wheel for your platform and no way to build one because there is no Rust toolchain. `pydantic-core` (under both FastMCP and the official SDK) is the clearest case. anodize has no compiled dependencies at all, so it installs where those cannot:
|
|
10
|
+
|
|
11
|
+
- **z/OS** (the sharpest case): IBM's Open Enterprise SDK for Python bundles `cryptography` and `numpy`, but there is no `rustc` targeting z/OS, so `pydantic-core` cannot be built or installed. anodize uses only `json`, `http.server`, `threading`, `dataclasses`, and `typing`.
|
|
12
|
+
- **Linux on IBM Z (s390x), AIX, Solaris/illumos, the BSDs, Cygwin** where prebuilt wheels are often absent (on s390x Linux you can build from source, slowly; anodize skips the build).
|
|
13
|
+
- **Exotic or older CPU architectures**: ppc64le, riscv64, ARMv6/v7, mips, sparc.
|
|
14
|
+
- **WebAssembly** (Pyodide, PyScript) and **locked-down or air-gapped build environments** with no compiler, no network, or a no-Rust policy.
|
|
15
|
+
|
|
16
|
+
| | Third-party deps | Compiled deps | Installs without a build toolchain |
|
|
17
|
+
|---|---|---|---|
|
|
18
|
+
| Official `mcp` SDK | pydantic, anyio, httpx, starlette, uvicorn | pydantic-core (Rust) | no |
|
|
19
|
+
| FastMCP | pydantic + many | pydantic-core (Rust) | no |
|
|
20
|
+
| `pure-mcp` | pydantic, anyio, httpx, jsonschema | pydantic-core (Rust) | no |
|
|
21
|
+
| **anodize** | none | none | **yes** |
|
|
22
|
+
|
|
23
|
+
## Install
|
|
24
|
+
|
|
25
|
+
```sh
|
|
26
|
+
pip install anodize-mcp
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Requires Python 3.9 or newer. There are no other dependencies.
|
|
30
|
+
|
|
31
|
+
## Quickstart
|
|
32
|
+
|
|
33
|
+
```python
|
|
34
|
+
from anodize_mcp import AnodizeMCP
|
|
35
|
+
|
|
36
|
+
mcp = AnodizeMCP("demo", instructions="A small demo server.")
|
|
37
|
+
|
|
38
|
+
@mcp.tool
|
|
39
|
+
def add(a: int, b: int) -> int:
|
|
40
|
+
"Add two numbers."
|
|
41
|
+
return a + b
|
|
42
|
+
|
|
43
|
+
@mcp.resource("config://app")
|
|
44
|
+
def config() -> str:
|
|
45
|
+
return '{"theme": "dark"}'
|
|
46
|
+
|
|
47
|
+
@mcp.prompt
|
|
48
|
+
def review(code: str) -> str:
|
|
49
|
+
return f"Review this code:\n\n{code}"
|
|
50
|
+
|
|
51
|
+
if __name__ == "__main__":
|
|
52
|
+
mcp.run() # stdio transport
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Tool input schemas are generated from type hints. Supported types include the primitives, `Optional`/`Union`, `list`/`dict`/`set`/`tuple`, `Literal`, `Enum`, dataclasses, and stdlib types (`datetime`, `date`, `UUID`, `Decimal`). Arguments are validated and coerced at call time. Constraints come from `Annotated`:
|
|
56
|
+
|
|
57
|
+
```python
|
|
58
|
+
from typing import Annotated
|
|
59
|
+
from anodize_mcp import Field
|
|
60
|
+
|
|
61
|
+
@mcp.tool
|
|
62
|
+
def scale(factor: Annotated[float, Field(ge=0, le=10, description="0 to 10")]) -> float:
|
|
63
|
+
return factor * 2
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
A dataclass return value produces an `outputSchema` and `structuredContent` automatically.
|
|
67
|
+
|
|
68
|
+
## Drop-in compatibility with FastMCP
|
|
69
|
+
|
|
70
|
+
The intended workflow: build your server with AnodizeMCP today on a platform
|
|
71
|
+
where Rust is unavailable, and if Rust later becomes available, switch to
|
|
72
|
+
FastMCP by changing one import line.
|
|
73
|
+
|
|
74
|
+
The class is exported as `FastMCP`, and the decorator and `Context` APIs match
|
|
75
|
+
FastMCP's:
|
|
76
|
+
|
|
77
|
+
```python
|
|
78
|
+
from anodize_mcp import FastMCP, Context # later: from fastmcp import FastMCP
|
|
79
|
+
|
|
80
|
+
mcp = FastMCP("demo", instructions="...")
|
|
81
|
+
|
|
82
|
+
@mcp.tool
|
|
83
|
+
async def summarize(text: str, ctx: Context) -> str:
|
|
84
|
+
await ctx.info("summarizing")
|
|
85
|
+
result = await ctx.sample(text, system_prompt="Be concise.")
|
|
86
|
+
return result.text
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
To stay portable both directions, write FastMCP's async style: `async def`
|
|
90
|
+
handlers and `await ctx.*`. anodize's `Context` methods are awaitable for this
|
|
91
|
+
reason (they also work without `await`, as a convenience, but that sync-only
|
|
92
|
+
form does not port back to FastMCP).
|
|
93
|
+
|
|
94
|
+
This is checked against the official `mcp` reference client: the same client
|
|
95
|
+
driving a FastMCP server and an AnodizeMCP server (identical bodies, only the
|
|
96
|
+
import differs) sees matching tool descriptions, input schemas
|
|
97
|
+
(`additionalProperties: false`, parameter `default`s), and structured output
|
|
98
|
+
(scalar returns wrapped as `{"result": value}` with an `outputSchema`, like
|
|
99
|
+
FastMCP).
|
|
100
|
+
|
|
101
|
+
### What ports unchanged
|
|
102
|
+
|
|
103
|
+
- `FastMCP(name, instructions=..., version=...)`; `@mcp.tool`, `@mcp.resource`, `@mcp.prompt` with `name`/`title`/`description`/`annotations`/`tags`; `add_tool`/`add_resource`/`add_prompt`
|
|
104
|
+
- `ctx: Context` injection; `await ctx.debug/info/notice/warning/error(...)`, `ctx.log(message, level=...)`, `report_progress`, `read_resource`, `list_resources`, `list_prompts`, `get_prompt`, `get_state/set_state/delete_state`, `sample` (result `.text`), `elicit(message, dataclass)` (result `.action`/`.data`), `list_roots`; `ctx.session_id`/`client_id`/`request_id`
|
|
105
|
+
- Parameter types: primitives, `Optional`/`Union`/`Literal`/`Enum`, `list`/`dict`/`set`/`tuple`, `datetime`/`date`/`UUID`/`Decimal`, dataclasses, and constraints via either anodize's `Field` or **`pydantic.Field`/`annotated_types`** (`Annotated[int, Field(ge=0)]` validates)
|
|
106
|
+
- Return types: `str`, numbers, `dict`, `list`, dataclasses, `bytes`, `None`, and content blocks (`TextContent`, `ImageContent`, ...)
|
|
107
|
+
- `mcp.run(transport="stdio"|"http", host=..., port=...)`
|
|
108
|
+
|
|
109
|
+
### What does not port (use the alternative, or it is unsupported)
|
|
110
|
+
|
|
111
|
+
| FastMCP feature | On anodize |
|
|
112
|
+
|---|---|
|
|
113
|
+
| `pydantic.BaseModel` as a tool parameter | Use a `@dataclass` instead (BaseModel params are the one hard break) |
|
|
114
|
+
| `from fastmcp.exceptions import ToolError` | `from anodize_mcp import ToolError` (one import line) |
|
|
115
|
+
| `mcp.mount` / `import_server` / server composition | Not supported |
|
|
116
|
+
| `@mcp.custom_route`, middleware, auth providers | Not supported |
|
|
117
|
+
| `@mcp.tool(task=True)` background tasks | Not supported |
|
|
118
|
+
| `transport="sse"` (deprecated) | Raises a clear error; use `"http"` |
|
|
119
|
+
|
|
120
|
+
The other expected difference is the negotiated protocol revision: AnodizeMCP
|
|
121
|
+
implements `2025-06-18` and negotiates down gracefully if the client offers a
|
|
122
|
+
newer one.
|
|
123
|
+
|
|
124
|
+
## Protocol coverage
|
|
125
|
+
|
|
126
|
+
Implements MCP protocol revision `2025-06-18`.
|
|
127
|
+
|
|
128
|
+
| Area | Methods |
|
|
129
|
+
|---|---|
|
|
130
|
+
| Lifecycle | `initialize`, `notifications/initialized`, `ping` |
|
|
131
|
+
| Tools | `tools/list` (paginated), `tools/call`, `notifications/tools/list_changed` |
|
|
132
|
+
| Resources | `resources/list`, `resources/read`, `resources/templates/list`, `resources/subscribe`, `resources/unsubscribe`, `notifications/resources/updated`, `notifications/resources/list_changed` |
|
|
133
|
+
| Prompts | `prompts/list`, `prompts/get`, `notifications/prompts/list_changed` |
|
|
134
|
+
| Completions | `completion/complete` |
|
|
135
|
+
| Logging | `logging/setLevel`, `notifications/message` |
|
|
136
|
+
| Progress | `notifications/progress` |
|
|
137
|
+
| Sampling | `sampling/createMessage` (server to client) |
|
|
138
|
+
| Elicitation | `elicitation/create` (server to client) |
|
|
139
|
+
| Roots | `roots/list` (server to client) |
|
|
140
|
+
|
|
141
|
+
## Context
|
|
142
|
+
|
|
143
|
+
A handler receives a `Context` by declaring a parameter annotated as `Context`. It is excluded from the input schema and injected at call time.
|
|
144
|
+
|
|
145
|
+
```python
|
|
146
|
+
from anodize_mcp import Context
|
|
147
|
+
|
|
148
|
+
@mcp.tool
|
|
149
|
+
def review(code: str, ctx: Context) -> str:
|
|
150
|
+
ctx.info("starting review")
|
|
151
|
+
result = ctx.sample(f"Review:\n{code}", system_prompt="Be terse.")
|
|
152
|
+
return result.text
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
Context provides:
|
|
156
|
+
|
|
157
|
+
- Logging: `ctx.debug/info/notice/warning/error(...)`. The default level is `info`; the client narrows it with `logging/setLevel`.
|
|
158
|
+
- Progress: `ctx.report_progress(progress, total=..., message=...)`.
|
|
159
|
+
- Reading resources: `ctx.read_resource(uri)`.
|
|
160
|
+
- Sampling: `ctx.sample(messages, system_prompt=..., max_tokens=...)` asks the client's LLM. `messages` is a string, a single message dict, or a list of either.
|
|
161
|
+
- Elicitation: `ctx.elicit(message, schema)` asks the user, where `schema` is a JSON Schema dict or a dataclass.
|
|
162
|
+
- Roots: `ctx.list_roots()` returns the client's filesystem roots.
|
|
163
|
+
|
|
164
|
+
`sample`, `elicit`, and `list_roots` are server-to-client requests: the handler blocks until the client responds. They require the client to have declared the matching capability, otherwise they raise an error.
|
|
165
|
+
|
|
166
|
+
A tool can return a string (text), a dataclass (structured output), or content blocks built directly:
|
|
167
|
+
|
|
168
|
+
```python
|
|
169
|
+
from anodize_mcp import TextContent, ImageContent
|
|
170
|
+
|
|
171
|
+
@mcp.tool
|
|
172
|
+
def render() -> list:
|
|
173
|
+
return [TextContent(text="caption"), ImageContent.from_bytes(png_bytes, "image/png")]
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
## Transports
|
|
177
|
+
|
|
178
|
+
stdio (default), newline-delimited UTF-8 JSON:
|
|
179
|
+
|
|
180
|
+
```python
|
|
181
|
+
mcp.run() # or mcp.run("stdio")
|
|
182
|
+
mcp.run("stdio", max_workers=8) # thread pool size for concurrent handlers
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
Streamable HTTP, a single endpoint (default `/mcp`) on the standard-library HTTP server:
|
|
186
|
+
|
|
187
|
+
```python
|
|
188
|
+
mcp.run("http", host="127.0.0.1", port=8000) # serves POST/GET on /mcp
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
The HTTP transport validates the `Origin` header (localhost only by default), tracks sessions with `Mcp-Session-Id`, and serves server-to-client messages (progress, logging, sampling) over a GET SSE stream. A client that never opens that GET stream will not receive those notifications; queued ones are bounded and drop oldest-first. Options:
|
|
192
|
+
|
|
193
|
+
```python
|
|
194
|
+
mcp.run(
|
|
195
|
+
"http",
|
|
196
|
+
host="127.0.0.1",
|
|
197
|
+
port=8000,
|
|
198
|
+
endpoint="/mcp",
|
|
199
|
+
allowed_origins={"localhost", "127.0.0.1"}, # or {"*"} to disable the check
|
|
200
|
+
stateless=False, # True skips session tracking
|
|
201
|
+
)
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
## Completions
|
|
205
|
+
|
|
206
|
+
Register argument completers per prompt or resource template:
|
|
207
|
+
|
|
208
|
+
```python
|
|
209
|
+
@mcp.complete_prompt("review")
|
|
210
|
+
def complete(argument: str, value: str) -> list[str]:
|
|
211
|
+
if argument == "language":
|
|
212
|
+
return [x for x in ("python", "rust", "go") if x.startswith(value)]
|
|
213
|
+
return []
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
A completer may take a third `context` argument (the already-entered values) and may return a `CompletionResult(values=..., total=..., has_more=...)` for explicit totals.
|
|
217
|
+
|
|
218
|
+
## Dynamic changes
|
|
219
|
+
|
|
220
|
+
Registries can change at runtime. Removing an item or calling a notify method broadcasts the corresponding `list_changed` notification to connected clients:
|
|
221
|
+
|
|
222
|
+
```python
|
|
223
|
+
mcp.remove_tool("old_tool") # broadcasts notifications/tools/list_changed
|
|
224
|
+
mcp.notify_resource_updated(uri) # to clients subscribed to that uri
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
## Pagination
|
|
228
|
+
|
|
229
|
+
List endpoints page automatically when a registry exceeds `page_size`:
|
|
230
|
+
|
|
231
|
+
```python
|
|
232
|
+
mcp = AnodizeMCP("demo", page_size=100)
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
Clients receive a `nextCursor` and echo it back. The cursor is opaque.
|
|
236
|
+
|
|
237
|
+
## Development
|
|
238
|
+
|
|
239
|
+
```sh
|
|
240
|
+
uv venv && uv pip install -e ".[dev]"
|
|
241
|
+
python -m unittest discover -s tests
|
|
242
|
+
ruff format . && ruff check . && mypy
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
The test suite uses only the standard library `unittest`.
|
|
246
|
+
|
|
247
|
+
## License
|
|
248
|
+
|
|
249
|
+
MIT. See [LICENSE](LICENSE).
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "anodize-mcp"
|
|
7
|
+
dynamic = ["version"]
|
|
8
|
+
description = "A lightweight, pure-Python MCP (Model Context Protocol) server framework with zero dependencies and no Rust toolchain required."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.9"
|
|
11
|
+
license = { text = "MIT" }
|
|
12
|
+
authors = [{ name = "Adam Munawar Rahman" }]
|
|
13
|
+
keywords = [
|
|
14
|
+
"mcp",
|
|
15
|
+
"model-context-protocol",
|
|
16
|
+
"llm",
|
|
17
|
+
"pure-python",
|
|
18
|
+
"no-rust",
|
|
19
|
+
"stdlib",
|
|
20
|
+
"zero-dependency",
|
|
21
|
+
]
|
|
22
|
+
classifiers = [
|
|
23
|
+
"Development Status :: 4 - Beta",
|
|
24
|
+
"Intended Audience :: Developers",
|
|
25
|
+
"License :: OSI Approved :: MIT License",
|
|
26
|
+
"Operating System :: OS Independent",
|
|
27
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
28
|
+
"Programming Language :: Python :: 3.9",
|
|
29
|
+
"Programming Language :: Python :: 3.10",
|
|
30
|
+
"Programming Language :: Python :: 3.11",
|
|
31
|
+
"Programming Language :: Python :: 3.12",
|
|
32
|
+
"Programming Language :: Python :: 3.13",
|
|
33
|
+
"Topic :: Software Development :: Libraries :: Application Frameworks",
|
|
34
|
+
"Typing :: Typed",
|
|
35
|
+
]
|
|
36
|
+
dependencies = []
|
|
37
|
+
|
|
38
|
+
[project.urls]
|
|
39
|
+
Homepage = "https://github.com/msradam/anodize-mcp"
|
|
40
|
+
Repository = "https://github.com/msradam/anodize-mcp"
|
|
41
|
+
Issues = "https://github.com/msradam/anodize-mcp/issues"
|
|
42
|
+
|
|
43
|
+
[project.optional-dependencies]
|
|
44
|
+
dev = ["ruff", "mypy"]
|
|
45
|
+
|
|
46
|
+
[tool.setuptools.dynamic]
|
|
47
|
+
version = { attr = "anodize_mcp.__version__" }
|
|
48
|
+
|
|
49
|
+
[tool.setuptools.packages.find]
|
|
50
|
+
where = ["src"]
|
|
51
|
+
|
|
52
|
+
[tool.setuptools.package-data]
|
|
53
|
+
anodize_mcp = ["py.typed"]
|
|
54
|
+
|
|
55
|
+
[tool.ruff]
|
|
56
|
+
line-length = 100
|
|
57
|
+
target-version = "py39"
|
|
58
|
+
src = ["src", "tests"]
|
|
59
|
+
|
|
60
|
+
[tool.ruff.lint]
|
|
61
|
+
select = ["E", "F", "I", "UP", "B", "FURB", "SIM"]
|
|
62
|
+
# This library targets Python 3.9, where `X | Y` unions are unsafe in
|
|
63
|
+
# runtime-evaluated positions (type aliases, cast targets). Keep Optional/Union.
|
|
64
|
+
ignore = ["E501", "UP007", "UP045", "UP035", "UP006"]
|
|
65
|
+
|
|
66
|
+
[tool.mypy]
|
|
67
|
+
# Runtime floor is 3.9 (enforced by ruff target-version); mypy itself no longer
|
|
68
|
+
# accepts python_version 3.9, so type-check against 3.10. The code avoids
|
|
69
|
+
# 3.10-only runtime features via `from __future__ import annotations`.
|
|
70
|
+
python_version = "3.10"
|
|
71
|
+
files = ["src"]
|
|
72
|
+
warn_unused_ignores = true
|
|
73
|
+
warn_redundant_casts = true
|
|
74
|
+
no_implicit_optional = true
|
|
75
|
+
check_untyped_defs = true
|
|
76
|
+
disallow_incomplete_defs = true
|