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.
- easy_mcp_kit-0.1.0/.github/workflows/ci.yml +47 -0
- easy_mcp_kit-0.1.0/.gitignore +25 -0
- easy_mcp_kit-0.1.0/LICENSE +21 -0
- easy_mcp_kit-0.1.0/PKG-INFO +276 -0
- easy_mcp_kit-0.1.0/README.md +241 -0
- easy_mcp_kit-0.1.0/ROADMAP.md +33 -0
- easy_mcp_kit-0.1.0/SECURITY.md +90 -0
- easy_mcp_kit-0.1.0/easy_mcp/__init__.py +62 -0
- easy_mcp_kit-0.1.0/easy_mcp/decorators.py +175 -0
- easy_mcp_kit-0.1.0/easy_mcp/exceptions.py +112 -0
- easy_mcp_kit-0.1.0/easy_mcp/logging.py +69 -0
- easy_mcp_kit-0.1.0/easy_mcp/py.typed +0 -0
- easy_mcp_kit-0.1.0/easy_mcp/schema.py +246 -0
- easy_mcp_kit-0.1.0/easy_mcp/security/__init__.py +13 -0
- easy_mcp_kit-0.1.0/easy_mcp/security/auth.py +156 -0
- easy_mcp_kit-0.1.0/easy_mcp/security/ratelimit.py +67 -0
- easy_mcp_kit-0.1.0/easy_mcp/server.py +520 -0
- easy_mcp_kit-0.1.0/easy_mcp/transport/__init__.py +7 -0
- easy_mcp_kit-0.1.0/easy_mcp/transport/base.py +56 -0
- easy_mcp_kit-0.1.0/easy_mcp/transport/sse.py +247 -0
- easy_mcp_kit-0.1.0/examples/demo_server.py +100 -0
- easy_mcp_kit-0.1.0/examples/raw_client.py +60 -0
- easy_mcp_kit-0.1.0/pyproject.toml +76 -0
- easy_mcp_kit-0.1.0/tests/conftest.py +42 -0
- easy_mcp_kit-0.1.0/tests/test_dispatch.py +268 -0
- easy_mcp_kit-0.1.0/tests/test_registration.py +136 -0
- easy_mcp_kit-0.1.0/tests/test_schema.py +207 -0
- easy_mcp_kit-0.1.0/tests/test_security.py +224 -0
- easy_mcp_kit-0.1.0/tests/test_transport.py +175 -0
|
@@ -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.
|