Merge pull request #3319 from PrefectHQ/update-published-docs

Fast-forward published-docs to include banner CSS fix
This commit is contained in:
Jeremiah Lowin 2026-02-27 11:42:43 -05:00 committed by GitHub
commit 243df39f04
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
18 changed files with 370 additions and 71 deletions

View file

@ -8,9 +8,10 @@ on:
types: [created]
permissions:
actions: read
contents: write
issues: write
pull-requests: read
pull-requests: write
id-token: write
jobs:
@ -20,7 +21,7 @@ jobs:
contains(github.event.comment.body, '/marvin') &&
contains(fromJSON('["OWNER", "MEMBER", "COLLABORATOR"]'), github.event.comment.author_association)
runs-on: ubuntu-latest
timeout-minutes: 15
timeout-minutes: 60
steps:
- name: Checkout repository
@ -79,12 +80,9 @@ jobs:
</task>
<constraints>
This workflow allows read, write, and execute capabilities but cannot push changes.
You CAN: Read/analyze code, modify files, write code, run tests, execute commands
You CANNOT: Commit code, push changes, create branches, checkout branches, create pull requests
You CAN: Commit code, push changes, create branches, create pull requests
**Important**: You cannot push changes to the repository - you can only make changes locally and provide feedback or recommendations.
</constraints>
<allowed_tools>

View file

@ -1,10 +1,10 @@
name: Update MCPServerConfig Schema
# Regenerates config schema on PRs and commits it back to the branch,
# so the PR is self-contained and main is correct after merge.
# Regenerates config schema on pushes to main and opens a long-lived PR
# with the changes, so contributor PRs stay clean.
on:
pull_request:
push:
branches: ["main"]
paths:
- "src/fastmcp/utilities/mcp_server_config/**"
@ -13,14 +13,12 @@ on:
permissions:
contents: write
pull-requests: write
jobs:
update-config-schema:
timeout-minutes: 5
runs-on: ubuntu-latest
if: >-
github.event_name == 'workflow_dispatch' ||
github.event.pull_request.head.repo.full_name == github.repository
steps:
- name: Generate Marvin App token
@ -32,7 +30,6 @@ jobs:
- uses: actions/checkout@v6
with:
ref: ${{ github.head_ref || github.ref }}
token: ${{ steps.marvin-token.outputs.token }}
- name: Install uv
@ -53,15 +50,23 @@ jobs:
generate_schema('src/fastmcp/utilities/mcp_server_config/v1/schema.json')
"
- name: Commit and push if changed
run: |
git config user.name "marvin-context-protocol[bot]"
git config user.email "225465937+marvin-context-protocol[bot]@users.noreply.github.com"
git add docs/public/schemas/ src/fastmcp/utilities/mcp_server_config/v1/schema.json
if git diff --cached --quiet; then
echo "Config schema is up to date"
else
git commit -m "chore: Update fastmcp.json schema"
git push
echo "Config schema updated and pushed"
fi
- name: Create Pull Request
uses: peter-evans/create-pull-request@v8
with:
token: ${{ steps.marvin-token.outputs.token }}
commit-message: "chore: Update fastmcp.json schema"
title: "chore: Update fastmcp.json schema"
body: |
This PR updates the fastmcp.json schema files to match the current source code.
The schema is automatically generated from `src/fastmcp/utilities/mcp_server_config/` to ensure consistency.
**Note:** This PR is fully automated and will update itself with any subsequent changes to the schema, or close automatically if the schema becomes up-to-date through other means.
🤖 Generated by Marvin
branch: marvin/update-config-schema
labels: |
ignore in release notes
delete-branch: true
author: "marvin-context-protocol[bot] <225465937+marvin-context-protocol[bot]@users.noreply.github.com>"
committer: "marvin-context-protocol[bot] <225465937+marvin-context-protocol[bot]@users.noreply.github.com>"

View file

@ -1,10 +1,10 @@
name: Update SDK Documentation
# Regenerates SDK docs on PRs and commits them back to the branch,
# so the PR is self-contained and main is correct after merge.
# Regenerates SDK docs on pushes to main and opens a long-lived PR
# with the changes, so contributor PRs stay clean.
on:
pull_request:
push:
branches: ["main"]
paths:
- "src/**"
@ -13,14 +13,12 @@ on:
permissions:
contents: write
pull-requests: write
jobs:
update-sdk-docs:
timeout-minutes: 5
runs-on: ubuntu-latest
if: >-
github.event_name == 'workflow_dispatch' ||
github.event.pull_request.head.repo.full_name == github.repository
steps:
- name: Generate Marvin App token
@ -32,7 +30,6 @@ jobs:
- uses: actions/checkout@v6
with:
ref: ${{ github.head_ref || github.ref }}
token: ${{ steps.marvin-token.outputs.token }}
- name: Install uv
@ -50,15 +47,23 @@ jobs:
- name: Generate SDK documentation
run: just api-ref-all
- name: Commit and push if changed
run: |
git config user.name "marvin-context-protocol[bot]"
git config user.email "225465937+marvin-context-protocol[bot]@users.noreply.github.com"
git add docs/python-sdk/
if git diff --cached --quiet; then
echo "SDK documentation is up to date"
else
git commit -m "chore: Update SDK documentation"
git push
echo "SDK documentation updated and pushed"
fi
- name: Create Pull Request
uses: peter-evans/create-pull-request@v8
with:
token: ${{ steps.marvin-token.outputs.token }}
commit-message: "chore: Update SDK documentation"
title: "chore: Update SDK documentation"
body: |
This PR updates the auto-generated SDK documentation to reflect the latest source code changes.
📚 Documentation is automatically generated from the source code docstrings and type annotations.
**Note:** This PR is fully automated and will update itself with any subsequent changes to the SDK, or close automatically if the documentation becomes up-to-date through other means. Feel free to leave it open until you're ready to merge.
🤖 Generated by Marvin
branch: marvin/update-sdk-docs
labels: |
ignore in release notes
delete-branch: true
author: "marvin-context-protocol[bot] <225465937+marvin-context-protocol[bot]@users.noreply.github.com>"
committer: "marvin-context-protocol[bot] <225465937+marvin-context-protocol[bot]@users.noreply.github.com>"

View file

@ -96,7 +96,8 @@ When modifying MCP functionality, changes typically need to be applied across al
- Uses Mintlify framework
- Files must be in docs.json to be included
- Do not manually modify `docs/python-sdk/**` — a bot automatically updates these files via commits added to PRs. Changes to these files in PR diffs are expected and should not be flagged during review.
- Do not manually modify `docs/python-sdk/**` — these files are auto-generated from source code by a bot and maintained via a long-lived PR. Do not include changes to these files in contributor PRs.
- Do not manually modify `docs/public/schemas/**` or `src/fastmcp/utilities/mcp_server_config/v1/schema.json` — these are auto-generated and maintained via a long-lived PR.
- **Core Principle:** A feature doesn't exist unless it is documented!
### Documentation Guidelines

View file

@ -5,6 +5,23 @@ rss: true
tag: NEW
---
<Update label="v3.0.2" description="2026-02-22">
**[v3.0.2: Threecovery Mode II](https://github.com/PrefectHQ/fastmcp/releases/tag/v3.0.2)**
Two community-contributed fixes: auth headers from MCP transport no longer leak through to downstream OpenAPI APIs, and background task workers now correctly receive the originating request ID. Plus a new docs example for context-aware tool factories.
### Fixes 🐞
* fix: prevent MCP transport auth header from leaking to downstream OpenAPI APIs by [@stakeswky](https://github.com/stakeswky) in [#3262](https://github.com/PrefectHQ/fastmcp/pull/3262)
* fix: propagate origin_request_id to background task workers by [@gfortaine](https://github.com/gfortaine) in [#3175](https://github.com/PrefectHQ/fastmcp/pull/3175)
### Docs 📚
* Add v3.0.1 release notes by [@jlowin](https://github.com/jlowin) in [#3259](https://github.com/PrefectHQ/fastmcp/pull/3259)
* docs: add context-aware tool factory example by [@machov](https://github.com/machov) in [#3264](https://github.com/PrefectHQ/fastmcp/pull/3264)
**Full Changelog**: [v3.0.1...v3.0.2](https://github.com/PrefectHQ/fastmcp/compare/v3.0.1...v3.0.2)
</Update>
<Update label="v3.0.1" description="2026-02-20">
**[v3.0.1: Three-covery Mode](https://github.com/PrefectHQ/fastmcp/releases/tag/v3.0.1)**
@ -13,7 +30,6 @@ First patch after 3.0 — mostly smoothing out rough edges discovered in the wil
### Enhancements 🔧
* Add verify_id_token option to OIDCProxy by [@jlowin](https://github.com/jlowin) in [#3248](https://github.com/PrefectHQ/fastmcp/pull/3248)
### Fixes 🐞
* Fix v3.0.0 changelog compare link by [@jlowin](https://github.com/jlowin) in [#3223](https://github.com/PrefectHQ/fastmcp/pull/3223)
* Fix MDX parse error in upgrade guide prompts by [@jlowin](https://github.com/jlowin) in [#3227](https://github.com/PrefectHQ/fastmcp/pull/3227)
@ -27,6 +43,7 @@ First patch after 3.0 — mostly smoothing out rough edges discovered in the wil
* Fix ty compatibility with upgraded deps by [@jlowin](https://github.com/jlowin) in [#3257](https://github.com/PrefectHQ/fastmcp/pull/3257)
* Fix decorator overload return types for function mode by [@jlowin](https://github.com/jlowin) in [#3258](https://github.com/PrefectHQ/fastmcp/pull/3258)
### Docs 📚
* Sync README with welcome.mdx, fix install count by [@jlowin](https://github.com/jlowin) in [#3224](https://github.com/PrefectHQ/fastmcp/pull/3224)
* Document dict-to-Message prompt migration in upgrade guides by [@jlowin](https://github.com/jlowin) in [#3225](https://github.com/PrefectHQ/fastmcp/pull/3225)

View file

@ -6,7 +6,6 @@
font-weight: 600 !important;
padding-top: 12px !important;
padding-bottom: 12px !important;
position: relative !important;
overflow: hidden !important;
}

View file

@ -191,3 +191,41 @@ mcp.add_tool(safe_division)
The `forward()` function handles argument mapping automatically. Call it with the transformed argument names, and it maps them back to the original function's parameters.
For direct access to the original function without mapping, use `forward_raw()` with the original parameter names.
## Context-Aware Tool Factories
You can write functions that act as "factories," generating specialized versions of a tool for different contexts. For example, create a `get_my_data` tool for the current user by hiding the `user_id` parameter and providing it automatically.
```python
from fastmcp import FastMCP
from fastmcp.tools import Tool, tool
from fastmcp.tools.tool_transform import ArgTransform
# A generic tool that requires a user_id
@tool
def get_user_data(user_id: str, query: str) -> str:
"""Fetch data for a specific user."""
return f"Data for user {user_id}: {query}"
def create_user_tool(user_id: str) -> Tool:
"""Factory that creates a user-specific version of get_user_data."""
return Tool.from_tool(
get_user_data,
name="get_my_data",
description="Fetch your data. No need to specify a user ID.",
transform_args={
"user_id": ArgTransform(hide=True, default=user_id),
},
)
# Create a server with a tool customized for the current user
mcp = FastMCP("User Server")
current_user_id = "user-123" # e.g., from auth context
mcp.add_tool(create_user_tool(current_user_id))
# Clients see "get_my_data(query: str)" — user_id is injected automatically
```
This pattern is useful for multi-tenant servers where each connection gets tools pre-configured with their identity, or for wrapping generic tools with environment-specific defaults.

View file

@ -5,6 +5,16 @@ icon: "sparkles"
tag: NEW
---
<Update label="FastMCP 3.0.2" description="February 22, 2026" tags={["Releases"]}>
<Card
title="FastMCP v3.0.2: Threecovery Mode II"
href="https://github.com/PrefectHQ/fastmcp/releases/tag/v3.0.2"
cta="Read the release notes"
>
Two community-contributed fixes: auth headers from MCP transport no longer leak through to downstream OpenAPI APIs, and background task workers now correctly receive the originating request ID. Plus a new docs example for context-aware tool factories.
</Card>
</Update>
<Update label="FastMCP 3.0.1" description="February 20, 2026" tags={["Releases"]}>
<Card
title="FastMCP v3.0.1: Three-covery Mode"

View file

@ -705,3 +705,35 @@ You can chain transformations by using an already transformed tool as the parent
### Context-Aware Tool Factories
You can write functions that act as "factories," generating specialized versions of a tool for different contexts. For example, you could create a `get_my_data` tool that is specific to the currently logged-in user by hiding the `user_id` parameter and providing it automatically.
```python
from fastmcp import FastMCP
from fastmcp.tools import Tool, tool
from fastmcp.tools.tool_transform import ArgTransform
# A generic tool that requires a user_id
@tool
def get_user_data(user_id: str, query: str) -> str:
"""Fetch data for a specific user."""
return f"Data for user {user_id}: {query}"
def create_user_tool(user_id: str) -> Tool:
"""Factory that creates a user-specific version of get_user_data."""
return Tool.from_tool(
get_user_data,
name="get_my_data",
description="Fetch your data. No need to specify a user ID.",
transform_args={
"user_id": ArgTransform(hide=True, default=user_id),
},
)
# Create a server with a tool customized for the current user
mcp = FastMCP("User Server")
current_user_id = "user-123" # e.g., from auth context
mcp.add_tool(create_user_tool(current_user_id))
# Clients see "get_my_data(query: str)" — user_id is injected automatically
```

View file

@ -95,7 +95,7 @@ class StreamableHttpTransport(ClientTransport):
# Load headers from an active HTTP request, if available. This will only be true
# if the client is used in a FastMCP Proxy, in which case the MCP client headers
# need to be forwarded to the remote server.
headers = get_http_headers() | self.headers
headers = get_http_headers(include={"authorization"}) | self.headers
# Configure timeout if provided, preserving MCP's 30s connect default
timeout: httpx.Timeout | None = None

View file

@ -69,7 +69,9 @@ class SSETransport(ClientTransport):
# load headers from an active HTTP request, if available. This will only be true
# if the client is used in a FastMCP Proxy, in which case the MCP client headers
# need to be forwarded to the remote server.
client_kwargs["headers"] = get_http_headers() | self.headers
client_kwargs["headers"] = (
get_http_headers(include={"authorization"}) | self.headers
)
# sse_read_timeout has a default value set, so we can't pass None without overriding it
# instead we simply leave the kwarg out if it's not provided

View file

@ -191,12 +191,14 @@ class Context:
session: ServerSession | None = None,
*,
task_id: str | None = None,
origin_request_id: str | None = None,
):
self._fastmcp: weakref.ref[FastMCP] = weakref.ref(fastmcp)
self._session: ServerSession | None = session # For state ops during init
self._tokens: list[Token] = []
# Background task support (SEP-1686)
self._task_id: str | None = task_id
self._origin_request_id: str | None = origin_request_id
# Request-scoped state for non-serializable values (serializable=False)
self._request_state: dict[str, Any] = {}
@ -227,6 +229,18 @@ class Context:
"""
return self._task_id
@property
def origin_request_id(self) -> str | None:
"""Get the request ID that originated this execution, if available.
In foreground request mode, this is the current request_id.
In background task mode, this is the request_id captured when the task
was submitted, if one was available.
"""
if self.request_context is not None:
return str(self.request_context.request_id)
return self._origin_request_id
@property
def fastmcp(self) -> FastMCP:
"""Get the FastMCP instance."""
@ -533,13 +547,14 @@ class Context:
extra: Optional mapping for additional arguments
"""
data = LogData(msg=message, extra=extra)
related_request_id = self.origin_request_id
await _log_to_server_and_client(
data=data,
session=self.session,
level=level or "info",
logger_name=logger_name,
related_request_id=self.request_id,
related_request_id=related_request_id,
)
@property

View file

@ -270,11 +270,14 @@ def transform_context_annotations(fn: Callable[..., Any]) -> Callable[..., Any]:
# First pass: identify which params need transformation
params_to_transform: set[str] = set()
optional_context_params: set[str] = set()
for name, param in sig.parameters.items():
annotation = type_hints.get(name, param.annotation)
if is_class_member_of_type(annotation, Context):
if not isinstance(param.default, Dependency):
params_to_transform.add(name)
if param.default is None:
optional_context_params.add(name)
if not params_to_transform:
return fn
@ -300,7 +303,10 @@ def transform_context_annotations(fn: Callable[..., Any]) -> Callable[..., Any]:
# We use CurrentContext() instead of Depends(get_context) because
# get_context() returns the Context which is an AsyncContextManager,
# and the DI system would try to enter it again (it's already entered)
param = param.replace(default=CurrentContext())
if name in optional_context_params:
param = param.replace(default=OptionalCurrentContext())
else:
param = param.replace(default=CurrentContext())
# Sort into buckets based on parameter kind
if param.kind == P.POSITIONAL_ONLY:
@ -434,14 +440,22 @@ def get_http_request() -> Request:
return request
def get_http_headers(include_all: bool = False) -> dict[str, str]:
def get_http_headers(
include_all: bool = False,
include: set[str] | None = None,
) -> dict[str, str]:
"""Extract headers from the current HTTP request if available.
Never raises an exception, even if there is no active HTTP request (in which case
an empty dict is returned).
By default, strips problematic headers like `content-length` that cause issues
if forwarded to downstream clients. If `include_all` is True, all headers are returned.
By default, strips problematic headers like `content-length` and `authorization`
that cause issues if forwarded to downstream services. If `include_all` is True,
all headers are returned.
The `include` parameter allows specific headers to be included even if they would
normally be excluded. This is useful for proxy transports that need to forward
authorization headers to upstream MCP servers.
"""
if include_all:
exclude_headers: set[str] = set()
@ -457,6 +471,7 @@ def get_http_headers(include_all: bool = False) -> dict[str, str]:
"keep-alive",
"expect",
"accept",
"authorization",
# Proxy-related headers
"proxy-authenticate",
"proxy-authorization",
@ -464,6 +479,8 @@ def get_http_headers(include_all: bool = False) -> dict[str, str]:
# MCP-related headers
"mcp-session-id",
}
if include:
exclude_headers -= {h.lower() for h in include}
# (just in case)
if not all(h.lower() == h for h in exclude_headers):
raise ValueError("Excluded headers must be lowercase")
@ -781,6 +798,36 @@ async def _restore_task_access_token(
return None
async def _restore_task_origin_request_id(session_id: str, task_id: str) -> str | None:
"""Restore the origin request ID snapshot for a background task.
Returns None if no request ID was captured at submission time.
"""
docket = _current_docket.get()
if docket is None:
return None
request_id_key = docket.key(
f"fastmcp:task:{session_id}:{task_id}:origin_request_id"
)
try:
async with docket.redis() as redis:
request_id_data = await redis.get(request_id_key)
if request_id_data is None:
return None
if isinstance(request_id_data, bytes):
return request_id_data.decode()
return str(request_id_data)
except Exception:
_logger.warning(
"Failed to restore origin request ID for task %s:%s",
session_id,
task_id,
exc_info=True,
)
return None
class _CurrentContext(Dependency): # type: ignore[misc]
"""Async context manager for Context dependency.
@ -807,11 +854,15 @@ class _CurrentContext(Dependency): # type: ignore[misc]
session = get_task_session(task_info.session_id)
# Get server from ContextVar
server = get_server()
origin_request_id = await _restore_task_origin_request_id(
task_info.session_id, task_info.task_id
)
# Create task-aware Context
self._context = Context(
fastmcp=server,
session=session,
task_id=task_info.task_id,
origin_request_id=origin_request_id,
)
# Enter the context to set up ContextVars
await self._context.__aenter__()
@ -842,6 +893,34 @@ class _CurrentContext(Dependency): # type: ignore[misc]
self._context = None
class _OptionalCurrentContext(Dependency): # type: ignore[misc]
"""Context dependency that degrades to None when no context is active.
This is implemented as a wrapper (composition), not a subclass of
`_CurrentContext`, to avoid overriding `__aenter__` with an incompatible
return type.
"""
_inner: _CurrentContext | None = None
async def __aenter__(self) -> Context | None:
inner = _CurrentContext()
try:
context = await inner.__aenter__()
except RuntimeError as exc:
if "No active context found" in str(exc):
return None
raise
self._inner = inner
return context
async def __aexit__(self, *args: object) -> None:
if self._inner is None:
return
await self._inner.__aexit__(*args)
self._inner = None
def CurrentContext() -> Context:
"""Get the current FastMCP Context instance.
@ -867,6 +946,11 @@ def CurrentContext() -> Context:
return cast("Context", _CurrentContext())
def OptionalCurrentContext() -> Context | None:
"""Get the current FastMCP Context, or None when no context is active."""
return cast("Context | None", _OptionalCurrentContext())
class _CurrentDocket(Dependency): # type: ignore[misc]
"""Async context manager for Docket dependency."""
@ -1037,7 +1121,7 @@ class _CurrentHeaders(Dependency): # type: ignore[misc]
"""Async context manager for HTTP Headers dependency."""
async def __aenter__(self) -> dict[str, str]:
return get_http_headers()
return get_http_headers(include={"authorization"})
async def __aexit__(self, *args: object) -> None:
pass
@ -1046,9 +1130,10 @@ class _CurrentHeaders(Dependency): # type: ignore[misc]
def CurrentHeaders() -> dict[str, str]:
"""Get the current HTTP request headers.
This dependency provides access to the HTTP headers for the current request.
Returns an empty dictionary when no HTTP request is available, making it
safe to use in code that might run over any transport.
This dependency provides access to the HTTP headers for the current request,
including the authorization header. Returns an empty dictionary when no HTTP
request is available, making it safe to use in code that might run over any
transport.
Returns:
A dependency that resolves to a dictionary of header name -> value

View file

@ -172,7 +172,9 @@ class OpenAPITool(Tool):
mcp_headers = get_http_headers()
if mcp_headers:
request.headers.update(mcp_headers)
for key, value in mcp_headers.items():
if key not in request.headers:
request.headers[key] = value
except Exception as e:
raise ValueError(
f"Error building request for {self._route.method.upper()} "

View file

@ -98,7 +98,13 @@ async def submit_to_docket(
poll_interval_key = docket.key(
f"fastmcp:task:{session_id}:{server_task_id}:poll_interval"
)
origin_request_id_key = docket.key(
f"fastmcp:task:{session_id}:{server_task_id}:origin_request_id"
)
poll_interval_ms = int(component.task_config.poll_interval.total_seconds() * 1000)
origin_request_id = (
str(ctx.request_context.request_id) if ctx.request_context is not None else None
)
# Snapshot the current access token (if any) for background task access (#3095)
access_token = get_access_token()
@ -110,6 +116,8 @@ async def submit_to_docket(
await redis.set(task_meta_key, task_key, ex=ttl_seconds)
await redis.set(created_at_key, created_at.isoformat(), ex=ttl_seconds)
await redis.set(poll_interval_key, str(poll_interval_ms), ex=ttl_seconds)
if origin_request_id is not None:
await redis.set(origin_request_id_key, origin_request_id, ex=ttl_seconds)
if access_token is not None:
await redis.set(
access_token_key, access_token.model_dump_json(), ex=ttl_seconds

View file

@ -14,6 +14,7 @@ from mcp import ServerSession
from fastmcp import FastMCP
from fastmcp.client import Client
from fastmcp.client.elicitation import ElicitResult
from fastmcp.dependencies import CurrentDocket
from fastmcp.server.auth import AccessToken
from fastmcp.server.context import Context
from fastmcp.server.dependencies import get_access_token
@ -229,6 +230,43 @@ class TestBackgroundTaskIntegration:
assert captured["session_id"] is not None
assert captured["is_background"] is True
async def test_origin_request_id_round_trips_through_background_task(self):
"""E2E: origin_request_id captured at submit time is restored in worker.
We validate this by comparing ctx.origin_request_id with the value
stored in Docket's Redis for this task.
"""
mcp = FastMCP("origin-request-id-roundtrip")
@mcp.tool(task=True)
async def check_origin_request_id(ctx: Context, docket=CurrentDocket()) -> str:
assert ctx.is_background_task is True
assert ctx.request_context is None
assert ctx.task_id is not None
origin = ctx.origin_request_id
assert origin is not None
assert isinstance(origin, str)
assert origin != ""
key = docket.key(
f"fastmcp:task:{ctx.session_id}:{ctx.task_id}:origin_request_id"
)
async with docket.redis() as redis:
raw = await redis.get(key)
assert raw is not None
if isinstance(raw, bytes):
raw = raw.decode()
assert str(raw) == origin
return "ok"
async with Client(mcp) as client:
task = await client.call_tool("check_origin_request_id", {}, task=True)
result = await task.result()
assert result.data == "ok"
async def test_elicit_accept_flow(self):
"""E2E: tool elicits input, client accepts via elicitation_handler."""
mcp = FastMCP("elicit-accept-test")

View file

@ -402,9 +402,11 @@ async def media_server(tmp_path):
),
(
"return_image_data",
lambda r: len(r.content) == 1
and r.content[0].type == "image"
and r.content[0].mimeType == "image/png",
lambda r: (
len(r.content) == 1
and r.content[0].type == "image"
and r.content[0].mimeType == "image/png"
),
),
(
"return_audio",
@ -615,15 +617,19 @@ async def mcp_content_server(tmp_path):
[
(
"return_text_content",
lambda r: len(r.content) == 1
and r.content[0].type == "text"
and r.content[0].text == "Direct text content",
lambda r: (
len(r.content) == 1
and r.content[0].type == "text"
and r.content[0].text == "Direct text content"
),
),
(
"return_image_content",
lambda r: len(r.content) == 1
and r.content[0].type == "image"
and r.content[0].mimeType == "image/png",
lambda r: (
len(r.content) == 1
and r.content[0].type == "image"
and r.content[0].mimeType == "image/png"
),
),
(
"return_embedded_resource",
@ -631,9 +637,11 @@ async def mcp_content_server(tmp_path):
),
(
"return_resource_link",
lambda r: len(r.content) == 1
and r.content[0].type == "resource_link"
and str(r.content[0].uri) == "test://linked",
lambda r: (
len(r.content) == 1
and r.content[0].type == "resource_link"
and str(r.content[0].uri) == "test://linked"
),
),
],
)

View file

@ -754,6 +754,42 @@ async def test_validation_error_propagates_from_dependency(mcp: FastMCP):
class TestTransformContextAnnotations:
"""Tests for the transform_context_annotations function."""
async def test_optional_context_degrades_to_none_without_active_context(self):
"""Optional Context should resolve to None when no context is active."""
import inspect
from fastmcp.server.dependencies import transform_context_annotations
async def fn_with_optional_ctx(name: str, ctx: Context | None = None) -> str:
return name
transform_context_annotations(fn_with_optional_ctx)
sig = inspect.signature(fn_with_optional_ctx)
ctx_dependency = sig.parameters["ctx"].default
resolved_ctx = await ctx_dependency.__aenter__()
try:
assert resolved_ctx is None
finally:
await ctx_dependency.__aexit__(None, None, None)
async def test_optional_context_still_injected_in_foreground_requests(
self, mcp: FastMCP
):
"""Optional Context should still be injected for normal MCP requests."""
@mcp.tool()
async def tool_with_optional_context(
name: str, ctx: Context | None = None
) -> str:
if ctx is None:
return f"missing:{name}"
return f"present:{ctx.session_id}:{name}"
async with Client(mcp) as client:
result = await client.call_tool("tool_with_optional_context", {"name": "x"})
assert result.content[0].text.startswith("present:")
async def test_basic_context_transformation(self, mcp: FastMCP):
"""Test basic Context type annotation is transformed."""