|
15 | 15 |
|
16 | 16 | import click |
17 | 17 | from mcp.server import ServerRequestContext |
| 18 | +from mcp.server.extension import require_client_extension |
18 | 19 | from mcp.server.mcpserver import Context, MCPServer |
19 | 20 | from mcp.server.mcpserver.prompts.base import UserMessage |
20 | 21 | 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 |
21 | 24 | from mcp.shared.exceptions import MCPError |
22 | 25 | from mcp_types import ( |
23 | 26 | AudioContent, |
|
47 | 50 | TextResourceContents, |
48 | 51 | UnsubscribeRequestParams, |
49 | 52 | ) |
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 |
51 | 54 | from pydantic import BaseModel, Field |
52 | 55 |
|
53 | 56 | logger = logging.getLogger(__name__) |
@@ -100,8 +103,23 @@ async def replay_events_after(self, last_event_id: EventId, send_callback: Event |
100 | 103 | # Create event store for SSE resumability (SEP-1699) |
101 | 104 | event_store = InMemoryEventStore() |
102 | 105 |
|
| 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 | + |
103 | 120 | mcp = MCPServer( |
104 | 121 | name="mcp-conformance-test-server", |
| 122 | + extensions=[Tasks(augment=lambda params: params.name in TASK_AUGMENTED_TOOLS)], |
105 | 123 | ) |
106 | 124 |
|
107 | 125 |
|
@@ -550,6 +568,86 @@ async def test_input_required_result_capabilities(ctx: Context) -> InputRequired |
550 | 568 | return InputRequiredResult(input_requests=requests, request_state="capability-gated") |
551 | 569 |
|
552 | 570 |
|
| 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 | + |
553 | 651 | # SEP-1613 / SEP-2106 JSON Schema 2020-12 fixture: a tool whose inputSchema carries |
554 | 652 | # the full set of 2020-12 keywords the conformance scenario asserts on. |
555 | 653 |
|
|
0 commit comments