fix(mcp): await fastmcp 3.x async get_state/set_state (closes #85)#86
Merged
Conversation
zzstoatzz
force-pushed
the
fix/fastmcp-3-async-state
branch
3 times, most recently
from
May 29, 2026 05:36
f35aa29 to
54c0d8f
Compare
fastmcp 3.x made `Context.get_state` and `Context.set_state` coroutines.
pdsx called them synchronously, so un-awaited coroutines were stored as
credentials and every MCP tool failed at runtime with
`'coroutine' object has no attribute 'startswith'/'endswith'`
(reads hit `repo.startswith("did:")`, auth hit `client.login`).
- bump `fastmcp>=3.3` (mcp extra + dev group); resolves 3.3.1
- await the async context-state API everywhere it's used:
- `_get_credentials_from_context`, `get_repo_from_context`, and
`resolve_pds_url` become async and await `ctx.get_state(...)` (client.py)
- middleware `_extract_credentials` becomes async and awaits each
`ctx.set_state(...)`; awaited from on_call_tool/on_read_resource
- await `get_repo_from_context()` / `resolve_pds_url()` at the server.py
call sites
- regression tests drive each helper + middleware through an async-state
context, asserting creds resolve to strings (fail on the old sync code)
also, to get CI green on this branch:
- test_query: in fastmcp 3.3.1 `@mcp.tool` registers the tool and returns
the original function (no `.fn` wrapper) — confirmed via `mcp.list_tools()`;
call `server.query(...)` directly at all 7 sites
- cli.py: narrow blob ref union (`str | bytes | IpldLink`) with an isinstance
guard so `.link` resolves cleanly under `ty`
- regenerate `docs/api-reference/*` so the mdxify pre-commit hook is a no-op
rebased on main (incl. the authenticated-query allowlist work).
full suite: 162 passed; prek (ruff/ty/mdxify) clean and idempotent.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
zzstoatzz
force-pushed
the
fix/fastmcp-3-async-state
branch
from
May 29, 2026 05:42
54c0d8f to
90d008b
Compare
zzstoatzz
added a commit
that referenced
this pull request
May 29, 2026
#86 awaited get_state/set_state unconditionally, which fixed local fastmcp 3.x but broke on FastMCP Cloud: the hosted runtime pins a fastmcp where those are still sync, so `await ctx.get_state(...)` raised `object NoneType can't be used in 'await' expression` on every tool call. the installed fastmcp version doesn't govern the hosted runtime, so detect the calling convention instead of assuming it: - add `_maybe_await()` — awaits the return value only if it's awaitable - route every `ctx.get_state(...)` (client.py) and `ctx.set_state(...)` (middleware.py) through it - regression tests now parametrize over `_FakeAsyncContext` (3.x) AND `_FakeSyncContext` (2.x / hosted); the sync variants fail on the bare-await code and pass with `_maybe_await` (verified by reverting) full suite: 166 passed; prek (ruff/ty/mdxify) clean and idempotent. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #85.
fastmcp3.x madeContext.get_state/Context.set_statecoroutines, but pdsx called them synchronously — so un-awaited coroutines were stored as credentials and every MCP tool failed at runtime:changes
fastmcp>=3.3(mcp extra + dev group) → resolves 3.3.1client.py: make_get_credentials_from_contextandget_repo_from_contextasync,awaiteachctx.get_state(...)middleware.py: make_extract_credentialsasync,awaiteachctx.set_state(...), andawaitit fromon_call_tool/on_read_resourceserver.py:await get_repo_from_context()at both call sitestests
TestAsyncStateRegression: drives the credential helpers + middleware through a fastmcp-3.x-style context whoseget_state/set_stateare coroutines, asserting credentials resolve to plain strings. Verified these fail on the pre-fix sync code (reverting the awaits → 3 failures).test_query.py: in fastmcp 3.3.1,@mcp.toolregisters the tool and returns the original function (no wrapper with a.fnattribute). Registration confirmed viamcp.list_tools()(all 8 tools present); updated the query tests to callserver.query(...)directly.ruff format/ruff checkclean. (tyreports one pre-existingpossibly-missing-attributeincli.py, unrelated to this change.)🤖 Generated with Claude Code