* Scope tasks to authorization context, not session
Tasks were keyed by the transport-layer Mcp-Session-Id, which is
server-assigned and changes on reconnect — so clients lost access to
their running tasks after any connection interruption.
The MCP spec says tasks should be bound to authorization context, not
session. This replaces session_id with task_scope (derived from
AccessToken.client_id, URL-encoded) in all task data Redis keys and
Docket task keys. When no auth is configured, a "_" sentinel is used
and security comes from UUID task ID entropy per the spec.
Session ID is still used for transport-level concerns (notification
queues, subscriber registration) and is now stored in the
TaskContextSnapshot payload so background workers can still deliver
notifications.
Also extracts all the task context infrastructure (TaskContextInfo,
TaskContextSnapshot, snapshot loading, session/server registries) from
server/dependencies.py into a new server/tasks/context.py to keep the
DI module from sprawling further.
Closes#3758🤖 Generated with Claude Code
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Rename _redis_key to _snapshot_redis_key
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Document in-process session registry as an optimization
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Tidy imports and docstrings
Hoist imports where safe, keep subscriptions/notifications deferred in
handlers.py since they pull in docket at module level. Sharpen docstrings
on keys.py and context.py so each module owns its lane. Clean up the
re-export block in dependencies.py.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Fix misleading comment on re-export block
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Tighten task scope: include sub claim, partition keyspaces
Addresses review feedback on #3800:
- Compose task scope from client_id and the JWT sub claim (when present)
so fixed-OAuth deployments isolate per user, not just per client.
- Replace the "_" anonymous sentinel with a tagged keyspace partition.
Docket keys are now auth:{enc_scope}:... or anon:..., and Redis keys
use fastmcp:task:auth:{enc_scope}:... or fastmcp:task:anon:...,
routed through a single task_redis_prefix() helper.
- get_task_scope() returns the raw scope (or None); encoding happens
once at the keys.py boundary, collapsing the previous double-quote
invariant.
- Drop the dormant fallback in notifications.py that routed
input_required relays into the anon keyspace when task_scope was
missing -- log and skip instead.
- Add comprehensive parser/encoder tests in test_task_keys.py covering
round-trips, malformed keys, and adversarial scopes ("anon", "_", and
scopes containing : / | %).
- Add cross-scope rejection tests: distinct client_ids, distinct sub
claims under a shared client_id, and authenticated vs anonymous.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
When a tool returns `list[dict]`, the client deserializes each dict as a
`Root()` dataclass with no fields instead of preserving the original dict
data.
The root cause is in `_get_from_type_handler`: its `"object"` branch
always fell through to `_create_dataclass` for schemas without
`properties`, creating an empty dataclass named `Root`. The top-level
`json_schema_to_type` already handled this case correctly (returning
`dict[str, Any]`), but that logic was not shared with `_schema_to_type`
which is used when converting nested schemas (e.g., array items).
Extract `_object_schema_to_type` to unify the four object-schema cases
(dict, typed dict, BaseModel with extra, dataclass) so both top-level
and nested paths produce the correct type.
Fixes#3867
Co-authored-by: Ke Wang <ke@pika.art>
Closes#1707
- Add example for configuring mcp.json with uv-managed projects (pyproject.toml)
- Add examples for running published pip packages via uvx
- Update both main and v2 docs
Co-authored-by: Emily Chen <emilychen.techwriter@gmail.com>
Co-authored-by: Jeremiah Lowin <153965+jlowin@users.noreply.github.com>
- Delete entirely commented-out test_run_server.py (99 lines dead code)
- Fix test_pydantic_model_with_stringified_json_no_strict: replace
try/except-both-branches-pass with clear pytest.raises assertion
- Fix test_path_traversal_blocked: remove dead assertions after
pytest.raises (lines after raise never execute)
Error handling middleware test fixes are in a separate PR (#3858)
which also fixes the underlying RetryMiddleware bug.
🤖 Generated with Claude Code
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Cap consecutive final_response validation retries to 3
Previously, when the LLM repeatedly called final_response with data that
failed validation, the retry loop would continue up to 100 times (the
shared max_iterations limit), wasting tokens on a model that cannot
satisfy the schema.
Add _MAX_VALIDATION_RETRIES (default 3) that caps consecutive validation
failures. The counter resets when the LLM calls other tools (not
final_response), so the cap only applies to consecutive failures.
Fixes#3848
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Add tests for consecutive validation retry cap
Tests cover:
- Validation failures within cap followed by success
- Consecutive validation failures exceeding cap (raises RuntimeError)
- Counter reset when LLM calls other tools between validation failures
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Slim down validation retry cap tests
Reduce boilerplate with helper functions.
Simplify counter-reset test from 5 calls to 4.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Fix static analysis: move imports to module level and format
Move CreateMessageResultWithTools and ToolUseContent imports to the
top of the test file so ty can resolve the names used in return-type
annotations of the helper functions. Also fix ruff import sorting
and formatting issues.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Align validation retry semantics with text-response retries
Change `>=` to `>` so _MAX_VALIDATION_RETRIES means "number of
retries after the initial attempt" (total = N+1), matching the
convention used by _MAX_TEXT_RESPONSE_RETRIES in the text-response
retry path.
Before: _MAX=3 meant 3 total attempts (>= comparison)
After: _MAX=3 means 1 initial + 3 retries = 4 total (> comparison)
🤖 Generated with Claude Code
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Fix GoogleGenaiSamplingHandler thought part leaking and unhelpful errors
- Filter thought parts (part.thought=True) from response content instead
of leaking them as TextContent in _response_to_result_with_tools
- Include finish_reason in error messages when no content is found, so
safety-filtered responses (SAFETY, RECITATION, etc.) are distinguishable
- Add specific error message for thinking-only responses in
_response_to_create_message_result
Fixes#3846
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Add tests for thought part filtering and error message improvements
Tests cover:
- Thought parts filtered from tool-path responses
- Thought-only responses produce descriptive errors
- Safety-filtered responses include finish_reason in error
- Normal responses (text + function calls) unaffected
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Fix ruff format and ty check issues
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Fix ty check errors in tests
Remove unused ty: ignore comments from lines where isinstance() narrows
the type, and add correct ty: ignore[invalid-argument-type] and
ty: ignore[not-subscriptable] comments on lines in newly added test
functions where ty cannot infer the union type is a list. Also apply
ruff format fix in test_task_return_types.py.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Fix: use all() not any() for thinking-only detection
Addresses review feedback: any() would misclassify mixed responses
(thought + function_call) as thinking-only, hiding the real error.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Fix broken code examples in docs
- Tag error output blocks as ```text instead of ```python (anthropic,
openai integration docs + v2 mirrors)
- Quote unquoted URL in Descope config example (+ v2 mirror)
- Fix GoogleGenAISamplingHandler → GoogleGenaiSamplingHandler casing
in sampling docs
- Fix import path: handlers.GoogleGenaiSamplingHandler →
handlers.google_genai.GoogleGenaiSamplingHandler in v3-features
🤖 Generated with Claude Code
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Fix remaining broken doc examples and add skip tags for false positives
- BearerTokenAuth → StaticTokenVerifier in deployment/http.mdx
- providers.oauth → server.auth import in authentication.mdx
- ListToolsNext → updated list_tools API in v3-features.mdx
- OAuthClientProvider → OAuth in v2/storage-backends.mdx
- Add test="skip" for upgrade guides, contrib placeholders, f-string backticks
🤖 Generated with Claude Code
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Ratchet doc example baselines to zero
All 1444 examples now pass syntax and import checks.
🤖 Generated with Claude Code
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Add pytest-examples dev dep, fix client_id in StaticTokenVerifier example, commit missed openapi fixes
- Add pytest-examples to dev dependencies (fixes CI ModuleNotFoundError)
- Include required client_id in StaticTokenVerifier token payload
- Commit previously unstaged HTTPRoute import fixes in openapi.mdx
🤖 Generated with Claude Code
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Update deprecated import paths across docs
- fastmcp.server.openapi → fastmcp.server.providers.openapi
- fastmcp.server.proxy → fastmcp.server.providers.proxy
- fastmcp.server.apps → fastmcp.apps
- Tag upgrade guide "Before" examples with test="skip"
🤖 Generated with Claude Code
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Raise on unhandled content types in sampling handler dispatch chains
The Anthropic and OpenAI sampling handlers have isinstance chains that
dispatch on MCP content types but silently drop unhandled variants like
EmbeddedResource and ResourceLink. This adds explicit else-raise guards
to match the Gemini handler's behavior and the single-content dispatch
paths that already raise.
Raising is the right choice over warn-and-skip: a partial conversion
produces a plausible-but-wrong LLM response (the model confidently
answers based on incomplete input), which is worse than a clear error
that tells the user exactly what isn't supported.
🤖 Generated with Claude Code
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Add tests for unsupported content type raises in sampling handlers
Tests the new ValueError raises for unsupported content types
(e.g. EmbeddedResource) in the Anthropic and OpenAI message
conversion loops. Uses model_construct to bypass Pydantic's
union validation since the raise is a defensive guard for
future SDK content types.
🤖 Generated with Claude Code
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Retry when LLM returns text instead of calling final_response tool
Instead of raising RuntimeError immediately when the LLM returns a text
response instead of calling the `final_response` tool for structured
output, retry up to 3 times with an explicit nudge message asking the
model to use the tool. This mirrors the existing retry behavior for
validation errors but with a separate, smaller cap.
Fixes#3847
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Add tests for text response retry logic
Tests cover:
- Text response followed by successful final_response (retry works)
- Text response exceeding max retries (raises RuntimeError)
- Nudge message appended to history on retry
- No retry when result_type is None (text is valid)
Addresses review feedback from PR review tool (v1 flagged missing tests as high severity).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Slim down text response retry tests
Remove test_nudge_message_in_history (implementation detail).
Reduce boilerplate in remaining 3 tests.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Fix ruff format
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replaces the ___ separator for FastMCPApp backend tool routing with a
deterministic hash(app_name, tool_name) prefix, and replaces the shared
singleton prefab renderer resource with per-tool resources synthesized
on demand.
Backend tools are now callable via <hash>_<local_name> instead of
<app_name>___<local_name>. The dispatcher walks the provider tree
recursively via get_tool_by_hash (same pattern as get_app_tool).
Each prefab tool gets its own renderer resource at
ui://prefab/tool/<hash>/renderer.html with per-tool CSP — fixing the
bug where PrefabAppConfig(csp=...) never actually applied.
Closes#3735, closes#3805
* fix: strip title fields from tool schemas for Gemini compatibility
Gemini 2.5 Flash produces MALFORMED_FUNCTION_CALL when a function
declaration's parameters_json_schema contains 'title' fields (which
Pydantic adds by default). Strip them in _convert_tool_to_google_genai
before passing to the API.
Fixes#3860
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Fix ty errors and lowest-deps test failure
- Add assertions for non-None before subscripting FunctionDeclaration
fields (ty check)
- Test compress_schema directly instead of constructing FunctionDeclaration
which may not support parameters_json_schema in google-genai==1.18.0
🤖 Generated with Claude Code
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
When transport is None (the default), run_async resolves it to
settings.transport which defaults to "stdio". The previous guard
`transport != "stdio"` passed HTTP kwargs through for None transport,
causing TypeError in run_stdio_async.
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- AggregateProvider._collect_list_results detects duplicate component
names across providers, respecting the server's on_duplicate setting
- Provider errors logged at WARNING instead of DEBUG
- Parent server re-masks ToolErrors from mounted children at the
FastMCPError catch boundary instead of mutating the child server
Fixes#3825
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Enables stricter type checking by promoting rules that default to
ignore: division-by-zero, possibly-missing-attribute,
possibly-missing-import, possibly-unresolved-reference,
unsupported-dynamic-base, unsupported-operator, unused-ignore-comment.
6 of the 7 rules had zero violations. possibly-unresolved-reference
had 9 (5 in src/, 3 in tests/, 1 walrus-operator false positive
suppressed with ty: ignore).
🤖 Generated with Claude Code
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: replace `or` with `is not None` checks for config/override merging
Falsy-but-valid values like port=0 (OS-assigned), host="" (all interfaces),
and description="" (explicitly cleared) were silently dropped by `x or default`
patterns across CLI, transport, and component registration.
Fixes#3832
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: replace `or` with `is not None` for description in FunctionResourceTemplate
🤖 Generated with Claude Code
Co-authored-by: Jeremiah Lowin <jlowin@users.noreply.github.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Jeremiah Lowin <153965+jlowin@users.noreply.github.com>
Co-authored-by: Jeremiah Lowin <jlowin@users.noreply.github.com>
* Fix crash bugs in json_schema_to_type
- Handle boolean schemas (True/False) at the public entry point
- Append trailing underscore to Python keyword property names (PEP 8)
- Return Any for empty enum values instead of crashing Pydantic
- Deduplicate field names after sanitization to prevent collisions
(e.g. "foo-bar" and "foo_bar" both sanitizing to "foo_bar")
🤖 Generated with Claude Code
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Move local imports to module level in test_json_schema_type
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Jeremiah Lowin <153965+jlowin@users.noreply.github.com>
Integration test that runs json_schema_to_type against 232K schemas
from 4,120 real-world OpenAPI specs (APIs.guru openapi-directory).
Snapshots crash counts as regression baselines so future changes
can't silently increase the crash rate.
Current baseline (openapi-directory@f7207cf0):
TypeErrors: 2,342 (datetime serialization)
SchemaErrors: 273 (invalid regexes in specs)
Timeouts: 0
Other: 0
Skipped unless openapi-directory is cloned locally.
Run with: pytest -m integration tests/.../test_real_world_schemas.py
🤖 Generated with Claude Code
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Detect async/sync generators after tool execution and materialize
into lists before the result conversion pipeline processes them
- Generator materialization runs inside timeout scope so slow generators
respect the configured timeout
- Handle bytes return types: UTF-8 bytes as text, non-UTF-8 as base64
- Suppress output_schema for bytes return types (can't be structured JSON)
- Catch UnicodeDecodeError alongside PydanticSerializationError in
convert_result for robustness
Fixes#3829
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The store_files tool checked the client-provided `size` field to enforce
max_file_size, but this field is untrusted input. A client could set
size=1 while sending a multi-megabyte payload, bypassing the limit.
Now computes actual size from the base64 data length instead.
🤖 Generated with Claude Code
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
pydocket 0.19.0 fixes the fakeredis 2.35.0 FakeConnection rename
internally, so we no longer need to carry the fakeredis ceiling
ourselves. Removes the direct fakeredis[lua]<2.35.0 dependency from the
tasks extra entirely — it's just a transitive of pydocket now.
🤖 Generated with Claude Code
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>