Skip to content

Commit 743bc4e

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 a56f75c commit 743bc4e

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
@@ -29,22 +29,26 @@ client:
2929
- auth/wif-jwt-bearer:wif-grant-type
3030

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

1313
import click
1414
from mcp.server import ServerRequestContext
15-
from mcp.server.mcpserver import Context, MCPServer, RequestStateSecurity
15+
from mcp.server.mcpserver import Context, MCPServer, RequestStateSecurity, require_client_extension
1616
from mcp.server.mcpserver.prompts.base import Prompt, UserMessage
1717
from mcp.server.streamable_http import EventCallback, EventMessage, EventStore
18+
from mcp.server.tasks import EXTENSION_ID as TASKS_EXTENSION_ID
19+
from mcp.server.tasks import Tasks
1820
from mcp.shared.exceptions import MCPError
1921
from mcp.types import (
22+
INTERNAL_ERROR,
2023
MISSING_REQUIRED_CLIENT_CAPABILITY,
2124
AudioContent,
2225
Completion,
@@ -100,10 +103,25 @@ 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
version="0.1.0",
106123
request_state_security=RequestStateSecurity(keys=[_REQUEST_STATE_KEY]),
124+
extensions=[Tasks(augment=lambda params: params.name in TASK_AUGMENTED_TOOLS)],
107125
)
108126

109127

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

537555

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

0 commit comments

Comments
 (0)