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.
Files changed (34) hide show
  1. anodize_mcp-0.1.0/LICENSE +21 -0
  2. anodize_mcp-0.1.0/PKG-INFO +279 -0
  3. anodize_mcp-0.1.0/README.md +249 -0
  4. anodize_mcp-0.1.0/pyproject.toml +76 -0
  5. anodize_mcp-0.1.0/setup.cfg +4 -0
  6. anodize_mcp-0.1.0/src/anodize_mcp/__init__.py +74 -0
  7. anodize_mcp-0.1.0/src/anodize_mcp/_asyncrun.py +62 -0
  8. anodize_mcp-0.1.0/src/anodize_mcp/_compat.py +55 -0
  9. anodize_mcp-0.1.0/src/anodize_mcp/_deferred.py +71 -0
  10. anodize_mcp-0.1.0/src/anodize_mcp/clientfeatures.py +107 -0
  11. anodize_mcp-0.1.0/src/anodize_mcp/content.py +230 -0
  12. anodize_mcp-0.1.0/src/anodize_mcp/context.py +233 -0
  13. anodize_mcp-0.1.0/src/anodize_mcp/exceptions.py +63 -0
  14. anodize_mcp-0.1.0/src/anodize_mcp/models.py +197 -0
  15. anodize_mcp-0.1.0/src/anodize_mcp/pagination.py +37 -0
  16. anodize_mcp-0.1.0/src/anodize_mcp/protocol.py +88 -0
  17. anodize_mcp-0.1.0/src/anodize_mcp/py.typed +0 -0
  18. anodize_mcp-0.1.0/src/anodize_mcp/schema.py +707 -0
  19. anodize_mcp-0.1.0/src/anodize_mcp/server.py +626 -0
  20. anodize_mcp-0.1.0/src/anodize_mcp/session.py +136 -0
  21. anodize_mcp-0.1.0/src/anodize_mcp/transports/__init__.py +1 -0
  22. anodize_mcp-0.1.0/src/anodize_mcp/transports/http.py +309 -0
  23. anodize_mcp-0.1.0/src/anodize_mcp/transports/stdio.py +84 -0
  24. anodize_mcp-0.1.0/src/anodize_mcp.egg-info/PKG-INFO +279 -0
  25. anodize_mcp-0.1.0/src/anodize_mcp.egg-info/SOURCES.txt +32 -0
  26. anodize_mcp-0.1.0/src/anodize_mcp.egg-info/dependency_links.txt +1 -0
  27. anodize_mcp-0.1.0/src/anodize_mcp.egg-info/requires.txt +4 -0
  28. anodize_mcp-0.1.0/src/anodize_mcp.egg-info/top_level.txt +1 -0
  29. anodize_mcp-0.1.0/tests/test_compat.py +147 -0
  30. anodize_mcp-0.1.0/tests/test_dispatch.py +261 -0
  31. anodize_mcp-0.1.0/tests/test_features.py +350 -0
  32. anodize_mcp-0.1.0/tests/test_http.py +190 -0
  33. anodize_mcp-0.1.0/tests/test_schema.py +292 -0
  34. 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
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+