Skip to content

Commit 9bdd698

Browse files
committed
Register the Tasks extension on the conformance fixture server
The everything-server now opts into Tasks with an allowlist augment predicate, so every pre-existing tool stays synchronous while the new task fixture tools (slow_compute, failing_job, protocol_error_job, confirm_delete, multi_input, test_tool_with_task) exercise augmentation, failed-task recording, the required-capability error, and the MRTR-then-task composition. Eight of the nine baselined tasks-* expected failures now pass and are removed; tasks-mrtr-input stays baselined with an accurate rationale (tasks are born terminal -- the in-task input_required/tasks/update resume loop is a documented follow-up). Verified locally against the pinned harness: all four server legs green with zero unexpected failures and zero stale baselines.
1 parent c436e6f commit 9bdd698

2 files changed

Lines changed: 117 additions & 15 deletions

File tree

.github/actions/conformance/expected-failures.yml

Lines changed: 18 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -13,22 +13,26 @@
1313
client: []
1414

1515
server:
16-
# SEP-2663 (io.modelcontextprotocol/tasks): the SDK does not implement the
17-
# tasks extension yet. These extension-tagged scenarios are selected only by
18-
# the bare `--suite all` leg — extension scenarios never match a
19-
# --spec-version filter and the active/draft suites exclude them — so these
20-
# entries are inert for the other legs that read this file.
16+
# SEP-2663 (io.modelcontextprotocol/tasks): the SDK implements the extension
17+
# with tasks born terminal — the tool runs to completion inside the
18+
# interceptor, so there is no background execution and no in-task
19+
# `input_required` parking (see the deferred follow-ups in
20+
# src/mcp/server/tasks.py). tasks-mrtr-input needs exactly that surface: a
21+
# task that parks with `status:"input_required"` + a non-empty
22+
# `inputRequests` map on tasks/get, resumes via the tasks/update
23+
# inputResponses loop, and supports partial fulfillment across two pending
24+
# keys. The everything-server's `confirm_delete`/`multi_input` fixtures
25+
# therefore resolve their elicitation on the original `tools/call` (SEP-2322
26+
# MRTR) and never mint a task, so all three checks fail with "did not create
27+
# a task". Remove this entry when background task execution and the in-task
28+
# input_required/tasks/update resume loop land. Extension-tagged scenarios
29+
# are selected only by the bare `--suite all` leg — they never match a
30+
# --spec-version filter and the active/draft suites exclude them — so this
31+
# entry is inert for the other legs that read this file.
2132
#
2233
# `tasks-status-notifications` is intentionally NOT listed: the harness
2334
# skips it unconditionally (pending its rewrite against subscriptions/
2435
# listen), and a baseline entry for a scenario with no failing checks is
25-
# flagged stale.
26-
- tasks-lifecycle
27-
- tasks-capability-negotiation
28-
- tasks-wire-fields
29-
- tasks-request-state-removal
36+
# flagged stale. The other eight tasks-* scenarios pass against the
37+
# everything-server's Tasks registration and are not listed either.
3038
- tasks-mrtr-input
31-
- tasks-request-headers
32-
- tasks-dispatch-and-envelope
33-
- tasks-required-task-error
34-
- tasks-mrtr-composition

examples/servers/everything-server/mcp_everything_server/server.py

Lines changed: 99 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,12 @@
1212

1313
import click
1414
from mcp.server import ServerRequestContext
15+
from mcp.server.extension import require_client_extension
1516
from mcp.server.mcpserver import Context, MCPServer, RequestStateSecurity
1617
from mcp.server.mcpserver.prompts.base import Prompt, UserMessage
1718
from mcp.server.streamable_http import EventCallback, EventMessage, EventStore
19+
from mcp.server.tasks import EXTENSION_ID as TASKS_EXTENSION_ID
20+
from mcp.server.tasks import Tasks
1821
from mcp.shared.exceptions import MCPError
1922
from mcp_types import (
2023
AudioContent,
@@ -44,7 +47,7 @@
4447
TextResourceContents,
4548
UnsubscribeRequestParams,
4649
)
47-
from mcp_types.jsonrpc import MISSING_REQUIRED_CLIENT_CAPABILITY
50+
from mcp_types.jsonrpc import INTERNAL_ERROR, MISSING_REQUIRED_CLIENT_CAPABILITY
4851
from pydantic import BaseModel, Field
4952

5053
logger = logging.getLogger(__name__)
@@ -100,9 +103,24 @@ async def replay_events_after(self, last_event_id: EventId, send_callback: Event
100103
# Fixed fixture key (RequestStateSecurity requires at least 32 bytes); a real deployment would load a shared secret.
101104
_REQUEST_STATE_KEY = b"everything-server-fixture-request-state-key"
102105

106+
# SEP-2663 Tasks extension: only these fixture tools are task-augmented, so every
107+
# other tool keeps its synchronous behaviour even for clients that declare the
108+
# extension (the tasks conformance scenarios assert e.g. `greet` stays sync).
109+
TASK_AUGMENTED_TOOLS = frozenset(
110+
{
111+
"slow_compute",
112+
"failing_job",
113+
"protocol_error_job",
114+
"confirm_delete",
115+
"multi_input",
116+
"test_tool_with_task",
117+
}
118+
)
119+
103120
mcp = MCPServer(
104121
name="mcp-conformance-test-server",
105122
request_state_security=RequestStateSecurity(keys=[_REQUEST_STATE_KEY]),
123+
extensions=[Tasks(augment=lambda params: params.name in TASK_AUGMENTED_TOOLS)],
106124
)
107125

108126

@@ -534,6 +552,86 @@ async def test_input_required_result_capabilities(ctx: Context) -> InputRequired
534552
return InputRequiredResult(input_requests=requests, request_state="capability-gated")
535553

536554

555+
# SEP-2663 Tasks extension fixtures (io.modelcontextprotocol/tasks). The server
556+
# decides augmentation per call via the TASK_AUGMENTED_TOOLS allowlist above;
557+
# `greet` stays deliberately outside it as the synchronous contrast the tasks
558+
# scenarios assert on.
559+
560+
CONFIRM_SCHEMA = {"type": "object", "properties": {"confirm": {"type": "boolean"}}, "required": ["confirm"]}
561+
562+
563+
@mcp.tool()
564+
def greet(name: str) -> str:
565+
"""Sync-only greeting tool; never task-augmented"""
566+
return f"Hello, {name}!"
567+
568+
569+
@mcp.tool()
570+
def slow_compute(seconds: float, label: str = "") -> str:
571+
"""Task-supporting compute fixture (SEP-2663).
572+
573+
Conformance passes durations up to 60s and immediately polls or cancels, so
574+
the fixture records the requested duration instead of sleeping — the
575+
born-terminal task is then observable well inside the scenario timeouts.
576+
"""
577+
return f"slow_compute({label or 'unlabelled'}) finished after {seconds}s"
578+
579+
580+
@mcp.tool()
581+
async def failing_job(ctx: Context) -> str:
582+
"""Task-required fixture: rejects non-declaring clients, then reports a tool error (SEP-2663)"""
583+
require_client_extension(ctx.request_context, TASKS_EXTENSION_ID)
584+
raise RuntimeError("failing_job always reports a tool execution error")
585+
586+
587+
@mcp.tool()
588+
def protocol_error_job() -> str:
589+
"""Task fixture that fails at the protocol level, recording a `failed` task (SEP-2663)"""
590+
raise MCPError(code=INTERNAL_ERROR, message="protocol_error_job failed at the protocol level")
591+
592+
593+
@mcp.tool()
594+
async def confirm_delete(filename: str, ctx: Context) -> str | InputRequiredResult:
595+
"""Task fixture gathering a deletion confirmation via elicitation (SEP-2322 + SEP-2663)"""
596+
responses = ctx.input_responses
597+
if responses and "confirm" in responses:
598+
answer = responses["confirm"]
599+
accepted = isinstance(answer, ElicitResult) and answer.action == "accept"
600+
return f"Deleted {filename}" if accepted else f"Kept {filename}"
601+
return InputRequiredResult(
602+
input_requests={
603+
"confirm": ElicitRequest(
604+
params=ElicitRequestFormParams(message=f"Really delete {filename}?", requested_schema=CONFIRM_SCHEMA)
605+
)
606+
}
607+
)
608+
609+
610+
@mcp.tool()
611+
async def multi_input(ctx: Context) -> str | InputRequiredResult:
612+
"""Task fixture fanning out two simultaneous elicitations (SEP-2663 partial fulfillment probe)"""
613+
responses = ctx.input_responses
614+
if responses and {"first", "second"} <= responses.keys():
615+
return "multi_input received both responses"
616+
return InputRequiredResult(
617+
input_requests={
618+
"first": _name_elicitation("First input?"),
619+
"second": _name_elicitation("Second input?"),
620+
}
621+
)
622+
623+
624+
@mcp.tool()
625+
async def test_tool_with_task(ctx: Context) -> str | InputRequiredResult:
626+
"""SEP-2663 MRTR-to-task composition fixture: gathers a name, then the final round becomes a task"""
627+
responses = ctx.input_responses
628+
if responses and "user_name" in responses:
629+
answer = responses["user_name"]
630+
name = answer.content.get("name", "stranger") if isinstance(answer, ElicitResult) and answer.content else "?"
631+
return f"Hello, {name}! The gathered-name task is complete."
632+
return InputRequiredResult(input_requests={"user_name": _name_elicitation()})
633+
634+
537635
# SEP-1613 / SEP-2106 JSON Schema 2020-12 fixture: a tool whose inputSchema carries
538636
# the full set of 2020-12 keywords the conformance scenario asserts on.
539637

0 commit comments

Comments
 (0)