easy-mcp-kit 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,47 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ tags: ["v*"]
7
+ pull_request:
8
+
9
+ jobs:
10
+ test:
11
+ runs-on: ubuntu-latest
12
+ strategy:
13
+ matrix:
14
+ python-version: ["3.11", "3.12", "3.13"]
15
+ steps:
16
+ - uses: actions/checkout@v4
17
+ - uses: actions/setup-python@v5
18
+ with:
19
+ python-version: ${{ matrix.python-version }}
20
+ - name: Install
21
+ run: pip install -e .[dev]
22
+ - name: Lint
23
+ run: ruff check easy_mcp tests examples
24
+ - name: Test
25
+ run: pytest -q
26
+
27
+ publish:
28
+ # Runs only on version tags (e.g. v0.1.0) after tests pass.
29
+ # Uses PyPI Trusted Publishing — configure the publisher on pypi.org
30
+ # first; no API token secret is needed.
31
+ needs: test
32
+ if: startsWith(github.ref, 'refs/tags/v')
33
+ runs-on: ubuntu-latest
34
+ environment: pypi
35
+ permissions:
36
+ id-token: write
37
+ steps:
38
+ - uses: actions/checkout@v4
39
+ - uses: actions/setup-python@v5
40
+ with:
41
+ python-version: "3.12"
42
+ - name: Build sdist and wheel
43
+ run: |
44
+ pip install build
45
+ python -m build
46
+ - name: Publish to PyPI
47
+ uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,25 @@
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ *.egg-info/
5
+ .eggs/
6
+ build/
7
+ dist/
8
+
9
+ # Environments
10
+ .venv/
11
+ venv/
12
+ .env
13
+
14
+ # Tooling caches
15
+ .pytest_cache/
16
+ .mypy_cache/
17
+ .ruff_cache/
18
+ .coverage
19
+ htmlcov/
20
+
21
+ # Editors / OS
22
+ .vscode/
23
+ .idea/
24
+ Thumbs.db
25
+ .DS_Store
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Mark Rodrigues
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,276 @@
1
+ Metadata-Version: 2.5
2
+ Name: easy-mcp-kit
3
+ Version: 0.1.0
4
+ Summary: Build secure MCP (Model Context Protocol) servers from plain Python functions.
5
+ Project-URL: Homepage, https://github.com/Mark007-R/Easy-MCP
6
+ Project-URL: Repository, https://github.com/Mark007-R/Easy-MCP
7
+ Project-URL: Issues, https://github.com/Mark007-R/Easy-MCP/issues
8
+ Project-URL: Roadmap, https://github.com/Mark007-R/Easy-MCP/blob/main/ROADMAP.md
9
+ Project-URL: Security, https://github.com/Mark007-R/Easy-MCP/blob/main/SECURITY.md
10
+ Author-email: Mark Rodrigues <markrodrigues2004@gmail.com>
11
+ License: MIT
12
+ License-File: LICENSE
13
+ Keywords: agents,ai,llm,mcp,model-context-protocol,server,sse,tools
14
+ Classifier: Development Status :: 4 - Beta
15
+ Classifier: Intended Audience :: Developers
16
+ Classifier: License :: OSI Approved :: MIT License
17
+ Classifier: Operating System :: OS Independent
18
+ Classifier: Programming Language :: Python :: 3
19
+ Classifier: Programming Language :: Python :: 3.11
20
+ Classifier: Programming Language :: Python :: 3.12
21
+ Classifier: Programming Language :: Python :: 3.13
22
+ Classifier: Topic :: Internet :: WWW/HTTP :: HTTP Servers
23
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
24
+ Classifier: Typing :: Typed
25
+ Requires-Python: >=3.11
26
+ Requires-Dist: starlette>=0.37
27
+ Requires-Dist: uvicorn>=0.30
28
+ Provides-Extra: dev
29
+ Requires-Dist: httpx>=0.27; extra == 'dev'
30
+ Requires-Dist: mypy>=1.10; extra == 'dev'
31
+ Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
32
+ Requires-Dist: pytest>=8.0; extra == 'dev'
33
+ Requires-Dist: ruff>=0.5; extra == 'dev'
34
+ Description-Content-Type: text/markdown
35
+
36
+ # easy_mcp
37
+
38
+ **Build secure MCP (Model Context Protocol) servers from plain Python functions.**
39
+
40
+ `easy_mcp` is FastAPI-for-MCP: declare a function, add a decorator, run a server.
41
+ Schema generation, validation, authentication, rate limiting, timeouts,
42
+ structured logging, and sanitized error handling are all built in — and secure
43
+ by default.
44
+
45
+ ```python
46
+ from easy_mcp import MCPServer
47
+
48
+ server = MCPServer(port=8000)
49
+
50
+ @server.tool
51
+ def add(a: int, b: int) -> int:
52
+ """Add two numbers."""
53
+ return a + b
54
+
55
+ server.run()
56
+ ```
57
+
58
+ That's a complete, MCP-compliant server. Connect any MCP client to
59
+ `http://127.0.0.1:8000/sse` and the `add` tool is discoverable and callable —
60
+ with its JSON schema generated from the type hints and its description taken
61
+ from the docstring.
62
+
63
+ ## Installation
64
+
65
+ ```bash
66
+ pip install easy-mcp-kit
67
+ ```
68
+
69
+ The package installs as `easy-mcp-kit`; the import name is `easy_mcp`.
70
+
71
+ Requires Python 3.11+. Only two runtime dependencies: `starlette` and `uvicorn`.
72
+
73
+ ## Why easy_mcp?
74
+
75
+ | Concern | What you write | What easy_mcp does |
76
+ |---|---|---|
77
+ | Schemas | Type hints | Generates strict JSON Schema (`additionalProperties: false`) |
78
+ | Descriptions | Docstrings | Parses summary + Google-style `Args:` into tool/param descriptions |
79
+ | Validation | Nothing | Rejects unknown fields, wrong types, missing params — before your code runs |
80
+ | Auth | `auth=APIKeyAuth({...})` | Constant-time key checks, per-tool scopes, hidden protected tools |
81
+ | Rate limits | `rate_limit_per_minute=120` | Sliding-window limiter per client |
82
+ | Errors | Just `raise` | Clients get a sanitized message + `error_id`; the log gets the traceback |
83
+ | Crashes | Nothing | One failing tool never takes down the server |
84
+
85
+ ## Quickstart tour
86
+
87
+ ### Tool registration
88
+
89
+ ```python
90
+ # Bare decorator — name, description, and schema are inferred:
91
+ @server.tool
92
+ def word_count(text: str) -> dict[str, int]:
93
+ """Count words and characters in a text.
94
+
95
+ Args:
96
+ text: The text to analyze.
97
+ """
98
+ return {"words": len(text.split()), "characters": len(text)}
99
+
100
+ # With options:
101
+ @server.tool(name="summarize", tags=("stats",), category="math",
102
+ examples=({"arguments": {"values": [1, 2, 3]}},), timeout=5.0)
103
+ def summarize_numbers(values: list[float]) -> dict[str, float]:
104
+ """Compute mean/min/max of a list of numbers."""
105
+ ...
106
+
107
+ # Async tools just work:
108
+ @server.tool
109
+ async def fetch_status(url: str) -> str:
110
+ """Fetch a status page."""
111
+ ...
112
+
113
+ # Dynamic registration at runtime:
114
+ server.register_tool(my_function, name="late_tool")
115
+ server.unregister_tool("late_tool")
116
+ ```
117
+
118
+ ### Supported parameter types
119
+
120
+ | Python annotation | JSON Schema |
121
+ |---|---|
122
+ | `str`, `int`, `float`, `bool` | `string`, `integer`, `number`, `boolean` |
123
+ | `list`, `list[T]` | `array` (+ typed `items`) |
124
+ | `dict`, `dict[str, T]` | `object` (+ typed `additionalProperties`) |
125
+ | `T \| None`, `Optional[T]`, unions | `anyOf` |
126
+ | `Literal["a", "b"]` | `enum` |
127
+ | defaults (`x: int = 3`) | optional param + advertised `default` |
128
+
129
+ Anything else is rejected **at registration time** with a clear error — never
130
+ at call time. Validation is strict: booleans are not integers, unknown
131
+ arguments are hard errors, and every violation is reported (not just the first).
132
+
133
+ ### Authentication and per-tool permissions
134
+
135
+ ```python
136
+ from easy_mcp import APIKeyAuth, MCPServer
137
+
138
+ auth = APIKeyAuth({
139
+ "long-random-admin-key...": "*", # all scopes
140
+ "long-random-viewer-key..": ["reports"], # specific scopes
141
+ })
142
+ # Or keep keys out of code entirely:
143
+ # auth = APIKeyAuth.from_env() # reads EASY_MCP_API_KEYS="key1:*;key2:reports|stats"
144
+
145
+ server = MCPServer(port=8000, auth=auth)
146
+
147
+ @server.tool
148
+ def public_tool() -> str:
149
+ """Anyone can call this."""
150
+
151
+ @server.tool(requires_auth=True)
152
+ def protected_tool() -> str:
153
+ """Any authenticated client can call this."""
154
+
155
+ @server.tool(scopes=("admin",))
156
+ def admin_tool() -> str:
157
+ """Only keys holding the 'admin' scope can call this."""
158
+ ```
159
+
160
+ Clients authenticate with `Authorization: Bearer <key>` or `X-API-Key`.
161
+ Protected tools are **invisible** to clients that cannot call them — they are
162
+ omitted from `tools/list` and reported as unknown on `tools/call`, so
163
+ unauthorized clients cannot even enumerate them.
164
+
165
+ ### Rate limiting, payload caps, timeouts, session limits
166
+
167
+ ```python
168
+ server = MCPServer(
169
+ port=8000,
170
+ rate_limit_per_minute=120, # per client; None disables
171
+ max_request_bytes=1_048_576, # enforced while reading the body
172
+ default_timeout=30.0, # per tool call; override per tool
173
+ )
174
+
175
+ @server.tool(timeout=2.0, max_calls_per_session=5)
176
+ async def expensive(query: str) -> str:
177
+ """A tool with its own timeout and a per-session usage cap."""
178
+ ```
179
+
180
+ Clients can also cancel long-running calls with the standard MCP
181
+ `notifications/cancelled` message.
182
+
183
+ ### Error handling
184
+
185
+ | Situation | What the client sees |
186
+ |---|---|
187
+ | Invalid arguments | JSON-RPC `-32602` listing every violation |
188
+ | Tool raises `ToolError("msg")` | `isError: true` with your message verbatim |
189
+ | Tool raises anything else | `isError: true` with `Tool execution failed (error_id=...)` — no traceback, no exception text |
190
+ | Tool exceeds its timeout | `-32005` timeout error |
191
+ | Rate limit exceeded | `-32003` with `retry_after_seconds` |
192
+ | Session cap reached | `-32006` |
193
+
194
+ In `debug=True` mode (development only) clients receive full tracebacks. The
195
+ `error_id` in production responses matches the server-side log entry that
196
+ contains the real traceback, so you can correlate without leaking internals.
197
+
198
+ ### Structured logging and audit trail
199
+
200
+ All logs are single-line JSON on stderr. Every tool call is audited with the
201
+ tool name, client id, duration, and outcome — never with API keys (only
202
+ SHA-256 fingerprints ever appear):
203
+
204
+ ```json
205
+ {"timestamp": "2026-07-20T12:00:00.000Z", "level": "INFO", "logger": "easy_mcp.audit",
206
+ "message": "tool_call", "event": {"type": "tool_call", "tool": "add",
207
+ "client_id": "3f9c2a71b04d", "duration_ms": 0.42, "status": "ok"}}
208
+ ```
209
+
210
+ ## Connecting a client
211
+
212
+ ```bash
213
+ # MCP Inspector (interactive UI):
214
+ npx @modelcontextprotocol/inspector # connect to http://127.0.0.1:8000/sse
215
+
216
+ # Claude Code:
217
+ claude mcp add --transport sse my-server http://127.0.0.1:8000/sse
218
+ ```
219
+
220
+ Or run the raw wire-protocol walkthrough in
221
+ [`examples/raw_client.py`](examples/raw_client.py) against
222
+ [`examples/demo_server.py`](examples/demo_server.py).
223
+
224
+ ## Architecture
225
+
226
+ ```
227
+ easy_mcp/
228
+ ├── server.py MCPServer: registration, dispatch, execution, lifecycle
229
+ ├── decorators.py @tool machinery, ToolDefinition, thread-safe registry
230
+ ├── schema.py type hints → JSON Schema; docstring parsing; validation
231
+ ├── security/
232
+ │ ├── auth.py APIKeyAuth (constant-time), scopes, visibility rules
233
+ │ └── ratelimit.py sliding-window per-client rate limiter
234
+ ├── transport/
235
+ │ ├── base.py Transport ABC + ClientContext
236
+ │ └── sse.py HTTP + SSE transport (Starlette/uvicorn)
237
+ ├── exceptions.py error hierarchy + stable JSON-RPC error codes
238
+ └── logging.py JSON logs + audit trail
239
+ ```
240
+
241
+ The dispatcher (`MCPServer.dispatch`) is transport-independent: it takes one
242
+ decoded JSON-RPC message plus a `ClientContext` and returns the response.
243
+ Transports only resolve credentials, cap payload sizes, and move bytes —
244
+ so adding HTTP/WebSocket/stdio transports (see [ROADMAP.md](ROADMAP.md))
245
+ cannot silently bypass a security check.
246
+
247
+ **Determinism:** tool listings are sorted, JSON output uses sorted keys, and
248
+ identical inputs produce byte-identical responses — useful for reproducible
249
+ agent runs and caching.
250
+
251
+ **Performance notes:** sync tools run in a worker thread pool so they never
252
+ block the event loop; async tools run natively. Schema validation is a small
253
+ hand-written walker (no dependency, ~microseconds for typical payloads). The
254
+ per-message overhead is dominated by JSON encode/decode; for large results
255
+ prefer returning compact structures over huge strings.
256
+
257
+ ## Production deployment
258
+
259
+ - Run behind TLS (reverse proxy such as Caddy/nginx) — API keys travel in headers.
260
+ - Load keys from the environment (`APIKeyAuth.from_env()`), never hardcode them.
261
+ - Keep `debug=False`; it is the only thing standing between clients and tracebacks.
262
+ - For multiple workers: `uvicorn "myapp:server.build_app" --factory` won't share
263
+ sessions across processes — v0.1 targets a single process (see ROADMAP).
264
+ - Read [SECURITY.md](SECURITY.md) before exposing a server beyond localhost.
265
+
266
+ ## Development
267
+
268
+ ```bash
269
+ pip install -e .[dev]
270
+ pytest # 60+ tests: schema, registration, dispatch, security, transport
271
+ ruff check .
272
+ ```
273
+
274
+ ## License
275
+
276
+ MIT — see [LICENSE](LICENSE).
@@ -0,0 +1,241 @@
1
+ # easy_mcp
2
+
3
+ **Build secure MCP (Model Context Protocol) servers from plain Python functions.**
4
+
5
+ `easy_mcp` is FastAPI-for-MCP: declare a function, add a decorator, run a server.
6
+ Schema generation, validation, authentication, rate limiting, timeouts,
7
+ structured logging, and sanitized error handling are all built in — and secure
8
+ by default.
9
+
10
+ ```python
11
+ from easy_mcp import MCPServer
12
+
13
+ server = MCPServer(port=8000)
14
+
15
+ @server.tool
16
+ def add(a: int, b: int) -> int:
17
+ """Add two numbers."""
18
+ return a + b
19
+
20
+ server.run()
21
+ ```
22
+
23
+ That's a complete, MCP-compliant server. Connect any MCP client to
24
+ `http://127.0.0.1:8000/sse` and the `add` tool is discoverable and callable —
25
+ with its JSON schema generated from the type hints and its description taken
26
+ from the docstring.
27
+
28
+ ## Installation
29
+
30
+ ```bash
31
+ pip install easy-mcp-kit
32
+ ```
33
+
34
+ The package installs as `easy-mcp-kit`; the import name is `easy_mcp`.
35
+
36
+ Requires Python 3.11+. Only two runtime dependencies: `starlette` and `uvicorn`.
37
+
38
+ ## Why easy_mcp?
39
+
40
+ | Concern | What you write | What easy_mcp does |
41
+ |---|---|---|
42
+ | Schemas | Type hints | Generates strict JSON Schema (`additionalProperties: false`) |
43
+ | Descriptions | Docstrings | Parses summary + Google-style `Args:` into tool/param descriptions |
44
+ | Validation | Nothing | Rejects unknown fields, wrong types, missing params — before your code runs |
45
+ | Auth | `auth=APIKeyAuth({...})` | Constant-time key checks, per-tool scopes, hidden protected tools |
46
+ | Rate limits | `rate_limit_per_minute=120` | Sliding-window limiter per client |
47
+ | Errors | Just `raise` | Clients get a sanitized message + `error_id`; the log gets the traceback |
48
+ | Crashes | Nothing | One failing tool never takes down the server |
49
+
50
+ ## Quickstart tour
51
+
52
+ ### Tool registration
53
+
54
+ ```python
55
+ # Bare decorator — name, description, and schema are inferred:
56
+ @server.tool
57
+ def word_count(text: str) -> dict[str, int]:
58
+ """Count words and characters in a text.
59
+
60
+ Args:
61
+ text: The text to analyze.
62
+ """
63
+ return {"words": len(text.split()), "characters": len(text)}
64
+
65
+ # With options:
66
+ @server.tool(name="summarize", tags=("stats",), category="math",
67
+ examples=({"arguments": {"values": [1, 2, 3]}},), timeout=5.0)
68
+ def summarize_numbers(values: list[float]) -> dict[str, float]:
69
+ """Compute mean/min/max of a list of numbers."""
70
+ ...
71
+
72
+ # Async tools just work:
73
+ @server.tool
74
+ async def fetch_status(url: str) -> str:
75
+ """Fetch a status page."""
76
+ ...
77
+
78
+ # Dynamic registration at runtime:
79
+ server.register_tool(my_function, name="late_tool")
80
+ server.unregister_tool("late_tool")
81
+ ```
82
+
83
+ ### Supported parameter types
84
+
85
+ | Python annotation | JSON Schema |
86
+ |---|---|
87
+ | `str`, `int`, `float`, `bool` | `string`, `integer`, `number`, `boolean` |
88
+ | `list`, `list[T]` | `array` (+ typed `items`) |
89
+ | `dict`, `dict[str, T]` | `object` (+ typed `additionalProperties`) |
90
+ | `T \| None`, `Optional[T]`, unions | `anyOf` |
91
+ | `Literal["a", "b"]` | `enum` |
92
+ | defaults (`x: int = 3`) | optional param + advertised `default` |
93
+
94
+ Anything else is rejected **at registration time** with a clear error — never
95
+ at call time. Validation is strict: booleans are not integers, unknown
96
+ arguments are hard errors, and every violation is reported (not just the first).
97
+
98
+ ### Authentication and per-tool permissions
99
+
100
+ ```python
101
+ from easy_mcp import APIKeyAuth, MCPServer
102
+
103
+ auth = APIKeyAuth({
104
+ "long-random-admin-key...": "*", # all scopes
105
+ "long-random-viewer-key..": ["reports"], # specific scopes
106
+ })
107
+ # Or keep keys out of code entirely:
108
+ # auth = APIKeyAuth.from_env() # reads EASY_MCP_API_KEYS="key1:*;key2:reports|stats"
109
+
110
+ server = MCPServer(port=8000, auth=auth)
111
+
112
+ @server.tool
113
+ def public_tool() -> str:
114
+ """Anyone can call this."""
115
+
116
+ @server.tool(requires_auth=True)
117
+ def protected_tool() -> str:
118
+ """Any authenticated client can call this."""
119
+
120
+ @server.tool(scopes=("admin",))
121
+ def admin_tool() -> str:
122
+ """Only keys holding the 'admin' scope can call this."""
123
+ ```
124
+
125
+ Clients authenticate with `Authorization: Bearer <key>` or `X-API-Key`.
126
+ Protected tools are **invisible** to clients that cannot call them — they are
127
+ omitted from `tools/list` and reported as unknown on `tools/call`, so
128
+ unauthorized clients cannot even enumerate them.
129
+
130
+ ### Rate limiting, payload caps, timeouts, session limits
131
+
132
+ ```python
133
+ server = MCPServer(
134
+ port=8000,
135
+ rate_limit_per_minute=120, # per client; None disables
136
+ max_request_bytes=1_048_576, # enforced while reading the body
137
+ default_timeout=30.0, # per tool call; override per tool
138
+ )
139
+
140
+ @server.tool(timeout=2.0, max_calls_per_session=5)
141
+ async def expensive(query: str) -> str:
142
+ """A tool with its own timeout and a per-session usage cap."""
143
+ ```
144
+
145
+ Clients can also cancel long-running calls with the standard MCP
146
+ `notifications/cancelled` message.
147
+
148
+ ### Error handling
149
+
150
+ | Situation | What the client sees |
151
+ |---|---|
152
+ | Invalid arguments | JSON-RPC `-32602` listing every violation |
153
+ | Tool raises `ToolError("msg")` | `isError: true` with your message verbatim |
154
+ | Tool raises anything else | `isError: true` with `Tool execution failed (error_id=...)` — no traceback, no exception text |
155
+ | Tool exceeds its timeout | `-32005` timeout error |
156
+ | Rate limit exceeded | `-32003` with `retry_after_seconds` |
157
+ | Session cap reached | `-32006` |
158
+
159
+ In `debug=True` mode (development only) clients receive full tracebacks. The
160
+ `error_id` in production responses matches the server-side log entry that
161
+ contains the real traceback, so you can correlate without leaking internals.
162
+
163
+ ### Structured logging and audit trail
164
+
165
+ All logs are single-line JSON on stderr. Every tool call is audited with the
166
+ tool name, client id, duration, and outcome — never with API keys (only
167
+ SHA-256 fingerprints ever appear):
168
+
169
+ ```json
170
+ {"timestamp": "2026-07-20T12:00:00.000Z", "level": "INFO", "logger": "easy_mcp.audit",
171
+ "message": "tool_call", "event": {"type": "tool_call", "tool": "add",
172
+ "client_id": "3f9c2a71b04d", "duration_ms": 0.42, "status": "ok"}}
173
+ ```
174
+
175
+ ## Connecting a client
176
+
177
+ ```bash
178
+ # MCP Inspector (interactive UI):
179
+ npx @modelcontextprotocol/inspector # connect to http://127.0.0.1:8000/sse
180
+
181
+ # Claude Code:
182
+ claude mcp add --transport sse my-server http://127.0.0.1:8000/sse
183
+ ```
184
+
185
+ Or run the raw wire-protocol walkthrough in
186
+ [`examples/raw_client.py`](examples/raw_client.py) against
187
+ [`examples/demo_server.py`](examples/demo_server.py).
188
+
189
+ ## Architecture
190
+
191
+ ```
192
+ easy_mcp/
193
+ ├── server.py MCPServer: registration, dispatch, execution, lifecycle
194
+ ├── decorators.py @tool machinery, ToolDefinition, thread-safe registry
195
+ ├── schema.py type hints → JSON Schema; docstring parsing; validation
196
+ ├── security/
197
+ │ ├── auth.py APIKeyAuth (constant-time), scopes, visibility rules
198
+ │ └── ratelimit.py sliding-window per-client rate limiter
199
+ ├── transport/
200
+ │ ├── base.py Transport ABC + ClientContext
201
+ │ └── sse.py HTTP + SSE transport (Starlette/uvicorn)
202
+ ├── exceptions.py error hierarchy + stable JSON-RPC error codes
203
+ └── logging.py JSON logs + audit trail
204
+ ```
205
+
206
+ The dispatcher (`MCPServer.dispatch`) is transport-independent: it takes one
207
+ decoded JSON-RPC message plus a `ClientContext` and returns the response.
208
+ Transports only resolve credentials, cap payload sizes, and move bytes —
209
+ so adding HTTP/WebSocket/stdio transports (see [ROADMAP.md](ROADMAP.md))
210
+ cannot silently bypass a security check.
211
+
212
+ **Determinism:** tool listings are sorted, JSON output uses sorted keys, and
213
+ identical inputs produce byte-identical responses — useful for reproducible
214
+ agent runs and caching.
215
+
216
+ **Performance notes:** sync tools run in a worker thread pool so they never
217
+ block the event loop; async tools run natively. Schema validation is a small
218
+ hand-written walker (no dependency, ~microseconds for typical payloads). The
219
+ per-message overhead is dominated by JSON encode/decode; for large results
220
+ prefer returning compact structures over huge strings.
221
+
222
+ ## Production deployment
223
+
224
+ - Run behind TLS (reverse proxy such as Caddy/nginx) — API keys travel in headers.
225
+ - Load keys from the environment (`APIKeyAuth.from_env()`), never hardcode them.
226
+ - Keep `debug=False`; it is the only thing standing between clients and tracebacks.
227
+ - For multiple workers: `uvicorn "myapp:server.build_app" --factory` won't share
228
+ sessions across processes — v0.1 targets a single process (see ROADMAP).
229
+ - Read [SECURITY.md](SECURITY.md) before exposing a server beyond localhost.
230
+
231
+ ## Development
232
+
233
+ ```bash
234
+ pip install -e .[dev]
235
+ pytest # 60+ tests: schema, registration, dispatch, security, transport
236
+ ruff check .
237
+ ```
238
+
239
+ ## License
240
+
241
+ MIT — see [LICENSE](LICENSE).
@@ -0,0 +1,33 @@
1
+ # Roadmap
2
+
3
+ `v0.1.0` is intentionally small but complete: one solid transport, strict
4
+ validation, real security defaults. Each release below stays backwards
5
+ compatible until `v1.0` freezes the public API.
6
+
7
+ ## v0.2 — Transports & richer schemas
8
+
9
+ - **stdio transport** (Claude Desktop and most local MCP clients).
10
+ - **Streamable HTTP transport** (the current MCP spec's successor to SSE).
11
+ - `Annotated[int, "description"]` parameter descriptions in addition to docstrings.
12
+ - Optional Pydantic model support for complex tool parameters and outputs.
13
+ - Structured output schemas (`outputSchema` / `structuredContent`) for typed results.
14
+ - `easy-mcp run examples.demo_server:server` CLI for zero-code launching.
15
+
16
+ ## v0.3 — Protocol surface & operations
17
+
18
+ - MCP **resources** and **prompts** (not just tools).
19
+ - `listChanged` notifications when tools are registered/unregistered at runtime.
20
+ - Middleware hooks (before/after tool call) for custom auth, tracing, metrics.
21
+ - OpenTelemetry spans + Prometheus-style metrics endpoint.
22
+ - Shared session store (Redis) for multi-worker deployments.
23
+ - OAuth 2.1 / bearer-token verification per the MCP authorization spec.
24
+
25
+ ## v1.0 — Stability & hardening
26
+
27
+ - Frozen public API with semantic versioning guarantees.
28
+ - WebSocket transport.
29
+ - Per-key quotas and cost accounting (beyond per-minute rate limits).
30
+ - Built-in subprocess sandbox runner for semi-trusted tools.
31
+ - Property-based fuzzing of the validator and dispatcher in CI.
32
+ - Third-party security review of auth, session, and transport code.
33
+ - Strict `mypy --strict` gate and 100% branch coverage on security paths.