Skip to content

Commit 4ecdfb5

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 9b52ee6 commit 4ecdfb5

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

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

534552

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

0 commit comments

Comments
 (0)