Skip to content

Commit b25a533

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 b598b22 commit b25a533

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
@@ -30,22 +30,26 @@ server:
3030
# validates Mcp-Param headers against body params. Read by the draft leg and
3131
# the bare `--suite all` leg; the 2026-07-28 leg carries its own entry.
3232
- http-custom-header-server-validation
33-
# SEP-2663 (io.modelcontextprotocol/tasks): the SDK does not implement the
34-
# tasks extension yet. These extension-tagged scenarios are selected only by
35-
# the bare `--suite all` leg — extension scenarios never match a
36-
# --spec-version filter and the active/draft suites exclude them — so these
37-
# entries are inert for the other legs that read this file.
33+
# SEP-2663 (io.modelcontextprotocol/tasks): the SDK implements the extension
34+
# with tasks born terminal — the tool runs to completion inside the
35+
# interceptor, so there is no background execution and no in-task
36+
# `input_required` parking (see the deferred follow-ups in
37+
# src/mcp/server/tasks.py). tasks-mrtr-input needs exactly that surface: a
38+
# task that parks with `status:"input_required"` + a non-empty
39+
# `inputRequests` map on tasks/get, resumes via the tasks/update
40+
# inputResponses loop, and supports partial fulfillment across two pending
41+
# keys. The everything-server's `confirm_delete`/`multi_input` fixtures
42+
# therefore resolve their elicitation on the original `tools/call` (SEP-2322
43+
# MRTR) and never mint a task, so all three checks fail with "did not create
44+
# a task". Remove this entry when background task execution and the in-task
45+
# input_required/tasks/update resume loop land. Extension-tagged scenarios
46+
# are selected only by the bare `--suite all` leg — they never match a
47+
# --spec-version filter and the active/draft suites exclude them — so this
48+
# entry is inert for the other legs that read this file.
3849
#
3950
# `tasks-status-notifications` is intentionally NOT listed: the harness
4051
# skips it unconditionally (pending its rewrite against subscriptions/
4152
# listen), and a baseline entry for a scenario with no failing checks is
42-
# flagged stale.
43-
- tasks-lifecycle
44-
- tasks-capability-negotiation
45-
- tasks-wire-fields
46-
- tasks-request-state-removal
53+
# flagged stale. The other eight tasks-* scenarios pass against the
54+
# everything-server's Tasks registration and are not listed either.
4755
- tasks-mrtr-input
48-
- tasks-request-headers
49-
- tasks-dispatch-and-envelope
50-
- tasks-required-task-error
51-
- 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
@@ -15,9 +15,12 @@
1515

1616
import click
1717
from mcp.server import ServerRequestContext
18+
from mcp.server.extension import require_client_extension
1819
from mcp.server.mcpserver import Context, MCPServer
1920
from mcp.server.mcpserver.prompts.base import UserMessage
2021
from mcp.server.streamable_http import EventCallback, EventMessage, EventStore
22+
from mcp.server.tasks import EXTENSION_ID as TASKS_EXTENSION_ID
23+
from mcp.server.tasks import Tasks
2124
from mcp.shared.exceptions import MCPError
2225
from mcp_types import (
2326
AudioContent,
@@ -47,7 +50,7 @@
4750
TextResourceContents,
4851
UnsubscribeRequestParams,
4952
)
50-
from mcp_types.jsonrpc import INVALID_PARAMS, MISSING_REQUIRED_CLIENT_CAPABILITY
53+
from mcp_types.jsonrpc import INTERNAL_ERROR, INVALID_PARAMS, MISSING_REQUIRED_CLIENT_CAPABILITY
5154
from pydantic import BaseModel, Field
5255

5356
logger = logging.getLogger(__name__)
@@ -100,8 +103,23 @@ async def replay_events_after(self, last_event_id: EventId, send_callback: Event
100103
# Create event store for SSE resumability (SEP-1699)
101104
event_store = InMemoryEventStore()
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",
122+
extensions=[Tasks(augment=lambda params: params.name in TASK_AUGMENTED_TOOLS)],
105123
)
106124

107125

@@ -550,6 +568,86 @@ async def test_input_required_result_capabilities(ctx: Context) -> InputRequired
550568
return InputRequiredResult(input_requests=requests, request_state="capability-gated")
551569

552570

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

0 commit comments

Comments
 (0)