morgenmcp 0.2.1__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.
- morgenmcp-0.2.1/.claude/skills/fastmcp/SKILL.md +88 -0
- morgenmcp-0.2.1/.claude/skills/fastmcp/reference/CHANGELOG.md +163 -0
- morgenmcp-0.2.1/.claude/skills/fastmcp/reference/middleware.md +85 -0
- morgenmcp-0.2.1/.claude/skills/fastmcp/reference/registration.md +81 -0
- morgenmcp-0.2.1/.claude/skills/fastmcp/reference/testing.md +42 -0
- morgenmcp-0.2.1/.github/dependabot.yml +32 -0
- morgenmcp-0.2.1/.github/workflows/ci.yml +43 -0
- morgenmcp-0.2.1/.github/workflows/release.yml +48 -0
- morgenmcp-0.2.1/.gitignore +65 -0
- morgenmcp-0.2.1/.gitmodules +8 -0
- morgenmcp-0.2.1/.pre-commit-config.yaml +11 -0
- morgenmcp-0.2.1/.python-version +1 -0
- morgenmcp-0.2.1/AGENTS.md +68 -0
- morgenmcp-0.2.1/AI_POLICY.md +102 -0
- morgenmcp-0.2.1/CLAUDE.md +196 -0
- morgenmcp-0.2.1/LICENSE +202 -0
- morgenmcp-0.2.1/NOTICE +14 -0
- morgenmcp-0.2.1/PKG-INFO +230 -0
- morgenmcp-0.2.1/README.md +192 -0
- morgenmcp-0.2.1/mise.toml +7 -0
- morgenmcp-0.2.1/morgenmcp/__init__.py +12 -0
- morgenmcp-0.2.1/morgenmcp/_version.py +24 -0
- morgenmcp-0.2.1/morgenmcp/client.py +676 -0
- morgenmcp-0.2.1/morgenmcp/models.py +512 -0
- morgenmcp-0.2.1/morgenmcp/resources.py +308 -0
- morgenmcp-0.2.1/morgenmcp/server.py +744 -0
- morgenmcp-0.2.1/morgenmcp/tools/__init__.py +26 -0
- morgenmcp-0.2.1/morgenmcp/tools/accounts.py +42 -0
- morgenmcp-0.2.1/morgenmcp/tools/calendars.py +136 -0
- morgenmcp-0.2.1/morgenmcp/tools/events.py +775 -0
- morgenmcp-0.2.1/morgenmcp/tools/id_registry.py +242 -0
- morgenmcp-0.2.1/morgenmcp/tools/id_utils.py +64 -0
- morgenmcp-0.2.1/morgenmcp/tools/outputs.py +339 -0
- morgenmcp-0.2.1/morgenmcp/tools/tags.py +190 -0
- morgenmcp-0.2.1/morgenmcp/tools/tasks.py +597 -0
- morgenmcp-0.2.1/morgenmcp/tools/utils.py +209 -0
- morgenmcp-0.2.1/morgenmcp/validators.py +350 -0
- morgenmcp-0.2.1/pyproject.toml +110 -0
- morgenmcp-0.2.1/tests/__init__.py +1 -0
- morgenmcp-0.2.1/tests/conftest.py +20 -0
- morgenmcp-0.2.1/tests/test_client.py +918 -0
- morgenmcp-0.2.1/tests/test_id_persistence.py +232 -0
- morgenmcp-0.2.1/tests/test_integration.py +112 -0
- morgenmcp-0.2.1/tests/test_mcp_server.py +877 -0
- morgenmcp-0.2.1/tests/test_models.py +435 -0
- morgenmcp-0.2.1/tests/test_resources.py +382 -0
- morgenmcp-0.2.1/tests/test_tasks_tags.py +574 -0
- morgenmcp-0.2.1/tests/test_tools.py +1607 -0
- morgenmcp-0.2.1/tests/test_validators.py +405 -0
- morgenmcp-0.2.1/uv.lock +1495 -0
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: fastmcp
|
|
3
|
+
description: Up-to-date reference for FastMCP (Python MCP server framework) as used in MorgenMCP — middleware authoring, tool/resource/prompt registration, ToolError conventions, in-memory Client testing, transports. Use when modifying morgenmcp/server.py, tools/, resources.py, or writing/debugging tests against fastmcp.Client. Prefer this over memory: verify against docs/fastmcp/docs/ (pinned submodule) before relying on any API detail — FastMCP ships breaking changes across 3.x minors.
|
|
4
|
+
user-invocable: true
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# FastMCP reference (MorgenMCP)
|
|
8
|
+
|
|
9
|
+
**Pinned version: 3.4.3** (`pyproject.toml`: `fastmcp>=3.4,<3.5`). Local docs submodule
|
|
10
|
+
`docs/fastmcp/` is checked out at tag `v3.4.3` (`1eedd1f6`) — matches the installed package
|
|
11
|
+
exactly. **Always check `docs/fastmcp/docs/` before trusting a remembered API detail** —
|
|
12
|
+
online docs and this skill's own prose can describe a different version.
|
|
13
|
+
|
|
14
|
+
> Facts here were verified against the pinned submodule and cross-checked with a deep-research
|
|
15
|
+
> pass (2026-07-07). Re-verify before relying on a signature if the submodule tag has moved —
|
|
16
|
+
> see reference/CHANGELOG.md.
|
|
17
|
+
|
|
18
|
+
## Where MorgenMCP deviates from FastMCP defaults (know these first)
|
|
19
|
+
|
|
20
|
+
- **Call-expression registration, not decorators.** `server.py` uses `mcp.tool(name=...)(func)` /
|
|
21
|
+
`mcp.resource(uri, ...)(func)` instead of `@mcp.tool`. This is a documented-valid alternative
|
|
22
|
+
(see reference/registration.md) chosen so tool/resource functions stay plain, independently
|
|
23
|
+
importable async functions for unit testing.
|
|
24
|
+
- **`ResponseCachingMiddleware` is a FastMCP-provided class**, imported directly from
|
|
25
|
+
`fastmcp.server.middleware.caching` (not subclassed). MorgenMCP's contribution is only the
|
|
26
|
+
*configuration* — `CallToolSettings`/`ReadResourceSettings` (60s TTL, which tools/resources are
|
|
27
|
+
cached) — so a FastMCP upgrade touching this module directly affects this repo's cache behavior.
|
|
28
|
+
- **stdio transport only**, no HTTP/auth surface. FastMCP auth (OAuth Proxy, MultiAuth) is
|
|
29
|
+
HTTP-only by design — under stdio it is silently skipped, not rejected (`run_stdio_async` never
|
|
30
|
+
reads `self.auth`; the server runs with `skip_auth=True`). So auth is simply inert here, and
|
|
31
|
+
auth-hardening changelog entries can be skipped when assessing upgrade risk for this repo.
|
|
32
|
+
|
|
33
|
+
## Quick recipes
|
|
34
|
+
|
|
35
|
+
**Middleware** (subclass `fastmcp.server.middleware.Middleware`, register via `mcp.add_middleware`):
|
|
36
|
+
```python
|
|
37
|
+
from fastmcp.server.middleware import Middleware, MiddlewareContext
|
|
38
|
+
|
|
39
|
+
class LoggingMiddleware(Middleware):
|
|
40
|
+
async def on_message(self, context: MiddlewareContext, call_next):
|
|
41
|
+
return await call_next(context)
|
|
42
|
+
|
|
43
|
+
mcp.add_middleware(LoggingMiddleware()) # order = execution order, first added = outermost
|
|
44
|
+
```
|
|
45
|
+
Deny by **raising**, not returning falsy: `ToolError` / `ResourceError` / `PromptError` / `McpError`.
|
|
46
|
+
`on_initialize` must reject *before* calling `call_next()` — raising after only logs.
|
|
47
|
+
|
|
48
|
+
**Registration** (call-expression form, as used in this repo):
|
|
49
|
+
```python
|
|
50
|
+
mcp.tool(name="morgen_list_events", description=...)(list_events)
|
|
51
|
+
mcp.resource("morgen://calendar/{calendar_id}/events")(get_calendar_events)
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
**Testing** (in-memory, no transport — pass the server instance directly):
|
|
55
|
+
```python
|
|
56
|
+
from fastmcp import Client
|
|
57
|
+
|
|
58
|
+
async def test_x():
|
|
59
|
+
async with Client(mcp) as client:
|
|
60
|
+
result = await client.call_tool("morgen_list_events", {...})
|
|
61
|
+
assert result.data == {...}
|
|
62
|
+
```
|
|
63
|
+
Don't open `Client` inside a pytest fixture (event-loop issues) — only build/return the server there.
|
|
64
|
+
|
|
65
|
+
**Errors**: `ToolError` messages are *always* sent to the client, regardless of `mask_error_details`.
|
|
66
|
+
Generic exceptions get masked to a generic string when `mask_error_details=True`. This is why
|
|
67
|
+
`utils.py`'s `@handle_tool_errors` converts everything to `ToolError`.
|
|
68
|
+
|
|
69
|
+
**Transport** (this repo only uses the first):
|
|
70
|
+
```python
|
|
71
|
+
mcp.run() # stdio (default) — what MorgenMCP uses
|
|
72
|
+
mcp.run(transport="http", host="127.0.0.1", port=8000) # Streamable HTTP → http://host:port/mcp
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## Topic reference (load as needed)
|
|
76
|
+
|
|
77
|
+
- Middleware hook signatures, `MiddlewareContext` fields, deny-by-raising details → [reference/middleware.md](reference/middleware.md)
|
|
78
|
+
- Tool/resource/prompt registration forms, primitive-result auto-wrapping, RFC 6570 URI templates → [reference/registration.md](reference/registration.md)
|
|
79
|
+
- Testing patterns (in-memory Client, `run_server_async` vs `run_server_in_process`) → [reference/testing.md](reference/testing.md)
|
|
80
|
+
- Per-version changelog (3.0.0 → 3.4.3), what's a breaking change, security-hardening timeline → [reference/CHANGELOG.md](reference/CHANGELOG.md)
|
|
81
|
+
|
|
82
|
+
## Why FastMCP here (context)
|
|
83
|
+
|
|
84
|
+
MorgenMCP wraps the Morgen calendar API as an MCP server. It runs stdio-only with no auth
|
|
85
|
+
provider, registers 22 tools, and layers FastMCP's `ResponseCachingMiddleware` (60s TTL,
|
|
86
|
+
configured not subclassed) plus a virtual-ID system on top of FastMCP's tool/resource primitives.
|
|
87
|
+
See the project's own `CLAUDE.md` for the full architecture — this skill only covers the FastMCP
|
|
88
|
+
framework layer.
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
# FastMCP version history (3.0.0 → 3.4.3)
|
|
2
|
+
|
|
3
|
+
Compiled 2026-07-07 from `docs/fastmcp/docs/updates.mdx` (official changelog) plus direct git-log
|
|
4
|
+
inspection of the `docs/fastmcp` submodule, then re-audited 2026-07-07 against the submodule docs,
|
|
5
|
+
the installed `.venv` package source, and authoritative external sources (NVD/GHSA/PyPI/the
|
|
6
|
+
PrefectHQ/fastmcp repo). Every version entry, code-name, date, CVE/GHSA ID, and the `257fe325`
|
|
7
|
+
caching commit is confirmed; the re-audit corrected six items (see git history of this file).
|
|
8
|
+
Version numbers are as precise as sources allow — flag anything you verify further so this file
|
|
9
|
+
stays current (see "How to refresh" below).
|
|
10
|
+
|
|
11
|
+
## 3.0.0
|
|
12
|
+
|
|
13
|
+
- **`ResourceResult`/`ResourceContent`** established as the canonical resource return type (list
|
|
14
|
+
of per-item content, individual MIME types/metadata).
|
|
15
|
+
- **`version` kwarg** added to `@mcp.resource`/`@mcp.tool`/`@mcp.prompt`; the selected version is
|
|
16
|
+
surfaced through `meta.fastmcp.version`/`meta.fastmcp.versions` (and clients can request one via
|
|
17
|
+
`_meta.fastmcp.version`) — it rides the `_meta` channel, not a field separate from it.
|
|
18
|
+
- **RFC 6570 URI template query params** (`{?param1,param2}`) — note: `updates.mdx` attributes
|
|
19
|
+
these to **2.13.0** (Oct 2025), not 3.0.0. The `{param}` / `{param*}` (wildcard) / `{?...}`
|
|
20
|
+
(query) forms are all valid in 3.x; see reference/registration.md for the working syntax.
|
|
21
|
+
- **OAuth Proxy gains CIMD** (client ID metadata document) support.
|
|
22
|
+
- Component visibility controls added.
|
|
23
|
+
|
|
24
|
+
## 3.0.2
|
|
25
|
+
|
|
26
|
+
- Fix: MCP transport auth headers no longer leak through to downstream OpenAPI APIs (issue #3260,
|
|
27
|
+
fixed by PR #3262).
|
|
28
|
+
- Fix: background task workers correctly receive the originating request ID.
|
|
29
|
+
|
|
30
|
+
## 3.1.0 — "Code to Joy" (2026-03-03)
|
|
31
|
+
|
|
32
|
+
- **Code Mode Transform**: instead of exposing the raw tool catalog, the LLM gets meta-tools —
|
|
33
|
+
search for relevant tools via BM25, inspect schemas, then write Python chaining `call_tool()`
|
|
34
|
+
calls inside a sandbox. Reduces context spent on tool schemas for large tool catalogs.
|
|
35
|
+
- **`MultiAuth`**: composes multiple token-verification sources into one auth layer. Tries a
|
|
36
|
+
designated `server` (an OAuth Proxy, which owns all OAuth routes/metadata) first, then
|
|
37
|
+
`verifiers` in list order (contributing only token-verification logic); 401 if all fail.
|
|
38
|
+
- PropelAuth provider support.
|
|
39
|
+
- Google GenAI sampling handler.
|
|
40
|
+
|
|
41
|
+
## 3.1.1
|
|
42
|
+
|
|
43
|
+
- Pin `pydantic-monty<0.0.8` — hotfix for a breaking change in Monty affecting Code Mode.
|
|
44
|
+
|
|
45
|
+
## 3.2.0 — "Show Don't Tool"
|
|
46
|
+
|
|
47
|
+
- **The Apps release**: tools can return interactive UIs (charts, dashboards, forms, maps) via
|
|
48
|
+
`FastMCPApp` — `@app.ui()` for LLM-visible tools, `@app.tool()` for backend tools the UI calls.
|
|
49
|
+
- **Security hardening pass**, tied to GHSA-vv7q-7jx5-f767 (OpenAPI provider SSRF/path
|
|
50
|
+
traversal): SSRF/path-traversal prevention (URL-encoding path params, restricting `$ref`
|
|
51
|
+
resolution to local refs), JWT algorithm restriction (blocks `HS*` when JWKS is configured),
|
|
52
|
+
OAuth scope enforcement (prefers IdP-granted scopes), CSRF double-submit cookie validation,
|
|
53
|
+
refresh-token misuse prevention (rejects refresh tokens used as Bearer tokens).
|
|
54
|
+
- **Breaking change** ⚠️: `FastMCPApp` tool calls now route through `___`-prefixed compound tool
|
|
55
|
+
names internally (PR #3667), replacing a prior `_meta`-injection approach that broke in real MCP
|
|
56
|
+
hosts (Goose, MCP Jam) that don't forward `_meta` on `callServerTool`. **Superseded two patches
|
|
57
|
+
later**: 3.2.4 (#3824) replaced the `___` scheme with hash-based backend tool routing — so the
|
|
58
|
+
literal `AppName___tool_name` format is a 3.2.0–3.2.3-only artifact, not the current mechanism.
|
|
59
|
+
- Dev server: `fastmcp dev apps` previews app tools in-browser with an MCP message inspector.
|
|
60
|
+
|
|
61
|
+
## 3.2.1 – 3.2.4 (patch line)
|
|
62
|
+
|
|
63
|
+
- 3.2.1: auth-provider audience validation fixes (Cognito `client_id`, Azure `identifier_uri`),
|
|
64
|
+
consent-cookie LRU cap, OpenAPI 3.0 `nullable` field leak fix.
|
|
65
|
+
- 3.2.2: fixes the Azure audience regression from 3.2.1.
|
|
66
|
+
- 3.2.3: pins `fakeredis<2.35.0` (2.35.0 rename broke pydocket's `memory://` backend).
|
|
67
|
+
- 3.2.4: background tasks scoped to authorization context instead of MCP session (**breaking**
|
|
68
|
+
for session-scoped semantics), docstring-derived parameter descriptions, `FileUpload`
|
|
69
|
+
decoded-base64-size validation, proxy stops forwarding inbound headers to unrelated servers,
|
|
70
|
+
Keycloak OAuth provider added, and the 3.2.0 `___`-prefixed app-tool routing replaced with
|
|
71
|
+
hash-based backend routing (#3824) — also breaking for anyone who hard-coded the `___` names.
|
|
72
|
+
|
|
73
|
+
## 3.3.0 — "Slim Reaper"
|
|
74
|
+
|
|
75
|
+
- **`fastmcp-slim`**: dependency-light distribution shipping client + transport layer without
|
|
76
|
+
Starlette/Uvicorn/server stack — same import namespace, lighter footprint for CI/agents/library
|
|
77
|
+
dependents.
|
|
78
|
+
- OAuth proxy hardening: silent-consent AS-in-the-middle guard, dot-segment redirect rejection,
|
|
79
|
+
per-token response cache partitioning.
|
|
80
|
+
- `AzureB2CProvider` user flows, public `update_scopes()` on `OAuthProxy`.
|
|
81
|
+
- `@mcp.tool(run_in_thread=False)` for thread-affine tools.
|
|
82
|
+
|
|
83
|
+
## 3.3.1 — "Loop There It Is"
|
|
84
|
+
|
|
85
|
+
- Hotfix for the 3.3 packaging split: standalone component imports (`from fastmcp.tools import
|
|
86
|
+
tool`) no longer pull in the server stack or trip a circular import.
|
|
87
|
+
- **This is the version MorgenMCP shipped on until 2026-07-07.**
|
|
88
|
+
|
|
89
|
+
## 3.4.0 — "Remote Control"
|
|
90
|
+
|
|
91
|
+
- **`fastmcp-remote`**: standalone stdio↔HTTP bridge package — `uvx fastmcp-remote
|
|
92
|
+
https://example.com/mcp` connects stdio-only hosts to HTTP-hosted servers, OAuth auto-enabled
|
|
93
|
+
for HTTPS.
|
|
94
|
+
- Proxies now forward `initialize` upstream — a missing/misconfigured backend fails the handshake
|
|
95
|
+
loudly instead of returning an empty-but-connected proxy.
|
|
96
|
+
- `fastmcp_access_token_expiry_seconds` decouples client-facing token lifetime from short
|
|
97
|
+
upstream `expires_in`.
|
|
98
|
+
- `ToolResult(..., is_error=True)` returns rich errors the model can act on instead of only
|
|
99
|
+
raising.
|
|
100
|
+
|
|
101
|
+
## 3.4.1 — "Floor It"
|
|
102
|
+
|
|
103
|
+
- Security patch: floors `starlette>=1.0.1`, closing CVE-2026-48710 (previously only constrained
|
|
104
|
+
transitively via `mcp`).
|
|
105
|
+
- OAuthProxy logs refresh-token cache misses instead of failing silently.
|
|
106
|
+
|
|
107
|
+
## 3.4.2 — "Heads Up"
|
|
108
|
+
|
|
109
|
+
- Compatibility patch: `JWTVerifier` now accepts JWTs with private, non-critical JWS header
|
|
110
|
+
parameters (e.g. Clerk's `cat`) instead of rejecting before signature/claim validation.
|
|
111
|
+
|
|
112
|
+
## 3.4.3 — "The Fast and the Secure-ious" (2026-07-05)
|
|
113
|
+
|
|
114
|
+
- SSRF allow-list hardening: every IPv6 transition form (NAT64, 6to4, Teredo, ISATAP) now
|
|
115
|
+
unwraps to its embedded IPv4 target and is checked against the same allow-list policy.
|
|
116
|
+
- DNS rebinding protection: Streamable HTTP validates `Host` and browser `Origin` before session
|
|
117
|
+
handling.
|
|
118
|
+
- Stricter OAuth redirects: unsafe schemes rejected before registration, DCR clients bound to
|
|
119
|
+
their registered redirect URIs.
|
|
120
|
+
- Reliability: proxy session-teardown races, discriminator-tag handling in JSON schema
|
|
121
|
+
conversion, several middleware/resource-template fixes.
|
|
122
|
+
- **This is the version MorgenMCP upgraded to on 2026-07-07** (`fastmcp>=3.4,<3.5`).
|
|
123
|
+
|
|
124
|
+
## Relevant to a specific bug hunt (caching middleware)
|
|
125
|
+
|
|
126
|
+
Between 3.3.1 and 3.4.3, commit `257fe325` ("fix: caching middleware TypeError on cache miss due
|
|
127
|
+
to mismatched call_next parameter", #4301) patched a positional/keyword mismatch in the
|
|
128
|
+
*vendored `fastmcp_slim` docs-submodule snapshot's* middleware chain composition
|
|
129
|
+
(`fastmcp_slim/fastmcp/server/server.py`). **This did not reproduce against the actually
|
|
130
|
+
installed 3.3.1 package** — the installed chain composition (`partial(mw, call_next=chain)`) is
|
|
131
|
+
shaped differently and was verified empirically (via `Client(mcp).list_tools()`) to work fine on
|
|
132
|
+
both 3.3.1 and 3.4.3. Lesson: the `docs/fastmcp` submodule's *git history* can contain commits
|
|
133
|
+
that don't map cleanly onto the installed PyPI package's actual code layout — always verify
|
|
134
|
+
against the installed package (`.venv/lib/.../site-packages/fastmcp/`), not just submodule diffs,
|
|
135
|
+
before concluding a changelog entry affects this repo.
|
|
136
|
+
|
|
137
|
+
**Second lesson from the same investigation**: an earlier version of this file, and of SKILL.md /
|
|
138
|
+
reference/middleware.md, claimed `ResponseCachingMiddleware` was hand-rolled by MorgenMCP rather
|
|
139
|
+
than FastMCP-provided — sourced from a deep-research pass whose adversarial verifiers refuted the
|
|
140
|
+
true claim 0-3, apparently because they only checked public docs pages and never inspected the
|
|
141
|
+
actually-installed package. That was wrong (see reference/middleware.md for the corrected facts).
|
|
142
|
+
Always cross-check "framework doesn't have X" claims directly against
|
|
143
|
+
`.venv/lib/.../site-packages/fastmcp/` before writing them into a skill as fact — a web-search-based
|
|
144
|
+
research pass can produce confident false negatives about implementation details that aren't
|
|
145
|
+
documented in public docs but are plainly present in the source.
|
|
146
|
+
|
|
147
|
+
## Not FastMCP framework features (commonly conflated)
|
|
148
|
+
|
|
149
|
+
- **No `fastmcp.ValidationError`** documented for tool/resource argument validation as of 3.4.3 —
|
|
150
|
+
validation failures surface via Pydantic coercion, not a dedicated FastMCP exception type.
|
|
151
|
+
- **Pydantic `by_alias`/`exclude_none` serialization** is a MorgenMCP project convention, not a
|
|
152
|
+
documented FastMCP requirement. The `model_dump(by_alias=True, exclude_none=True)` calls live in
|
|
153
|
+
`client.py` (request bodies); `models.py` supplies the field aliases and `validate_by_name`/
|
|
154
|
+
`validate_by_alias` config that make that round-trip work.
|
|
155
|
+
|
|
156
|
+
## How to refresh this file
|
|
157
|
+
|
|
158
|
+
1. `cd docs/fastmcp && git fetch --tags origin && git tag -l "v3.*"` — check for new tags.
|
|
159
|
+
2. `git log --oneline <old-tag>..<new-tag>` — skim for security/breaking-change commits.
|
|
160
|
+
3. `git show <new-tag>:docs/updates.mdx` — read the official per-release summary.
|
|
161
|
+
4. Update `SKILL.md`'s "Pinned version" line and this file's changelog table.
|
|
162
|
+
5. Empirically re-verify any claim about MorgenMCP's own code (like the caching-middleware note
|
|
163
|
+
above) against the *installed* package, not just the docs submodule — they can diverge.
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# Middleware
|
|
2
|
+
|
|
3
|
+
Source: `docs/fastmcp/docs/servers/middleware.mdx` (pinned v3.4.3).
|
|
4
|
+
|
|
5
|
+
## Authoring
|
|
6
|
+
|
|
7
|
+
Subclass `fastmcp.server.middleware.Middleware`, override the hooks you need:
|
|
8
|
+
|
|
9
|
+
```python
|
|
10
|
+
from fastmcp.server.middleware import Middleware, MiddlewareContext
|
|
11
|
+
|
|
12
|
+
class LoggingMiddleware(Middleware):
|
|
13
|
+
async def on_message(self, context: MiddlewareContext, call_next):
|
|
14
|
+
result = await call_next(context)
|
|
15
|
+
return result
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
Register on a server instance:
|
|
19
|
+
|
|
20
|
+
```python
|
|
21
|
+
mcp.add_middleware(LoggingMiddleware())
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
`add_middleware` (`fastmcp_slim/fastmcp/server/server.py:527`):
|
|
25
|
+
```python
|
|
26
|
+
def add_middleware(self, middleware: Middleware) -> None:
|
|
27
|
+
self.middleware.append(middleware)
|
|
28
|
+
```
|
|
29
|
+
Registration order = execution order for the request leg; **reversed** for the response leg
|
|
30
|
+
(first-added middleware is outermost — sees the request first, the response last). This repo's
|
|
31
|
+
`ResponseCachingMiddleware` registration order in `server.py` therefore determines whether other
|
|
32
|
+
middleware sees pre- or post-cache traffic.
|
|
33
|
+
|
|
34
|
+
## Hook signatures
|
|
35
|
+
|
|
36
|
+
All hooks share the same shape:
|
|
37
|
+
```python
|
|
38
|
+
async def hook(self, context: MiddlewareContext, call_next) -> result_type
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Most general → most specific:
|
|
42
|
+
- `on_message` — every message, requests + notifications
|
|
43
|
+
- `on_request` / `on_notification`
|
|
44
|
+
- `on_call_tool`, `on_read_resource`, `on_get_prompt`
|
|
45
|
+
- `on_list_tools`, `on_list_resources`, `on_list_resource_templates`, `on_list_prompts`
|
|
46
|
+
- `on_initialize`
|
|
47
|
+
|
|
48
|
+
Override `__call__` directly instead of the named hooks for raw/uniform handling of every message
|
|
49
|
+
type without dispatch overhead.
|
|
50
|
+
|
|
51
|
+
`MiddlewareContext` fields: `method`, `source`, `type`, `message`, `timestamp`, `fastmcp_context`.
|
|
52
|
+
- Tool/prompt calls: `context.message.name`, `context.message.arguments`
|
|
53
|
+
- Resource reads: `context.message.uri`
|
|
54
|
+
|
|
55
|
+
`on_list_tools` returns `list[Tool]` — filter/mutate the list before returning to hide tools from
|
|
56
|
+
specific clients.
|
|
57
|
+
|
|
58
|
+
## Denying requests
|
|
59
|
+
|
|
60
|
+
Raise, don't return falsy:
|
|
61
|
+
- `ToolError` — tool calls
|
|
62
|
+
- `ResourceError` — resource reads
|
|
63
|
+
- `PromptError` — prompt gets
|
|
64
|
+
- `McpError` — general/protocol-level
|
|
65
|
+
|
|
66
|
+
**`on_initialize` must reject *before* calling `call_next()`.** Raising after `call_next()`
|
|
67
|
+
returns only logs the error server-side — it does not reach the client, because the initialize
|
|
68
|
+
response has already been dispatched.
|
|
69
|
+
|
|
70
|
+
## Caching middleware — is a framework feature
|
|
71
|
+
|
|
72
|
+
FastMCP ships a built-in response-caching middleware:
|
|
73
|
+
`fastmcp.server.middleware.caching.ResponseCachingMiddleware`, confirmed directly against the
|
|
74
|
+
installed package (`.venv/lib/python3.14/site-packages/fastmcp/server/middleware/caching.py`,
|
|
75
|
+
593 lines). It comes with per-method settings classes — `CallToolSettings`,
|
|
76
|
+
`ReadResourceSettings`, `ListToolsSettings`, `ListResourcesSettings`, `ListPromptsSettings`,
|
|
77
|
+
`GetPromptSettings` (all `TypedDict`s controlling TTL/enable per operation) — plus
|
|
78
|
+
`ResponseCachingStatistics` and cache-key partitioning by auth token.
|
|
79
|
+
|
|
80
|
+
`morgenmcp/server.py` imports and configures this class directly (not a subclass): 60s TTL,
|
|
81
|
+
an explicit allowlist of cacheable read-only tools plus all resources. Upgrading FastMCP *can*
|
|
82
|
+
change this repo's caching behavior if a release touches `fastmcp.server.middleware.caching` —
|
|
83
|
+
check that module's diff specifically, not just the general `Middleware` base class, when
|
|
84
|
+
assessing upgrade risk (see reference/CHANGELOG.md for the version-by-version history and a
|
|
85
|
+
past false-negative on this exact claim).
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# Tool / resource / prompt registration
|
|
2
|
+
|
|
3
|
+
Sources: `docs/fastmcp/docs/servers/tools.mdx`, `docs/fastmcp/docs/servers/resources.mdx`.
|
|
4
|
+
|
|
5
|
+
## Registration forms
|
|
6
|
+
|
|
7
|
+
Bare decorator:
|
|
8
|
+
```python
|
|
9
|
+
@mcp.tool
|
|
10
|
+
def my_tool(x: int) -> int: ...
|
|
11
|
+
|
|
12
|
+
@mcp.resource("users://email/{email}")
|
|
13
|
+
def lookup_user(email: str) -> dict: ...
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
With options:
|
|
17
|
+
```python
|
|
18
|
+
@mcp.tool(name=..., description=..., tags={...}, meta={...}, timeout=float, output_schema={...})
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
**Call-expression form** (what MorgenMCP uses in `server.py`, so functions stay plain and
|
|
22
|
+
independently testable):
|
|
23
|
+
```python
|
|
24
|
+
mcp.resource("users://email/{email}")(lookup_user)
|
|
25
|
+
mcp.tool(name="morgen_list_events")(list_events)
|
|
26
|
+
```
|
|
27
|
+
This also lets one function register under multiple URIs/names by calling it more than once.
|
|
28
|
+
|
|
29
|
+
For bound methods, use `fastmcp.tools.tool()` standalone then `mcp.add_tool(bound_method)` —
|
|
30
|
+
the decorator form doesn't work directly on instance/class methods.
|
|
31
|
+
|
|
32
|
+
## Primitive-result auto-wrapping
|
|
33
|
+
|
|
34
|
+
MCP structured-output schemas require an object root. FastMCP handles this automatically based
|
|
35
|
+
on the function's return annotation:
|
|
36
|
+
- Annotated primitive (e.g. `-> int`) → wrapped into `structuredContent: {"result": 8}`, with
|
|
37
|
+
`"x-fastmcp-wrap-result": true` on the schema. Clients auto-unwrap this into `.data`.
|
|
38
|
+
- No return annotation → no `structuredContent` at all, only `content`.
|
|
39
|
+
- `dict` / dataclass / Pydantic model → always becomes structured content directly, no wrapping,
|
|
40
|
+
even without an explicit `output_schema`.
|
|
41
|
+
- Full manual control: return `ToolResult(content=..., structured_content=..., meta=...)` —
|
|
42
|
+
importable as `from fastmcp.tools import ToolResult` (or `fastmcp.tools.tool`, a runtime
|
|
43
|
+
`sys.modules` alias; the actual class lives in `fastmcp/tools/base.py`, not a `tool.py` file).
|
|
44
|
+
|
|
45
|
+
MorgenMCP's tools return `{"success": True, ...}` dicts (per `CLAUDE.md`), so they land in the
|
|
46
|
+
"dict → structured content directly" case — no wrapping envelope to worry about.
|
|
47
|
+
|
|
48
|
+
## Error handling
|
|
49
|
+
|
|
50
|
+
- `ToolError` (`fastmcp.exceptions`) messages are **always** sent to the client verbatim,
|
|
51
|
+
regardless of the `mask_error_details` setting on `FastMCP(...)`.
|
|
52
|
+
- Generic exceptions (`TypeError`, `ValueError`, etc.) get masked to a generic string when
|
|
53
|
+
`mask_error_details=True`.
|
|
54
|
+
- No `fastmcp.ValidationError` is documented for tool/resource handlers in these files —
|
|
55
|
+
argument validation failures come from Pydantic coercion. Coercion strictness is controlled by
|
|
56
|
+
`strict_input_validation` (default: flexible coercion; `FastMCP(strict_input_validation=True)`
|
|
57
|
+
enforces strict JSON-Schema validation instead).
|
|
58
|
+
- This is why `morgenmcp/tools/utils.py`'s `@handle_tool_errors` converts `ValidationError`,
|
|
59
|
+
`MorgenAPIError`, and unexpected exceptions to `ToolError` — it's the only exception type
|
|
60
|
+
guaranteed to reach the client with a useful message.
|
|
61
|
+
|
|
62
|
+
## RFC 6570 URI templates (resources)
|
|
63
|
+
|
|
64
|
+
`docs/fastmcp/docs/servers/resources.mdx` §"RFC 6570 URI Templates":
|
|
65
|
+
- `{param}` — exactly one path segment, does not cross `/`.
|
|
66
|
+
- `{param*}` — wildcard, captures multiple segments up to the next literal/param:
|
|
67
|
+
`@mcp.resource("path://{filepath*}")`.
|
|
68
|
+
- `{?param1,param2}` — form-style query params (v2.13.0+). Must map to *optional* (defaulted)
|
|
69
|
+
function params; path params must map to *required* params:
|
|
70
|
+
```python
|
|
71
|
+
@mcp.resource("data://{id}{?format}")
|
|
72
|
+
def get_data(id: str, format: str = "json"): ...
|
|
73
|
+
```
|
|
74
|
+
Values are auto-coerced to `int`/`float`/`bool`/`str` per type hint.
|
|
75
|
+
|
|
76
|
+
## Return type: `ResourceResult`
|
|
77
|
+
|
|
78
|
+
Introduced v3.0.0. `ResourceResult(contents=str|bytes|list[ResourceContent], meta=dict|None)`;
|
|
79
|
+
`ResourceContent(content=Any, mime_type=str|None, meta=dict|None)`. Plain `str` becomes
|
|
80
|
+
`TextResourceContents`, `bytes` becomes `BlobResourceContents` (base64-encoded). Simple resources
|
|
81
|
+
can just return `str`/`bytes` directly without constructing `ResourceResult`.
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# Testing patterns
|
|
2
|
+
|
|
3
|
+
Source: `docs/fastmcp/docs/development/tests.mdx`.
|
|
4
|
+
|
|
5
|
+
## In-memory Client testing (the pattern to use)
|
|
6
|
+
|
|
7
|
+
Pass the `FastMCP` server instance directly to `Client` — this runs the real MCP protocol in the
|
|
8
|
+
same process, no network transport, no subprocess:
|
|
9
|
+
|
|
10
|
+
```python
|
|
11
|
+
from fastmcp import FastMCP, Client
|
|
12
|
+
|
|
13
|
+
server = FastMCP("WeatherServer")
|
|
14
|
+
|
|
15
|
+
@server.tool
|
|
16
|
+
def get_temperature(city: str) -> dict:
|
|
17
|
+
return {"city": city, "temp": temps.get(city, 70)}
|
|
18
|
+
|
|
19
|
+
async def test_weather_operations():
|
|
20
|
+
async with Client(server) as client:
|
|
21
|
+
result = await client.call_tool("get_temperature", {"city": "NYC"})
|
|
22
|
+
assert result.data == {"city": "NYC", "temp": 72}
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
**Don't open `Client` inside a pytest fixture** — event-loop lifecycle issues. Only build/return
|
|
26
|
+
the server instance from fixtures; open the `Client` context manager inside the test function
|
|
27
|
+
itself.
|
|
28
|
+
|
|
29
|
+
This is exactly the pattern MorgenMCP's `test_mcp_server.py` uses: `fastmcp.Client(mcp)` against
|
|
30
|
+
the real server object, with `MORGENMCP_DATA_DIR=tmp_path` to isolate the persistent ID store.
|
|
31
|
+
|
|
32
|
+
## Real network transport (rarely needed)
|
|
33
|
+
|
|
34
|
+
Two options, in order of preference:
|
|
35
|
+
- `fastmcp.utilities.tests.run_server_async(server) as url` — in-process, async context manager.
|
|
36
|
+
Prefer this when you actually need a URL (e.g. testing HTTP-specific behavior).
|
|
37
|
+
- `run_server_in_process` — spawns a real subprocess. Only use when true process isolation is
|
|
38
|
+
required (e.g. testing stdio transport specifically, or crash isolation).
|
|
39
|
+
|
|
40
|
+
MorgenMCP doesn't currently need either — all tests go through the in-memory `Client` pattern or
|
|
41
|
+
mock the HTTP client directly (`test_client.py` via `@respx.mock`, `test_tools.py` via
|
|
42
|
+
`patch("morgenmcp.tools.*.get_client")`).
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# Low-maintenance Dependabot config: monthly, grouped updates so a quiet month
|
|
2
|
+
# yields one PR per ecosystem. Submodules and pre-commit hooks are left out on
|
|
3
|
+
# purpose (submodules are version-pinned; stale hooks are harmless).
|
|
4
|
+
version: 2
|
|
5
|
+
|
|
6
|
+
updates:
|
|
7
|
+
- package-ecosystem: uv
|
|
8
|
+
directory: /
|
|
9
|
+
schedule:
|
|
10
|
+
interval: monthly
|
|
11
|
+
open-pull-requests-limit: 3
|
|
12
|
+
commit-message:
|
|
13
|
+
prefix: "chore(deps)"
|
|
14
|
+
groups:
|
|
15
|
+
python-minor-patch:
|
|
16
|
+
patterns: ["*"]
|
|
17
|
+
update-types: ["minor", "patch"]
|
|
18
|
+
ignore:
|
|
19
|
+
# fastmcp is submodule-pinned; only patch bumps are wanted here.
|
|
20
|
+
- dependency-name: fastmcp
|
|
21
|
+
update-types: ["version-update:semver-major", "version-update:semver-minor"]
|
|
22
|
+
|
|
23
|
+
- package-ecosystem: github-actions
|
|
24
|
+
directory: /
|
|
25
|
+
schedule:
|
|
26
|
+
interval: monthly
|
|
27
|
+
open-pull-requests-limit: 3
|
|
28
|
+
commit-message:
|
|
29
|
+
prefix: "chore(ci)"
|
|
30
|
+
groups:
|
|
31
|
+
actions:
|
|
32
|
+
patterns: ["*"]
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
branches: [main]
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
test:
|
|
11
|
+
runs-on: ${{ matrix.os }}
|
|
12
|
+
strategy:
|
|
13
|
+
matrix:
|
|
14
|
+
os: [ubuntu-latest, macos-latest, windows-latest]
|
|
15
|
+
python-version: ["3.14"]
|
|
16
|
+
include:
|
|
17
|
+
# Also test requires-python's floor, so newer syntax fails here.
|
|
18
|
+
- os: ubuntu-latest
|
|
19
|
+
python-version: "3.12"
|
|
20
|
+
|
|
21
|
+
steps:
|
|
22
|
+
- uses: actions/checkout@v7
|
|
23
|
+
|
|
24
|
+
- name: Install uv
|
|
25
|
+
uses: astral-sh/setup-uv@v7
|
|
26
|
+
with:
|
|
27
|
+
python-version: ${{ matrix.python-version }}
|
|
28
|
+
enable-cache: true
|
|
29
|
+
|
|
30
|
+
- name: Install dependencies
|
|
31
|
+
run: uv sync --all-extras
|
|
32
|
+
|
|
33
|
+
- name: Lint with ruff
|
|
34
|
+
run: uv run ruff check .
|
|
35
|
+
|
|
36
|
+
- name: Check formatting with ruff
|
|
37
|
+
run: uv run ruff format --check .
|
|
38
|
+
|
|
39
|
+
- name: Type check with pyright
|
|
40
|
+
run: uv run pyright
|
|
41
|
+
|
|
42
|
+
- name: Run tests
|
|
43
|
+
run: uv run pytest
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
name: Release
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
tags:
|
|
6
|
+
- 'v*'
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
test:
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
steps:
|
|
12
|
+
- uses: actions/checkout@v7
|
|
13
|
+
|
|
14
|
+
- name: Install uv
|
|
15
|
+
uses: astral-sh/setup-uv@v7
|
|
16
|
+
|
|
17
|
+
- name: Install dependencies
|
|
18
|
+
run: uv sync --all-extras
|
|
19
|
+
|
|
20
|
+
- name: Run tests
|
|
21
|
+
run: uv run pytest
|
|
22
|
+
|
|
23
|
+
release:
|
|
24
|
+
needs: test
|
|
25
|
+
runs-on: ubuntu-latest
|
|
26
|
+
environment: pypi
|
|
27
|
+
permissions:
|
|
28
|
+
contents: write
|
|
29
|
+
id-token: write
|
|
30
|
+
|
|
31
|
+
steps:
|
|
32
|
+
- uses: actions/checkout@v7
|
|
33
|
+
with:
|
|
34
|
+
fetch-depth: 0
|
|
35
|
+
|
|
36
|
+
- name: Install uv
|
|
37
|
+
uses: astral-sh/setup-uv@v7
|
|
38
|
+
|
|
39
|
+
- name: Build sdist and wheel
|
|
40
|
+
run: uv build
|
|
41
|
+
|
|
42
|
+
- name: Create GitHub Release
|
|
43
|
+
env:
|
|
44
|
+
GH_TOKEN: ${{ github.token }}
|
|
45
|
+
run: gh release create ${{ github.ref_name }} dist/* --generate-notes --title "${{ github.ref_name }}"
|
|
46
|
+
|
|
47
|
+
- name: Publish to PyPI
|
|
48
|
+
run: uv publish
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*$py.class
|
|
5
|
+
*.so
|
|
6
|
+
.Python
|
|
7
|
+
build/
|
|
8
|
+
develop-eggs/
|
|
9
|
+
dist/
|
|
10
|
+
downloads/
|
|
11
|
+
eggs/
|
|
12
|
+
.eggs/
|
|
13
|
+
lib/
|
|
14
|
+
lib64/
|
|
15
|
+
parts/
|
|
16
|
+
sdist/
|
|
17
|
+
var/
|
|
18
|
+
wheels/
|
|
19
|
+
*.egg-info/
|
|
20
|
+
.installed.cfg
|
|
21
|
+
*.egg
|
|
22
|
+
|
|
23
|
+
# Virtual environments
|
|
24
|
+
.venv/
|
|
25
|
+
venv/
|
|
26
|
+
ENV/
|
|
27
|
+
|
|
28
|
+
# PyCharm / JetBrains IDEs
|
|
29
|
+
.idea/
|
|
30
|
+
|
|
31
|
+
# Junie (JetBrains AI assistant)
|
|
32
|
+
.junie/
|
|
33
|
+
|
|
34
|
+
# VS Code
|
|
35
|
+
.vscode/
|
|
36
|
+
|
|
37
|
+
# Editor swap files
|
|
38
|
+
*.swp
|
|
39
|
+
*.swo
|
|
40
|
+
*~
|
|
41
|
+
|
|
42
|
+
# Testing
|
|
43
|
+
.pytest_cache/
|
|
44
|
+
.coverage
|
|
45
|
+
htmlcov/
|
|
46
|
+
.tox/
|
|
47
|
+
.nox/
|
|
48
|
+
|
|
49
|
+
# Local settings and secrets — track shared config, ignore the rest
|
|
50
|
+
.claude/*
|
|
51
|
+
!.claude/settings.json
|
|
52
|
+
!.claude/hooks
|
|
53
|
+
!.claude/skills
|
|
54
|
+
.env
|
|
55
|
+
.env.local
|
|
56
|
+
*.env
|
|
57
|
+
.envrc
|
|
58
|
+
mise.local.toml
|
|
59
|
+
|
|
60
|
+
# OS files
|
|
61
|
+
.DS_Store
|
|
62
|
+
Thumbs.db
|
|
63
|
+
|
|
64
|
+
# Generated at build time by hatch-vcs (see [tool.hatch.build.hooks.vcs])
|
|
65
|
+
morgenmcp/_version.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
3.12
|