mirror of
https://github.com/PrefectHQ/fastmcp.git
synced 2026-08-09 07:09:11 +02:00
Gate task registry shim to handshake-era protocol versions
This commit is contained in:
parent
956abd9e5b
commit
8a58b1c4e0
2 changed files with 92 additions and 13 deletions
|
|
@ -1,12 +1,16 @@
|
|||
"""Temporary in-place patches for gaps in the pinned MCP SDK.
|
||||
|
||||
## SEP-1686 task methods missing from the SDK method registries
|
||||
## SEP-1686 task methods missing from the handshake-era method registries
|
||||
|
||||
`mcp==2.0.0b1` ships the task types (`CreateTaskResult`, `GetTaskResult`,
|
||||
`GetTaskPayloadResult`, `ListTasksResult`, `CancelTaskResult`) but its
|
||||
`mcp_types.methods` registries have no `tasks/*` rows, and every `tools/call`
|
||||
result row is a plain `CallToolResult` (2025 eras) or `CallToolResult |
|
||||
InputRequiredResult` (2026) with no `CreateTaskResult` arm.
|
||||
This shim compensates for a genuine gap in the SDK's *handshake-era*
|
||||
(2025-11-25 and earlier) task registry. In the 2025-11-25 SEP-1686 model, tasks
|
||||
are a first-class part of the core protocol: `CallToolRequestParams` carries a
|
||||
`task: TaskMetadata` field and a task-augmented `tools/call` returns a
|
||||
`CreateTaskResult`. `mcp==2.0.0b1` ships those task types (`CreateTaskResult`,
|
||||
`GetTaskResult`, `GetTaskPayloadResult`, `ListTasksResult`, `CancelTaskResult`)
|
||||
and the `task` request field, but its `mcp_types.methods` registries were never
|
||||
wired for them: there are no `tasks/*` rows, and the handshake-era `tools/call`
|
||||
result rows are a plain `CallToolResult` with no `CreateTaskResult` arm.
|
||||
|
||||
The lowlevel server runner (`mcp.server.runner`) serializes a handler's result
|
||||
through `serialize_server_result(method, version, ...)` for any method in
|
||||
|
|
@ -19,12 +23,28 @@ their handler results already bypass serialization and reach the wire
|
|||
unvalidated; we still register their result rows here for symmetry and so the
|
||||
maps are consistent if a future SDK adds them to the spec method set.
|
||||
|
||||
## Scope: handshake-era versions only
|
||||
|
||||
The widening + `tasks/*` registration is gated to
|
||||
`HANDSHAKE_PROTOCOL_VERSIONS` (2025-11-25 and earlier) because those are the
|
||||
versions where the 2025 SEP-1686 task model actually applies and where the
|
||||
SDK's registry has the genuine gap we compensate for.
|
||||
|
||||
The 2026-07-28 protocol is intentionally NOT patched here. Tasks left the core
|
||||
protocol in 2026-07-28 and became the separate `io.modelcontextprotocol/tasks`
|
||||
extension; `CreateTaskResult` and the `task` field on `CallToolRequestParams`
|
||||
do not exist in that schema (a task-augmented `tools/call` was replaced by the
|
||||
mutually-recursive `CallToolResult | InputRequiredResult` result). Injecting the
|
||||
2025-era `CreateTaskResult` into the 2026 `tools/call` union would assert the
|
||||
wrong task model onto that protocol, so we leave its rows untouched.
|
||||
|
||||
This module widens the registries IN PLACE (the maps are `MappingProxyType`
|
||||
views over private dicts, so we reach the backing dict via `gc.get_referents`
|
||||
and mutate it, which the already-bound default-argument references in
|
||||
`mcp_types.methods` observe). `install()` is idempotent.
|
||||
|
||||
# TODO(sdk-upstream): remove when mcp>=2.0.0bX includes SEP-1686 in method registries
|
||||
# TODO(sdk-upstream): remove when mcp>=2.0.0bX wires SEP-1686 into the
|
||||
# handshake-era method registries.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
|
@ -34,6 +54,7 @@ from types import MappingProxyType, UnionType
|
|||
|
||||
import mcp_types
|
||||
from mcp_types import methods as _methods
|
||||
from mcp_types.version import HANDSHAKE_PROTOCOL_VERSIONS
|
||||
|
||||
# Result type for each task method, keyed by the client request method name.
|
||||
_TASK_RESULT_TYPES: dict[str, type] = {
|
||||
|
|
@ -78,8 +99,13 @@ def install() -> None:
|
|||
|
||||
server_results = _backing_dict(_methods.SERVER_RESULTS)
|
||||
|
||||
# Gate to handshake-era versions only: the 2025 SEP-1686 task model applies
|
||||
# there, and 2026-07-28 tasks are the separate io.modelcontextprotocol/tasks
|
||||
# extension (see module docstring) — its rows must stay untouched.
|
||||
versions_with_tools_call = {
|
||||
version for (method, version) in server_results if method == "tools/call"
|
||||
version
|
||||
for (method, version) in server_results
|
||||
if method == "tools/call" and version in HANDSHAKE_PROTOCOL_VERSIONS
|
||||
}
|
||||
|
||||
for version in versions_with_tools_call:
|
||||
|
|
|
|||
|
|
@ -27,6 +27,11 @@ from mcp.client import Client as SDKClient
|
|||
from mcp.client.session import ClientRequestContext
|
||||
from mcp.server import Server as LowLevelServer
|
||||
from mcp.shared.exceptions import MCPError
|
||||
from mcp_types import methods
|
||||
from mcp_types.version import (
|
||||
HANDSHAKE_PROTOCOL_VERSIONS,
|
||||
MODERN_PROTOCOL_VERSIONS,
|
||||
)
|
||||
from pydantic import FileUrl
|
||||
|
||||
from fastmcp import Client as FastMCPClient
|
||||
|
|
@ -403,10 +408,12 @@ async def test_task_submission_and_get_on_legacy_latest(task_server):
|
|||
"The v2 SDK high-level client (mcp.client.Client) and ClientSession "
|
||||
"expose no `task=` parameter on call_tool, so a task-augmented "
|
||||
"tools/call cannot be submitted through it at any era; a hand-built "
|
||||
"raw CallToolRequest does not drive FastMCP's task path either. The "
|
||||
"_sdk_patches shim widens the 2026-07-28 tools/call result union "
|
||||
"(sdk-feedback.md #1), but there is no client-side surface to reach it. "
|
||||
"Remove once the SDK client supports task submission."
|
||||
"raw CallToolRequest does not drive FastMCP's task path either. On "
|
||||
"2026-07-28 tasks moved to the io.modelcontextprotocol/tasks extension "
|
||||
"and CreateTaskResult is not part of the tools/call union, so the "
|
||||
"_sdk_patches shim intentionally does not widen the modern row "
|
||||
"(sdk-feedback.md #1). Remove once the SDK client supports task "
|
||||
"submission."
|
||||
),
|
||||
)
|
||||
async def test_task_submission_on_modern(task_server):
|
||||
|
|
@ -422,6 +429,52 @@ async def test_task_submission_on_modern(task_server):
|
|||
assert isinstance(result, types.CreateTaskResult)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 4b. _sdk_patches registry gating: the SEP-1686 task shim widens ONLY the
|
||||
# handshake-era rows and leaves the 2026-07-28 (extension-era) rows untouched.
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_task_shim_widens_handshake_tools_call_rows():
|
||||
"""Every handshake-era tools/call row gains a CreateTaskResult arm."""
|
||||
from fastmcp._sdk_patches import get_union_arms
|
||||
|
||||
for version in HANDSHAKE_PROTOCOL_VERSIONS:
|
||||
row = methods.SERVER_RESULTS[("tools/call", version)]
|
||||
assert types.CreateTaskResult in get_union_arms(row), version
|
||||
|
||||
|
||||
def test_task_shim_does_not_touch_modern_tools_call_row():
|
||||
"""The 2026-07-28 tools/call row stays the unpatched MRTR union: tasks are
|
||||
the io.modelcontextprotocol/tasks extension there, so CreateTaskResult must
|
||||
not be injected."""
|
||||
from fastmcp._sdk_patches import get_union_arms
|
||||
|
||||
row = methods.SERVER_RESULTS[("tools/call", "2026-07-28")]
|
||||
arms = get_union_arms(row)
|
||||
assert types.CreateTaskResult not in arms
|
||||
# Unchanged from the SDK default: the 2026 mutually-recursive tool result
|
||||
# (CallToolResult | InputRequiredResult), keyed by the version-specific types.
|
||||
arm_names = {arm.__name__ for arm in arms}
|
||||
assert arm_names == {"CallToolResult", "InputRequiredResult"}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"task_method",
|
||||
["tasks/get", "tasks/result", "tasks/list", "tasks/cancel"],
|
||||
)
|
||||
def test_task_shim_registers_tasks_rows_only_for_handshake_eras(task_method):
|
||||
"""tasks/* result rows exist for handshake-era versions and are absent for
|
||||
the modern (extension) era."""
|
||||
for version in HANDSHAKE_PROTOCOL_VERSIONS:
|
||||
assert (task_method, version) in methods.SERVER_RESULTS, (task_method, version)
|
||||
for version in MODERN_PROTOCOL_VERSIONS:
|
||||
assert (task_method, version) not in methods.SERVER_RESULTS, (
|
||||
task_method,
|
||||
version,
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 5. Sessionless safety: session-id-keyed paths must not crash on 2026 in-memory
|
||||
# ---------------------------------------------------------------------------
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue