feat: Implement SEP-2663 Tasks Extension (#1020)
* feat!: implement SEP-2663 Tasks extension, removing the experimental 2025-11-25 tasks design Reshape tasks from the experimental SEP-1319/1686 core-protocol feature into the official io.modelcontextprotocol/tasks extension (SEP-2663): Model: - Re-model Task (statusMessage, ttlMs nullable, pollIntervalMs) and add DetailedTask with status-discriminated payloads (inputRequests/result/error inlined per spec) - CreateTaskResult now flattens Task with resultType: "task"; add ResultType::TASK and CallToolResponse::Task - Add tasks/update (UpdateTaskParams with MRTR InputResponses); tasks/get and tasks/cancel reworked; remove tasks/list, tasks/result - notifications/tasks now carries a full DetailedTask; removed from client notifications (SEP-2260) Capabilities: - Remove core TasksCapability et al; tasks are declared via the extensions map (enable_tasks() builders, supports_tasks() accessors) - Remove tool-level execution.taskSupport and the _meta.task / TaskMetadata / with_task() opt-in: task creation is server-directed and gated on the per-request client capability Runtime: - Replace OperationProcessor with TaskManager: durable-before-response task creation, input_required round-trips via tasks/update, cooperative cancellation, TTL expiry - Client peer helpers get_task/update_task/cancel_task - Emit Mcp-Name routing header from params.taskId for tasks/* methods (SEP-2243/2663) Macros: - Remove #[task_handler] and the tool execution() attribute Tests/examples/docs updated; schema snapshots regenerated. BREAKING CHANGE: the 2025-11-25 experimental tasks API is removed without a compatibility shim. Clients that do not declare the tasks extension always receive synchronous results. * feat: gate tasks/* methods on the client tasks-extension capability (SEP-2663) When the server advertises io.modelcontextprotocol/tasks but the client did not declare it (per-request _meta clientCapabilities, or initialize-time capabilities in session mode), tasks/get, tasks/update, and tasks/cancel now return -32021 Missing Required Client Capability with the required extension in the error data, instead of falling through to the handler's -32601 default. Servers that do not advertise the extension keep returning -32601. Also add regression tests confirming that unknown taskIds yield -32602 and that a legacy 2025-11-25 'task' param on tools/call is silently ignored. * feat: pass SEP-2663 Tasks extension conformance suite Conformance fixtures (conformance/src/bin/server.rs): - Add the required fixture tools: greet (sync-only), slow_compute, failing_job (task support: required), protocol_error_job, confirm_delete, multi_input, and test_tool_with_task (MRTR -> task escalation), all backed by TaskManager with server-directed task creation gated on the client's tasks-extension capability - Advertise io.modelcontextprotocol/tasks in server capabilities and wire get_task/update_task/cancel_task handlers - Task-required tools reject with -32021 when the client did not declare the extension SDK wire-shape fixes surfaced by the suite: - Add resultType: "complete" to GetTaskResult and introduce TaskAckResult so tasks/update and tasks/cancel acks carry the SEP-2322 discriminator (spec: every non-CreateTaskResult response on the tasks surface is resultType complete); dispatch now returns ServerResult::task_ack - CreateTaskResult gets a strict deserializer requiring resultType: "task" so it does not shadow task-shaped results in untagged unions - Client update_task/cancel_task accept both TaskAckResult and EmptyResult All 9 runnable Tasks extension server scenarios now pass (35/35 checks; tasks-status-notifications remains upstream-skipped), so the corresponding entries are removed from conformance/expected-failures-extensions.yaml. 2025-11-25 server suite (40/40), 2026-07-28 server suite (114/114), draft client suite, and extensions client suite all pass their baselines. * fix: address PR 1020 review feedback on DetailedTask schema and cooperative cancel - DetailedTask's JsonSchema now derives from the actual flattened wire shape (DetailedTaskWire: base Task + optional inputRequests/result/error) instead of approximating with the base Task schema, so generated schemas for GetTaskResult and notifications/tasks document the status-specific payload fields. Golden message schemas regenerated. - TaskManager::cancel_task no longer aborts the underlying future. It records the observable cancelled state and acks immediately (spec's eventually consistent semantics), but lets the operation keep running so it can observe is_cancel_requested() or the error from a woken request_input() and perform cleanup; any late result is discarded. New unit tests cover both the cooperative-cleanup path and waking parked input requests. * fix: make tasks/cancel truly cooperative per SEP-2663 (FEEDBACK_2) - TaskManager::cancel_task no longer forces terminal 'cancelled'. It records the cancellation intent, acks immediately, and wakes parked request_input awaits, but the operation decides its own terminal state: a post-cancel error settles as 'cancelled', while an operation that finishes its work settles as 'completed' — per the spec, 'the task may still reach a non-cancelled terminal status'. - Add TaskContext::cancelled(), a watch-based await for use with tokio::select! as the cooperative cancellation exit path. - Correct the unsupported-notification method string from 'notifications/tasks/status' to 'notifications/tasks' and document that task status notifications are not yet routable through subscriptions/listen (SubscriptionFilter has no taskIds field; upstream check still skipped). - Update conformance slow_compute fixture, task_demo example, and tests to honor cancellation via ctx.cancelled(); lifecycle conformance still 8/8 (all 9 scenarios remain green, 35/35 checks). * fix: sweep and evict expired tasks from TaskManager (FEEDBACK_3) TTL handling previously only ran from get_task and only flipped overdue non-terminal tasks to 'failed', never removing entries — an unbounded leak for long-lived servers, and it made ttl_ms: None = 'unlimited retention' meaningless since everything was retained forever. - Rename expire_overdue to sweep_expired and run it from every entry point (spawn, get_task, update_task, cancel_task). - Track terminal_at on each entry; terminal tasks are evicted after being retained for one further ttl_ms window past their terminal transition, so well-behaved pollers can observe the final state before late tasks/get calls return -32602 (spec: servers may delete expired tasks at any time, and returning task-not-found for purged tasks is compliant behavior). - ttl_ms: None entries are never evicted (spec: unlimited retention); document the retention model on TaskManager, including that there is no background sweeper. - New tests: retention-window eviction, sweep of abandoned tasks via other entry points, unlimited-TTL retention, and error-code assertions (-32602) for unknown ids across get/update/cancel. Reviewed the second feedback item (dedicated task-not-found error code) against SEP-2663 §Protocol Errors and it is incorrect: the SEP explicitly specifies -32602 (Invalid params) for invalid or nonexistent taskIds, which is what unknown_task already returns. Kept -32602; strengthened tests to assert the code. * fix: address Copilot review feedback on PR #1020 - TaskManager: drop the JoinHandle as soon as the operation settles instead of retaining it for the whole retention window, and only store it at spawn time if the task is still non-terminal (avoids keeping a completed handle that the completion path could never clear). - Use RequestContext::client_capabilities() (which applies the initialize-time fallback for session peers) instead of raw context.meta.client_capabilities() in the task_demo example, the README snippet, and the test server — the meta-only form would wrongly treat session-declared tasks clients as unsupported. All 9 Tasks extension conformance scenarios remain green (35/35 checks). * fix: enforce SEP-2663 task-result gating in tools/call dispatch A handler could return CallToolResponse::Task without checking whether the request declared the io.modelcontextprotocol/tasks extension, sending a CreateTaskResult to a client that cannot parse it. The SDK dispatch now rejects that case with -32021 Missing Required Client Capability before the response leaves the server, using RequestContext::client_capabilities() (per-request _meta with initialize-time fallback). Adds a regression test with a deliberately misbehaving handler that always materializes a task; all Tasks extension conformance scenarios remain green (35/35 checks). * fix: align TTL boundary comparisons in sweep_expired Copilot review: ttlMs: 0 never expired immediately because phase 1 used a strict 'elapsed > ttl_ms' comparison, and phase 2 retained terminal tasks at exactly the TTL boundary ('elapsed <= ttl_ms'). Treat elapsed >= ttl_ms as expired in phase 1 and evict when elapsed >= ttl_ms in phase 2, so both phases agree at the boundary and ttlMs: 0 expires/evicts on the first sweep. * fix: strict TaskAckResult deserializer and TASK_REQUIRED_TOOLS consistency - TaskAckResult carried only resultType (+ optional _meta) with a derived Deserialize, so inside the untagged ServerResult union it greedily matched any result object containing a resultType key, shadowing CustomResult and losing data (verified with a probe). Replace with a strict deserializer: deny_unknown_fields and require resultType == "complete". Regression tests cover both the non-matching shapes and the genuine ack shape. - Conformance fixtures: confirm_delete and multi_input rejected non-tasks clients inline, contradicting the TASK_REQUIRED_TOOLS doc/constant. Move them into TASK_REQUIRED_TOOLS (they park on in-task elicitation and have no synchronous fallback) so the upfront -32021 gate is the single source of truth, and drop the now-unreachable inline checks. All 9 Tasks extension conformance scenarios remain green (35/35 checks). * fix: let the operation decide its terminal state via TaskExit After tasks/cancel, any operation error was coerced to terminal 'cancelled', masking real failures (e.g. an unrelated error landing just after a late cancel request) and contradicting the documented 'operation decides its own terminal state' contract. Change TaskFuture's error type from McpError to a new TaskExit enum: - TaskExit::Cancelled — an explicit cooperative-cancellation exit; settles as terminal 'cancelled'. - TaskExit::Error(McpError) — a real failure; settles as terminal 'failed' with the error inlined, even after tasks/cancel was received. From<McpError> for TaskExit keeps '?' ergonomic in task bodies, and request_input() now returns TaskExit directly (its wake-on-cancel path yields TaskExit::Cancelled), so parked operations that propagate it with '?' settle as 'cancelled' automatically. Update the conformance fixtures, task_demo example, and tests; add a regression test asserting a post-cancel unrelated error settles as 'failed' with its error payload preserved. All 9 Tasks extension conformance scenarios remain green (35/35 checks). * fix: allow TaskExit enum to be exhaustive * fix: close spawn/shutdown race, sweep in running_task_count, clarify TTL retention docs Addresses branch-review findings: - spawn() raced with shutdown(): if shutdown drained the task map between the entry insert and the JoinHandle store, the handle was dropped and the operation kept running detached. If the entry is gone at store time, the handle is now aborted instead. - running_task_count() now runs the TTL sweep like every other entry point, so it no longer reports overdue tasks as running (matching the documented sweep-on-every-entry-point behavior). Regression test added. - Document that terminal-task retention intentionally extends one ttl_ms window past the terminal transition (observation grace period) beyond the creation-based lifetime ttlMs advertises on the wire — compliant since SEP-2663 allows deleting expired tasks at any time after the TTL.
This commit is contained in:
parent
ac9c637d04
commit
07abfdfd99
8 changed files with 2950 additions and 3236 deletions
|
|
@ -17,10 +17,8 @@ fn test_tool_list_result() {
|
|||
/// Regression tests for `#[serde(untagged)]` deserialization of `ServerResult`.
|
||||
///
|
||||
/// `ServerResult` is an untagged enum, so serde tries each variant in declaration
|
||||
/// order. `GetTaskPayloadResult` has a custom `Deserialize` impl that always fails
|
||||
/// so it is skipped, and `CustomResult(Value)` acts as the catch-all. If variant
|
||||
/// ordering changes or the custom impl is removed, these tests will catch the
|
||||
/// regression.
|
||||
/// order, with `CustomResult(Value)` acting as the catch-all. If variant ordering
|
||||
/// changes, these tests will catch the regression.
|
||||
mod untagged_server_result {
|
||||
use rmcp::model::{CallToolResult, JsonRpcResponse, ServerJsonRpcMessage, ServerResult};
|
||||
use serde_json::json;
|
||||
|
|
@ -84,7 +82,7 @@ mod untagged_server_result {
|
|||
#[test]
|
||||
fn unknown_shape_falls_through_to_custom_result() {
|
||||
// A value that doesn't match any known result type should land in
|
||||
// CustomResult, NOT GetTaskPayloadResult.
|
||||
// CustomResult.
|
||||
let result = parse_result(wrap_response(json!({
|
||||
"some_unknown_field": "some_value",
|
||||
"number": 42
|
||||
|
|
@ -96,10 +94,40 @@ mod untagged_server_result {
|
|||
}
|
||||
|
||||
#[test]
|
||||
fn arbitrary_json_value_does_not_deserialize_as_get_task_payload_result() {
|
||||
// GetTaskPayloadResult wraps a bare Value, but its custom Deserialize
|
||||
// always fails so serde skips it during untagged resolution.
|
||||
// Any JSON value must fall through to CustomResult instead.
|
||||
fn result_type_bearing_objects_do_not_match_task_ack() {
|
||||
// TaskAckResult carries only `resultType` (+ optional `_meta`), so it
|
||||
// must not greedily swallow arbitrary results that happen to include
|
||||
// a `resultType` key inside the untagged ServerResult union.
|
||||
let result = parse_result(wrap_response(json!({
|
||||
"resultType": "weird-custom",
|
||||
"payload": { "a": 1 }
|
||||
})));
|
||||
assert!(
|
||||
matches!(result, ServerResult::CustomResult(_)),
|
||||
"expected CustomResult, got {result:?}"
|
||||
);
|
||||
|
||||
let result = parse_result(wrap_response(json!({
|
||||
"resultType": "complete",
|
||||
"customField": 42
|
||||
})));
|
||||
assert!(
|
||||
matches!(result, ServerResult::CustomResult(_)),
|
||||
"expected CustomResult, got {result:?}"
|
||||
);
|
||||
|
||||
// A bare complete ack (the actual tasks/update / tasks/cancel ack
|
||||
// shape) still parses as TaskAckResult.
|
||||
let result = parse_result(wrap_response(json!({ "resultType": "complete" })));
|
||||
assert!(
|
||||
matches!(result, ServerResult::TaskAckResult(_)),
|
||||
"expected TaskAckResult, got {result:?}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn arbitrary_json_value_falls_through_to_custom_result() {
|
||||
// Any bare JSON value must fall through to CustomResult.
|
||||
for value in [json!(42), json!("hello"), json!(null), json!([1, 2, 3])] {
|
||||
let result = parse_result(wrap_response(value.clone()));
|
||||
assert!(
|
||||
|
|
|
|||
|
|
@ -126,7 +126,7 @@
|
|||
"const": "tools/call"
|
||||
},
|
||||
"CallToolRequestParams": {
|
||||
"description": "Parameters for calling a tool provided by an MCP server.\n\nContains the tool name and optional arguments needed to execute\nthe tool operation.\n\nThis implements `TaskAugmentedRequestParamsMeta` as tool calls can be\nlong-running and may benefit from task-based execution.",
|
||||
"description": "Parameters for calling a tool provided by an MCP server.\n\nContains the tool name and optional arguments needed to execute\nthe tool operation.",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"_meta": {
|
||||
|
|
@ -166,17 +166,6 @@
|
|||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"task": {
|
||||
"description": "Task metadata for async task management (SEP-1319)",
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/TaskMetadata"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
|
|
@ -304,16 +293,6 @@
|
|||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"tasks": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/TasksCapability"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
@ -657,18 +636,6 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"ElicitationTaskCapability": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"create": {
|
||||
"type": [
|
||||
"object",
|
||||
"null"
|
||||
],
|
||||
"additionalProperties": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"EmbeddedResource": {
|
||||
"description": "Embedded resource content (spec `EmbeddedResource`).",
|
||||
"type": "object",
|
||||
|
|
@ -815,34 +782,6 @@
|
|||
"type": "object",
|
||||
"properties": {
|
||||
"_meta": {
|
||||
"description": "Protocol-level metadata for this request (SEP-1319)",
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/RequestMetaObject"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"taskId": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"taskId"
|
||||
]
|
||||
},
|
||||
"GetTaskPayloadMethod": {
|
||||
"type": "string",
|
||||
"format": "const",
|
||||
"const": "tasks/result"
|
||||
},
|
||||
"GetTaskPayloadParams": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"_meta": {
|
||||
"description": "Protocol-level metadata for this request (SEP-1319)",
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/RequestMetaObject"
|
||||
|
|
@ -853,6 +792,7 @@
|
|||
]
|
||||
},
|
||||
"taskId": {
|
||||
"description": "Identifier of the task to query.",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
|
|
@ -1099,9 +1039,6 @@
|
|||
{
|
||||
"$ref": "#/definitions/NotificationNoParam2"
|
||||
},
|
||||
{
|
||||
"$ref": "#/definitions/Notification3"
|
||||
},
|
||||
{
|
||||
"$ref": "#/definitions/CustomNotification"
|
||||
}
|
||||
|
|
@ -1169,9 +1106,6 @@
|
|||
{
|
||||
"$ref": "#/definitions/Request11"
|
||||
},
|
||||
{
|
||||
"$ref": "#/definitions/RequestOptionalParam5"
|
||||
},
|
||||
{
|
||||
"$ref": "#/definitions/Request12"
|
||||
},
|
||||
|
|
@ -1251,11 +1185,6 @@
|
|||
"roots"
|
||||
]
|
||||
},
|
||||
"ListTasksMethod": {
|
||||
"type": "string",
|
||||
"format": "const",
|
||||
"const": "tasks/list"
|
||||
},
|
||||
"ListToolsRequestMethod": {
|
||||
"type": "string",
|
||||
"format": "const",
|
||||
|
|
@ -1311,21 +1240,6 @@
|
|||
"params"
|
||||
]
|
||||
},
|
||||
"Notification3": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"method": {
|
||||
"$ref": "#/definitions/TaskStatusNotificationMethod"
|
||||
},
|
||||
"params": {
|
||||
"$ref": "#/definitions/TaskStatusNotificationParam"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"method",
|
||||
"params"
|
||||
]
|
||||
},
|
||||
"NotificationMetaObject": {
|
||||
"description": "Metadata reserved by MCP on notifications. Extension keys are also allowed.",
|
||||
"type": "object",
|
||||
|
|
@ -1606,10 +1520,10 @@
|
|||
"type": "object",
|
||||
"properties": {
|
||||
"method": {
|
||||
"$ref": "#/definitions/GetTaskPayloadMethod"
|
||||
"$ref": "#/definitions/UpdateTaskMethod"
|
||||
},
|
||||
"params": {
|
||||
"$ref": "#/definitions/GetTaskPayloadParams"
|
||||
"$ref": "#/definitions/UpdateTaskParams"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
|
|
@ -1878,27 +1792,6 @@
|
|||
"method"
|
||||
]
|
||||
},
|
||||
"RequestOptionalParam5": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"method": {
|
||||
"$ref": "#/definitions/ListTasksMethod"
|
||||
},
|
||||
"params": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/PaginatedRequestParams"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"method"
|
||||
]
|
||||
},
|
||||
"Resource": {
|
||||
"description": "A known resource that the server is capable of reading (spec `Resource`).\n\nAlso used as the inner type of `ContentBlock::ResourceLink` (spec `ResourceLink extends Resource`).",
|
||||
"type": "object",
|
||||
|
|
@ -2246,19 +2139,6 @@
|
|||
}
|
||||
]
|
||||
},
|
||||
"SamplingTaskCapability": {
|
||||
"description": "Sampling task capability. Deprecated by SEP-2577; remains functional and\nwill be removed in a future release.\nSee <https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2577>.",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"createMessage": {
|
||||
"type": [
|
||||
"object",
|
||||
"null"
|
||||
],
|
||||
"additionalProperties": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"SetLevelRequestMethod": {
|
||||
"type": "string",
|
||||
"format": "const",
|
||||
|
|
@ -2393,188 +2273,6 @@
|
|||
"notifications"
|
||||
]
|
||||
},
|
||||
"TaskMetadata": {
|
||||
"description": "Metadata for augmenting a request with task execution (spec `TaskMetadata`).",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"ttl": {
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
],
|
||||
"format": "uint64",
|
||||
"minimum": 0
|
||||
}
|
||||
}
|
||||
},
|
||||
"TaskRequestsCapability": {
|
||||
"description": "Request types that support task-augmented execution.",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"elicitation": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/ElicitationTaskCapability"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"sampling": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/SamplingTaskCapability"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"tools": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/ToolsTaskCapability"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"TaskStatus": {
|
||||
"description": "Canonical task lifecycle status as defined by SEP-1686.",
|
||||
"oneOf": [
|
||||
{
|
||||
"description": "The receiver accepted the request and is currently working on it.",
|
||||
"type": "string",
|
||||
"const": "working"
|
||||
},
|
||||
{
|
||||
"description": "The receiver requires additional input before work can continue.",
|
||||
"type": "string",
|
||||
"const": "input_required"
|
||||
},
|
||||
{
|
||||
"description": "The underlying operation completed successfully and the result is ready.",
|
||||
"type": "string",
|
||||
"const": "completed"
|
||||
},
|
||||
{
|
||||
"description": "The underlying operation failed and will not continue.",
|
||||
"type": "string",
|
||||
"const": "failed"
|
||||
},
|
||||
{
|
||||
"description": "The task was cancelled and will not continue processing.",
|
||||
"type": "string",
|
||||
"const": "cancelled"
|
||||
}
|
||||
]
|
||||
},
|
||||
"TaskStatusNotificationMethod": {
|
||||
"type": "string",
|
||||
"format": "const",
|
||||
"const": "notifications/tasks/status"
|
||||
},
|
||||
"TaskStatusNotificationParam": {
|
||||
"description": "Parameters for a task status notification (spec `TaskStatusNotificationParams`).\n\nThe task fields are flattened at the top level: `NotificationParams & Task`.",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"_meta": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/NotificationMetaObject"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"createdAt": {
|
||||
"description": "ISO-8601 creation timestamp.",
|
||||
"type": "string"
|
||||
},
|
||||
"lastUpdatedAt": {
|
||||
"description": "ISO-8601 timestamp for the most recent status change.",
|
||||
"type": "string"
|
||||
},
|
||||
"pollInterval": {
|
||||
"description": "Suggested polling interval (milliseconds).",
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
],
|
||||
"format": "uint64",
|
||||
"minimum": 0
|
||||
},
|
||||
"status": {
|
||||
"description": "Current lifecycle status (see [`TaskStatus`]).",
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/TaskStatus"
|
||||
}
|
||||
]
|
||||
},
|
||||
"statusMessage": {
|
||||
"description": "Optional human-readable status message for UI surfaces.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"taskId": {
|
||||
"description": "Unique task identifier generated by the receiver.",
|
||||
"type": "string"
|
||||
},
|
||||
"ttl": {
|
||||
"description": "Retention window in milliseconds that the receiver agreed to honor.\n`None` (serialized as `null`) means unlimited retention.",
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
],
|
||||
"format": "uint64",
|
||||
"minimum": 0
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"taskId",
|
||||
"status",
|
||||
"createdAt",
|
||||
"lastUpdatedAt"
|
||||
]
|
||||
},
|
||||
"TasksCapability": {
|
||||
"description": "Task capabilities shared by client and server.",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"cancel": {
|
||||
"type": [
|
||||
"object",
|
||||
"null"
|
||||
],
|
||||
"additionalProperties": true
|
||||
},
|
||||
"list": {
|
||||
"type": [
|
||||
"object",
|
||||
"null"
|
||||
],
|
||||
"additionalProperties": true
|
||||
},
|
||||
"requests": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/TaskRequestsCapability"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"TextContent": {
|
||||
"description": "Text content block (spec `TextContent`).",
|
||||
"type": "object",
|
||||
|
|
@ -2679,18 +2377,6 @@
|
|||
"input"
|
||||
]
|
||||
},
|
||||
"ToolsTaskCapability": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"call": {
|
||||
"type": [
|
||||
"object",
|
||||
"null"
|
||||
],
|
||||
"additionalProperties": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"UnsubscribeRequestMethod": {
|
||||
"type": "string",
|
||||
"format": "const",
|
||||
|
|
@ -2720,6 +2406,40 @@
|
|||
"uri"
|
||||
]
|
||||
},
|
||||
"UpdateTaskMethod": {
|
||||
"type": "string",
|
||||
"format": "const",
|
||||
"const": "tasks/update"
|
||||
},
|
||||
"UpdateTaskParams": {
|
||||
"description": "Parameters for `tasks/update` (SEP-2663): deliver responses to outstanding\nin-task server-to-client requests surfaced via `tasks/get` `inputRequests`.",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"_meta": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/RequestMetaObject"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"inputResponses": {
|
||||
"description": "Responses to outstanding `inputRequests` previously surfaced by the\nserver. Each key MUST correspond to a currently-outstanding\n`inputRequests` key.",
|
||||
"type": "object",
|
||||
"additionalProperties": true
|
||||
},
|
||||
"taskId": {
|
||||
"description": "Identifier of the task to update.",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"taskId",
|
||||
"inputResponses"
|
||||
]
|
||||
},
|
||||
"UrlElicitationCapability": {
|
||||
"description": "Capability for URL mode elicitation.",
|
||||
"type": "object"
|
||||
|
|
|
|||
|
|
@ -126,7 +126,7 @@
|
|||
"const": "tools/call"
|
||||
},
|
||||
"CallToolRequestParams": {
|
||||
"description": "Parameters for calling a tool provided by an MCP server.\n\nContains the tool name and optional arguments needed to execute\nthe tool operation.\n\nThis implements `TaskAugmentedRequestParamsMeta` as tool calls can be\nlong-running and may benefit from task-based execution.",
|
||||
"description": "Parameters for calling a tool provided by an MCP server.\n\nContains the tool name and optional arguments needed to execute\nthe tool operation.",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"_meta": {
|
||||
|
|
@ -166,17 +166,6 @@
|
|||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"task": {
|
||||
"description": "Task metadata for async task management (SEP-1319)",
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/TaskMetadata"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
|
|
@ -304,16 +293,6 @@
|
|||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"tasks": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/TasksCapability"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
@ -657,18 +636,6 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"ElicitationTaskCapability": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"create": {
|
||||
"type": [
|
||||
"object",
|
||||
"null"
|
||||
],
|
||||
"additionalProperties": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"EmbeddedResource": {
|
||||
"description": "Embedded resource content (spec `EmbeddedResource`).",
|
||||
"type": "object",
|
||||
|
|
@ -815,34 +782,6 @@
|
|||
"type": "object",
|
||||
"properties": {
|
||||
"_meta": {
|
||||
"description": "Protocol-level metadata for this request (SEP-1319)",
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/RequestMetaObject"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"taskId": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"taskId"
|
||||
]
|
||||
},
|
||||
"GetTaskPayloadMethod": {
|
||||
"type": "string",
|
||||
"format": "const",
|
||||
"const": "tasks/result"
|
||||
},
|
||||
"GetTaskPayloadParams": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"_meta": {
|
||||
"description": "Protocol-level metadata for this request (SEP-1319)",
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/RequestMetaObject"
|
||||
|
|
@ -853,6 +792,7 @@
|
|||
]
|
||||
},
|
||||
"taskId": {
|
||||
"description": "Identifier of the task to query.",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
|
|
@ -1099,9 +1039,6 @@
|
|||
{
|
||||
"$ref": "#/definitions/NotificationNoParam2"
|
||||
},
|
||||
{
|
||||
"$ref": "#/definitions/Notification3"
|
||||
},
|
||||
{
|
||||
"$ref": "#/definitions/CustomNotification"
|
||||
}
|
||||
|
|
@ -1169,9 +1106,6 @@
|
|||
{
|
||||
"$ref": "#/definitions/Request11"
|
||||
},
|
||||
{
|
||||
"$ref": "#/definitions/RequestOptionalParam5"
|
||||
},
|
||||
{
|
||||
"$ref": "#/definitions/Request12"
|
||||
},
|
||||
|
|
@ -1251,11 +1185,6 @@
|
|||
"roots"
|
||||
]
|
||||
},
|
||||
"ListTasksMethod": {
|
||||
"type": "string",
|
||||
"format": "const",
|
||||
"const": "tasks/list"
|
||||
},
|
||||
"ListToolsRequestMethod": {
|
||||
"type": "string",
|
||||
"format": "const",
|
||||
|
|
@ -1311,21 +1240,6 @@
|
|||
"params"
|
||||
]
|
||||
},
|
||||
"Notification3": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"method": {
|
||||
"$ref": "#/definitions/TaskStatusNotificationMethod"
|
||||
},
|
||||
"params": {
|
||||
"$ref": "#/definitions/TaskStatusNotificationParam"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"method",
|
||||
"params"
|
||||
]
|
||||
},
|
||||
"NotificationMetaObject": {
|
||||
"description": "Metadata reserved by MCP on notifications. Extension keys are also allowed.",
|
||||
"type": "object",
|
||||
|
|
@ -1606,10 +1520,10 @@
|
|||
"type": "object",
|
||||
"properties": {
|
||||
"method": {
|
||||
"$ref": "#/definitions/GetTaskPayloadMethod"
|
||||
"$ref": "#/definitions/UpdateTaskMethod"
|
||||
},
|
||||
"params": {
|
||||
"$ref": "#/definitions/GetTaskPayloadParams"
|
||||
"$ref": "#/definitions/UpdateTaskParams"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
|
|
@ -1878,27 +1792,6 @@
|
|||
"method"
|
||||
]
|
||||
},
|
||||
"RequestOptionalParam5": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"method": {
|
||||
"$ref": "#/definitions/ListTasksMethod"
|
||||
},
|
||||
"params": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/PaginatedRequestParams"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"method"
|
||||
]
|
||||
},
|
||||
"Resource": {
|
||||
"description": "A known resource that the server is capable of reading (spec `Resource`).\n\nAlso used as the inner type of `ContentBlock::ResourceLink` (spec `ResourceLink extends Resource`).",
|
||||
"type": "object",
|
||||
|
|
@ -2246,19 +2139,6 @@
|
|||
}
|
||||
]
|
||||
},
|
||||
"SamplingTaskCapability": {
|
||||
"description": "Sampling task capability. Deprecated by SEP-2577; remains functional and\nwill be removed in a future release.\nSee <https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2577>.",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"createMessage": {
|
||||
"type": [
|
||||
"object",
|
||||
"null"
|
||||
],
|
||||
"additionalProperties": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"SetLevelRequestMethod": {
|
||||
"type": "string",
|
||||
"format": "const",
|
||||
|
|
@ -2393,188 +2273,6 @@
|
|||
"notifications"
|
||||
]
|
||||
},
|
||||
"TaskMetadata": {
|
||||
"description": "Metadata for augmenting a request with task execution (spec `TaskMetadata`).",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"ttl": {
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
],
|
||||
"format": "uint64",
|
||||
"minimum": 0
|
||||
}
|
||||
}
|
||||
},
|
||||
"TaskRequestsCapability": {
|
||||
"description": "Request types that support task-augmented execution.",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"elicitation": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/ElicitationTaskCapability"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"sampling": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/SamplingTaskCapability"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"tools": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/ToolsTaskCapability"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"TaskStatus": {
|
||||
"description": "Canonical task lifecycle status as defined by SEP-1686.",
|
||||
"oneOf": [
|
||||
{
|
||||
"description": "The receiver accepted the request and is currently working on it.",
|
||||
"type": "string",
|
||||
"const": "working"
|
||||
},
|
||||
{
|
||||
"description": "The receiver requires additional input before work can continue.",
|
||||
"type": "string",
|
||||
"const": "input_required"
|
||||
},
|
||||
{
|
||||
"description": "The underlying operation completed successfully and the result is ready.",
|
||||
"type": "string",
|
||||
"const": "completed"
|
||||
},
|
||||
{
|
||||
"description": "The underlying operation failed and will not continue.",
|
||||
"type": "string",
|
||||
"const": "failed"
|
||||
},
|
||||
{
|
||||
"description": "The task was cancelled and will not continue processing.",
|
||||
"type": "string",
|
||||
"const": "cancelled"
|
||||
}
|
||||
]
|
||||
},
|
||||
"TaskStatusNotificationMethod": {
|
||||
"type": "string",
|
||||
"format": "const",
|
||||
"const": "notifications/tasks/status"
|
||||
},
|
||||
"TaskStatusNotificationParam": {
|
||||
"description": "Parameters for a task status notification (spec `TaskStatusNotificationParams`).\n\nThe task fields are flattened at the top level: `NotificationParams & Task`.",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"_meta": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/NotificationMetaObject"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"createdAt": {
|
||||
"description": "ISO-8601 creation timestamp.",
|
||||
"type": "string"
|
||||
},
|
||||
"lastUpdatedAt": {
|
||||
"description": "ISO-8601 timestamp for the most recent status change.",
|
||||
"type": "string"
|
||||
},
|
||||
"pollInterval": {
|
||||
"description": "Suggested polling interval (milliseconds).",
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
],
|
||||
"format": "uint64",
|
||||
"minimum": 0
|
||||
},
|
||||
"status": {
|
||||
"description": "Current lifecycle status (see [`TaskStatus`]).",
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/TaskStatus"
|
||||
}
|
||||
]
|
||||
},
|
||||
"statusMessage": {
|
||||
"description": "Optional human-readable status message for UI surfaces.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"taskId": {
|
||||
"description": "Unique task identifier generated by the receiver.",
|
||||
"type": "string"
|
||||
},
|
||||
"ttl": {
|
||||
"description": "Retention window in milliseconds that the receiver agreed to honor.\n`None` (serialized as `null`) means unlimited retention.",
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
],
|
||||
"format": "uint64",
|
||||
"minimum": 0
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"taskId",
|
||||
"status",
|
||||
"createdAt",
|
||||
"lastUpdatedAt"
|
||||
]
|
||||
},
|
||||
"TasksCapability": {
|
||||
"description": "Task capabilities shared by client and server.",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"cancel": {
|
||||
"type": [
|
||||
"object",
|
||||
"null"
|
||||
],
|
||||
"additionalProperties": true
|
||||
},
|
||||
"list": {
|
||||
"type": [
|
||||
"object",
|
||||
"null"
|
||||
],
|
||||
"additionalProperties": true
|
||||
},
|
||||
"requests": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/TaskRequestsCapability"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"TextContent": {
|
||||
"description": "Text content block (spec `TextContent`).",
|
||||
"type": "object",
|
||||
|
|
@ -2679,18 +2377,6 @@
|
|||
"input"
|
||||
]
|
||||
},
|
||||
"ToolsTaskCapability": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"call": {
|
||||
"type": [
|
||||
"object",
|
||||
"null"
|
||||
],
|
||||
"additionalProperties": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"UnsubscribeRequestMethod": {
|
||||
"type": "string",
|
||||
"format": "const",
|
||||
|
|
@ -2720,6 +2406,40 @@
|
|||
"uri"
|
||||
]
|
||||
},
|
||||
"UpdateTaskMethod": {
|
||||
"type": "string",
|
||||
"format": "const",
|
||||
"const": "tasks/update"
|
||||
},
|
||||
"UpdateTaskParams": {
|
||||
"description": "Parameters for `tasks/update` (SEP-2663): deliver responses to outstanding\nin-task server-to-client requests surfaced via `tasks/get` `inputRequests`.",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"_meta": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/RequestMetaObject"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"inputResponses": {
|
||||
"description": "Responses to outstanding `inputRequests` previously surfaced by the\nserver. Each key MUST correspond to a currently-outstanding\n`inputRequests` key.",
|
||||
"type": "object",
|
||||
"additionalProperties": true
|
||||
},
|
||||
"taskId": {
|
||||
"description": "Identifier of the task to update.",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"taskId",
|
||||
"inputResponses"
|
||||
]
|
||||
},
|
||||
"UrlElicitationCapability": {
|
||||
"description": "Capability for URL mode elicitation.",
|
||||
"type": "object"
|
||||
|
|
|
|||
|
|
@ -212,73 +212,6 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"CancelTaskResult": {
|
||||
"description": "Response to a `tasks/cancel` request.\n\nPer spec, `CancelTaskResult = allOf[Result, Task]` — same shape as `GetTaskResult`.",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"_meta": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/MetaObject"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"createdAt": {
|
||||
"description": "ISO-8601 creation timestamp.",
|
||||
"type": "string"
|
||||
},
|
||||
"lastUpdatedAt": {
|
||||
"description": "ISO-8601 timestamp for the most recent status change.",
|
||||
"type": "string"
|
||||
},
|
||||
"pollInterval": {
|
||||
"description": "Suggested polling interval (milliseconds).",
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
],
|
||||
"format": "uint64",
|
||||
"minimum": 0
|
||||
},
|
||||
"status": {
|
||||
"description": "Current lifecycle status (see [`TaskStatus`]).",
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/TaskStatus"
|
||||
}
|
||||
]
|
||||
},
|
||||
"statusMessage": {
|
||||
"description": "Optional human-readable status message for UI surfaces.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"taskId": {
|
||||
"description": "Unique task identifier generated by the receiver.",
|
||||
"type": "string"
|
||||
},
|
||||
"ttl": {
|
||||
"description": "Retention window in milliseconds that the receiver agreed to honor.\n`None` (serialized as `null`) means unlimited retention.",
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
],
|
||||
"format": "uint64",
|
||||
"minimum": 0
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"taskId",
|
||||
"status",
|
||||
"createdAt",
|
||||
"lastUpdatedAt"
|
||||
]
|
||||
},
|
||||
"CancelledNotificationMethod": {
|
||||
"type": "string",
|
||||
"format": "const",
|
||||
|
|
@ -373,16 +306,6 @@
|
|||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"tasks": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/TasksCapability"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
@ -645,17 +568,6 @@
|
|||
"null"
|
||||
]
|
||||
},
|
||||
"task": {
|
||||
"description": "Task metadata for async task management (SEP-1319)",
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/TaskMetadata"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"temperature": {
|
||||
"description": "Temperature for controlling randomness (0.0 to 1.0)",
|
||||
"type": [
|
||||
|
|
@ -693,7 +605,7 @@
|
|||
]
|
||||
},
|
||||
"CreateTaskResult": {
|
||||
"description": "Wrapper returned by task-augmented requests (CreateTaskResult in SEP-1686).",
|
||||
"description": "Result returned in lieu of a standard result to indicate the request will\nbe processed asynchronously (spec `CreateTaskResult`, `resultType: \"task\"`).\n\nThe embedded task is the seed state for the task; the client uses\n`task.task_id` for all subsequent `tasks/get`, `tasks/update`, and\n`tasks/cancel` calls.",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"_meta": {
|
||||
|
|
@ -706,12 +618,66 @@
|
|||
}
|
||||
]
|
||||
},
|
||||
"task": {
|
||||
"$ref": "#/definitions/Task"
|
||||
"createdAt": {
|
||||
"description": "ISO 8601 timestamp when the task was created.",
|
||||
"type": "string"
|
||||
},
|
||||
"lastUpdatedAt": {
|
||||
"description": "ISO 8601 timestamp when the task was last updated.",
|
||||
"type": "string"
|
||||
},
|
||||
"pollIntervalMs": {
|
||||
"description": "Suggested polling interval in integer milliseconds. Clients SHOULD honor\nthis value to avoid overwhelming the server. This value MAY change over\nthe lifetime of a task.",
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
],
|
||||
"format": "uint64",
|
||||
"minimum": 0
|
||||
},
|
||||
"resultType": {
|
||||
"description": "Always `\"task\"`.",
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/ResultType"
|
||||
}
|
||||
]
|
||||
},
|
||||
"status": {
|
||||
"description": "Current task status.",
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/TaskStatus"
|
||||
}
|
||||
]
|
||||
},
|
||||
"statusMessage": {
|
||||
"description": "Optional message describing the current task state.\nThis MAY be exposed to the end-user or model.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"taskId": {
|
||||
"description": "Stable identifier for this task, generated by the server.",
|
||||
"type": "string"
|
||||
},
|
||||
"ttlMs": {
|
||||
"description": "Time-to-live duration from creation in integer milliseconds; `None`\n(serialized as `null`) means unlimited. The server may discard the task\nafter the TTL elapses. This value MAY change over the lifetime of a task.",
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
],
|
||||
"format": "uint64",
|
||||
"minimum": 0
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"task"
|
||||
"resultType",
|
||||
"taskId",
|
||||
"status",
|
||||
"createdAt",
|
||||
"lastUpdatedAt"
|
||||
]
|
||||
},
|
||||
"CustomNotification": {
|
||||
|
|
@ -1047,18 +1013,6 @@
|
|||
"properties"
|
||||
]
|
||||
},
|
||||
"ElicitationTaskCapability": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"create": {
|
||||
"type": [
|
||||
"object",
|
||||
"null"
|
||||
],
|
||||
"additionalProperties": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"EmbeddedResource": {
|
||||
"description": "Embedded resource content (spec `EmbeddedResource`).",
|
||||
"type": "object",
|
||||
|
|
@ -1199,11 +1153,8 @@
|
|||
"messages"
|
||||
]
|
||||
},
|
||||
"GetTaskPayloadResult": {
|
||||
"description": "Response to a `tasks/result` request.\n\nPer spec, the result structure matches the original request type\n(e.g., `CallToolResult` for `tools/call`). This is represented as\nan open object. The payload is the original request's result\nserialized as a JSON value."
|
||||
},
|
||||
"GetTaskResult": {
|
||||
"description": "Response to a `tasks/get` request.\n\nPer spec, `GetTaskResult = allOf[Result, Task]` — the Task fields are\nflattened at the top level, not nested under a `task` key.",
|
||||
"description": "Response to a `tasks/get` request (spec `GetTaskResult = Result & DetailedTask`).\n\n`resultType` is `\"complete\"` — this is the standard result shape for\n`tasks/get`, not a task handle.",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"_meta": {
|
||||
|
|
@ -1217,15 +1168,31 @@
|
|||
]
|
||||
},
|
||||
"createdAt": {
|
||||
"description": "ISO-8601 creation timestamp.",
|
||||
"description": "ISO 8601 timestamp when the task was created.",
|
||||
"type": "string"
|
||||
},
|
||||
"error": {
|
||||
"type": [
|
||||
"object",
|
||||
"null"
|
||||
],
|
||||
"additionalProperties": true
|
||||
},
|
||||
"inputRequests": {
|
||||
"type": [
|
||||
"object",
|
||||
"null"
|
||||
],
|
||||
"additionalProperties": {
|
||||
"$ref": "#/definitions/InputRequest"
|
||||
}
|
||||
},
|
||||
"lastUpdatedAt": {
|
||||
"description": "ISO-8601 timestamp for the most recent status change.",
|
||||
"description": "ISO 8601 timestamp when the task was last updated.",
|
||||
"type": "string"
|
||||
},
|
||||
"pollInterval": {
|
||||
"description": "Suggested polling interval (milliseconds).",
|
||||
"pollIntervalMs": {
|
||||
"description": "Suggested polling interval in integer milliseconds. Clients SHOULD honor\nthis value to avoid overwhelming the server. This value MAY change over\nthe lifetime of a task.",
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
|
|
@ -1233,8 +1200,24 @@
|
|||
"format": "uint64",
|
||||
"minimum": 0
|
||||
},
|
||||
"result": {
|
||||
"type": [
|
||||
"object",
|
||||
"null"
|
||||
],
|
||||
"additionalProperties": true
|
||||
},
|
||||
"resultType": {
|
||||
"description": "Result type discriminator. `tasks/get` responses are standard results:\n`\"complete\"` (SEP-2322). Absent values deserialize as `\"complete\"`.",
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/ResultType"
|
||||
}
|
||||
],
|
||||
"default": "complete"
|
||||
},
|
||||
"status": {
|
||||
"description": "Current lifecycle status (see [`TaskStatus`]).",
|
||||
"description": "Current task status.",
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/TaskStatus"
|
||||
|
|
@ -1242,18 +1225,18 @@
|
|||
]
|
||||
},
|
||||
"statusMessage": {
|
||||
"description": "Optional human-readable status message for UI surfaces.",
|
||||
"description": "Optional message describing the current task state.\nThis MAY be exposed to the end-user or model.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"taskId": {
|
||||
"description": "Unique task identifier generated by the receiver.",
|
||||
"description": "Stable identifier for this task, generated by the server.",
|
||||
"type": "string"
|
||||
},
|
||||
"ttl": {
|
||||
"description": "Retention window in milliseconds that the receiver agreed to honor.\n`None` (serialized as `null`) means unlimited retention.",
|
||||
"ttlMs": {
|
||||
"description": "Time-to-live duration from creation in integer milliseconds; `None`\n(serialized as `null`) means unlimited. The server may discard the task\nafter the TTL elapses. This value MAY change over the lifetime of a task.",
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
|
|
@ -1945,36 +1928,6 @@
|
|||
"format": "const",
|
||||
"const": "roots/list"
|
||||
},
|
||||
"ListTasksResult": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"_meta": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/MetaObject"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"nextCursor": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"tasks": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/Task"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"tasks"
|
||||
]
|
||||
},
|
||||
"ListToolsResult": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
|
@ -2246,7 +2199,7 @@
|
|||
"$ref": "#/definitions/TaskStatusNotificationMethod"
|
||||
},
|
||||
"params": {
|
||||
"$ref": "#/definitions/TaskStatusNotificationParam"
|
||||
"$ref": "#/definitions/TaskStatusNotificationParams"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
|
|
@ -3191,19 +3144,6 @@
|
|||
}
|
||||
]
|
||||
},
|
||||
"SamplingTaskCapability": {
|
||||
"description": "Sampling task capability. Deprecated by SEP-2577; remains functional and\nwill be removed in a future release.\nSee <https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2577>.",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"createMessage": {
|
||||
"type": [
|
||||
"object",
|
||||
"null"
|
||||
],
|
||||
"additionalProperties": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"ServerCapabilities": {
|
||||
"title": "Builder",
|
||||
"description": "```rust\n# use rmcp::model::ServerCapabilities;\nlet cap = ServerCapabilities::builder()\n .enable_experimental()\n .enable_prompts()\n .enable_resources()\n .enable_tools()\n .enable_tool_list_changed()\n .build();\n```",
|
||||
|
|
@ -3265,16 +3205,6 @@
|
|||
}
|
||||
]
|
||||
},
|
||||
"tasks": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/TasksCapability"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"tools": {
|
||||
"anyOf": [
|
||||
{
|
||||
|
|
@ -3325,15 +3255,9 @@
|
|||
{
|
||||
"$ref": "#/definitions/CreateTaskResult"
|
||||
},
|
||||
{
|
||||
"$ref": "#/definitions/ListTasksResult"
|
||||
},
|
||||
{
|
||||
"$ref": "#/definitions/GetTaskResult"
|
||||
},
|
||||
{
|
||||
"$ref": "#/definitions/CancelTaskResult"
|
||||
},
|
||||
{
|
||||
"$ref": "#/definitions/CallToolResult"
|
||||
},
|
||||
|
|
@ -3341,7 +3265,7 @@
|
|||
"$ref": "#/definitions/InputRequiredResult"
|
||||
},
|
||||
{
|
||||
"$ref": "#/definitions/GetTaskPayloadResult"
|
||||
"$ref": "#/definitions/TaskAckResult"
|
||||
},
|
||||
{
|
||||
"$ref": "#/definitions/EmptyObject"
|
||||
|
|
@ -3528,138 +3452,58 @@
|
|||
"io.modelcontextprotocol/subscriptionId"
|
||||
]
|
||||
},
|
||||
"Task": {
|
||||
"description": "Primary Task object that surfaces metadata during the task lifecycle.\n\nPer spec, `lastUpdatedAt` and `ttl` are required fields.\n`ttl` is nullable (`null` means unlimited retention).",
|
||||
"TaskAckResult": {
|
||||
"description": "Empty acknowledgement for `tasks/update` and `tasks/cancel` (SEP-2663).\n\nThe spec requires these acks to be empty results carrying the SEP-2322\n`resultType: \"complete\"` discriminator; task state changes are observed\nvia the next `tasks/get`.",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"createdAt": {
|
||||
"description": "ISO-8601 creation timestamp.",
|
||||
"type": "string"
|
||||
},
|
||||
"lastUpdatedAt": {
|
||||
"description": "ISO-8601 timestamp for the most recent status change.",
|
||||
"type": "string"
|
||||
},
|
||||
"pollInterval": {
|
||||
"description": "Suggested polling interval (milliseconds).",
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
],
|
||||
"format": "uint64",
|
||||
"minimum": 0
|
||||
},
|
||||
"status": {
|
||||
"description": "Current lifecycle status (see [`TaskStatus`]).",
|
||||
"allOf": [
|
||||
"_meta": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/TaskStatus"
|
||||
"$ref": "#/definitions/MetaObject"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"statusMessage": {
|
||||
"description": "Optional human-readable status message for UI surfaces.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
"resultType": {
|
||||
"description": "Always `\"complete\"`.",
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/ResultType"
|
||||
}
|
||||
]
|
||||
},
|
||||
"taskId": {
|
||||
"description": "Unique task identifier generated by the receiver.",
|
||||
"type": "string"
|
||||
},
|
||||
"ttl": {
|
||||
"description": "Retention window in milliseconds that the receiver agreed to honor.\n`None` (serialized as `null`) means unlimited retention.",
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
],
|
||||
"format": "uint64",
|
||||
"minimum": 0
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"taskId",
|
||||
"status",
|
||||
"createdAt",
|
||||
"lastUpdatedAt"
|
||||
"resultType"
|
||||
]
|
||||
},
|
||||
"TaskMetadata": {
|
||||
"description": "Metadata for augmenting a request with task execution (spec `TaskMetadata`).",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"ttl": {
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
],
|
||||
"format": "uint64",
|
||||
"minimum": 0
|
||||
}
|
||||
}
|
||||
},
|
||||
"TaskRequestsCapability": {
|
||||
"description": "Request types that support task-augmented execution.",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"elicitation": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/ElicitationTaskCapability"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"sampling": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/SamplingTaskCapability"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"tools": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/ToolsTaskCapability"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"TaskStatus": {
|
||||
"description": "Canonical task lifecycle status as defined by SEP-1686.",
|
||||
"description": "Canonical task lifecycle status (SEP-2663).",
|
||||
"oneOf": [
|
||||
{
|
||||
"description": "The receiver accepted the request and is currently working on it.",
|
||||
"description": "The request is currently being processed.",
|
||||
"type": "string",
|
||||
"const": "working"
|
||||
},
|
||||
{
|
||||
"description": "The receiver requires additional input before work can continue.",
|
||||
"description": "The server needs input from the client before the task can proceed.",
|
||||
"type": "string",
|
||||
"const": "input_required"
|
||||
},
|
||||
{
|
||||
"description": "The underlying operation completed successfully and the result is ready.",
|
||||
"description": "The request completed successfully and the result is available.\nThis includes tool calls that returned results with `isError: true`.",
|
||||
"type": "string",
|
||||
"const": "completed"
|
||||
},
|
||||
{
|
||||
"description": "The underlying operation failed and will not continue.",
|
||||
"description": "The request failed due to a JSON-RPC error during execution.",
|
||||
"type": "string",
|
||||
"const": "failed"
|
||||
},
|
||||
{
|
||||
"description": "The task was cancelled and will not continue processing.",
|
||||
"description": "The request was cancelled before completion.",
|
||||
"type": "string",
|
||||
"const": "cancelled"
|
||||
}
|
||||
|
|
@ -3668,10 +3512,10 @@
|
|||
"TaskStatusNotificationMethod": {
|
||||
"type": "string",
|
||||
"format": "const",
|
||||
"const": "notifications/tasks/status"
|
||||
"const": "notifications/tasks"
|
||||
},
|
||||
"TaskStatusNotificationParam": {
|
||||
"description": "Parameters for a task status notification (spec `TaskStatusNotificationParams`).\n\nThe task fields are flattened at the top level: `NotificationParams & Task`.",
|
||||
"TaskStatusNotificationParams": {
|
||||
"description": "Parameters for a task status notification (spec `TaskStatusNotificationParams`).\n\nCarries a complete [`DetailedTask`] for the current status, identical to\nwhat `tasks/get` would have returned at that moment. The task fields are\nflattened at the top level: `NotificationParams & Task`.",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"_meta": {
|
||||
|
|
@ -3685,15 +3529,31 @@
|
|||
]
|
||||
},
|
||||
"createdAt": {
|
||||
"description": "ISO-8601 creation timestamp.",
|
||||
"description": "ISO 8601 timestamp when the task was created.",
|
||||
"type": "string"
|
||||
},
|
||||
"error": {
|
||||
"type": [
|
||||
"object",
|
||||
"null"
|
||||
],
|
||||
"additionalProperties": true
|
||||
},
|
||||
"inputRequests": {
|
||||
"type": [
|
||||
"object",
|
||||
"null"
|
||||
],
|
||||
"additionalProperties": {
|
||||
"$ref": "#/definitions/InputRequest"
|
||||
}
|
||||
},
|
||||
"lastUpdatedAt": {
|
||||
"description": "ISO-8601 timestamp for the most recent status change.",
|
||||
"description": "ISO 8601 timestamp when the task was last updated.",
|
||||
"type": "string"
|
||||
},
|
||||
"pollInterval": {
|
||||
"description": "Suggested polling interval (milliseconds).",
|
||||
"pollIntervalMs": {
|
||||
"description": "Suggested polling interval in integer milliseconds. Clients SHOULD honor\nthis value to avoid overwhelming the server. This value MAY change over\nthe lifetime of a task.",
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
|
|
@ -3701,8 +3561,15 @@
|
|||
"format": "uint64",
|
||||
"minimum": 0
|
||||
},
|
||||
"result": {
|
||||
"type": [
|
||||
"object",
|
||||
"null"
|
||||
],
|
||||
"additionalProperties": true
|
||||
},
|
||||
"status": {
|
||||
"description": "Current lifecycle status (see [`TaskStatus`]).",
|
||||
"description": "Current task status.",
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/TaskStatus"
|
||||
|
|
@ -3710,18 +3577,18 @@
|
|||
]
|
||||
},
|
||||
"statusMessage": {
|
||||
"description": "Optional human-readable status message for UI surfaces.",
|
||||
"description": "Optional message describing the current task state.\nThis MAY be exposed to the end-user or model.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"taskId": {
|
||||
"description": "Unique task identifier generated by the receiver.",
|
||||
"description": "Stable identifier for this task, generated by the server.",
|
||||
"type": "string"
|
||||
},
|
||||
"ttl": {
|
||||
"description": "Retention window in milliseconds that the receiver agreed to honor.\n`None` (serialized as `null`) means unlimited retention.",
|
||||
"ttlMs": {
|
||||
"description": "Time-to-live duration from creation in integer milliseconds; `None`\n(serialized as `null`) means unlimited. The server may discard the task\nafter the TTL elapses. This value MAY change over the lifetime of a task.",
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
|
|
@ -3737,56 +3604,6 @@
|
|||
"lastUpdatedAt"
|
||||
]
|
||||
},
|
||||
"TaskSupport": {
|
||||
"description": "Per-tool task support mode as defined in the MCP specification.\n\nThis enum indicates whether a tool supports task-based invocation,\nallowing clients to know how to properly call the tool.\n\nSee [Tool-Level Negotiation](https://modelcontextprotocol.io/specification/2025-11-25/basic/utilities/tasks#tool-level-negotiation).",
|
||||
"oneOf": [
|
||||
{
|
||||
"description": "Clients MUST NOT invoke this tool as a task (default behavior).",
|
||||
"type": "string",
|
||||
"const": "forbidden"
|
||||
},
|
||||
{
|
||||
"description": "Clients MAY invoke this tool as either a task or a normal call.",
|
||||
"type": "string",
|
||||
"const": "optional"
|
||||
},
|
||||
{
|
||||
"description": "Clients MUST invoke this tool as a task.",
|
||||
"type": "string",
|
||||
"const": "required"
|
||||
}
|
||||
]
|
||||
},
|
||||
"TasksCapability": {
|
||||
"description": "Task capabilities shared by client and server.",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"cancel": {
|
||||
"type": [
|
||||
"object",
|
||||
"null"
|
||||
],
|
||||
"additionalProperties": true
|
||||
},
|
||||
"list": {
|
||||
"type": [
|
||||
"object",
|
||||
"null"
|
||||
],
|
||||
"additionalProperties": true
|
||||
},
|
||||
"requests": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/TaskRequestsCapability"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"TextContent": {
|
||||
"description": "Text content block (spec `TextContent`).",
|
||||
"type": "object",
|
||||
|
|
@ -3960,17 +3777,6 @@
|
|||
"null"
|
||||
]
|
||||
},
|
||||
"execution": {
|
||||
"description": "Execution-related configuration including task support mode.",
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/ToolExecution"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"icons": {
|
||||
"description": "Optional list of icons for the tool",
|
||||
"type": [
|
||||
|
|
@ -4089,23 +3895,6 @@
|
|||
}
|
||||
]
|
||||
},
|
||||
"ToolExecution": {
|
||||
"description": "Execution-related configuration for a tool.\n\nThis struct contains settings that control how a tool should be executed,\nincluding task support configuration.",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"taskSupport": {
|
||||
"description": "Indicates whether this tool supports task-based invocation.\n\nWhen not present or set to `Forbidden`, clients MUST NOT invoke this tool as a task.\nWhen set to `Optional`, clients MAY invoke this tool as a task or normal call.\nWhen set to `Required`, clients MUST invoke this tool as a task.",
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/TaskSupport"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"ToolListChangedNotificationMethod": {
|
||||
"type": "string",
|
||||
"format": "const",
|
||||
|
|
@ -4191,18 +3980,6 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"ToolsTaskCapability": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"call": {
|
||||
"type": [
|
||||
"object",
|
||||
"null"
|
||||
],
|
||||
"additionalProperties": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"UntitledItems": {
|
||||
"description": "Items for untitled multi-select options",
|
||||
"type": "object",
|
||||
|
|
|
|||
|
|
@ -212,73 +212,6 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"CancelTaskResult": {
|
||||
"description": "Response to a `tasks/cancel` request.\n\nPer spec, `CancelTaskResult = allOf[Result, Task]` — same shape as `GetTaskResult`.",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"_meta": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/MetaObject"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"createdAt": {
|
||||
"description": "ISO-8601 creation timestamp.",
|
||||
"type": "string"
|
||||
},
|
||||
"lastUpdatedAt": {
|
||||
"description": "ISO-8601 timestamp for the most recent status change.",
|
||||
"type": "string"
|
||||
},
|
||||
"pollInterval": {
|
||||
"description": "Suggested polling interval (milliseconds).",
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
],
|
||||
"format": "uint64",
|
||||
"minimum": 0
|
||||
},
|
||||
"status": {
|
||||
"description": "Current lifecycle status (see [`TaskStatus`]).",
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/TaskStatus"
|
||||
}
|
||||
]
|
||||
},
|
||||
"statusMessage": {
|
||||
"description": "Optional human-readable status message for UI surfaces.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"taskId": {
|
||||
"description": "Unique task identifier generated by the receiver.",
|
||||
"type": "string"
|
||||
},
|
||||
"ttl": {
|
||||
"description": "Retention window in milliseconds that the receiver agreed to honor.\n`None` (serialized as `null`) means unlimited retention.",
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
],
|
||||
"format": "uint64",
|
||||
"minimum": 0
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"taskId",
|
||||
"status",
|
||||
"createdAt",
|
||||
"lastUpdatedAt"
|
||||
]
|
||||
},
|
||||
"CancelledNotificationMethod": {
|
||||
"type": "string",
|
||||
"format": "const",
|
||||
|
|
@ -373,16 +306,6 @@
|
|||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"tasks": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/TasksCapability"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
@ -645,17 +568,6 @@
|
|||
"null"
|
||||
]
|
||||
},
|
||||
"task": {
|
||||
"description": "Task metadata for async task management (SEP-1319)",
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/TaskMetadata"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"temperature": {
|
||||
"description": "Temperature for controlling randomness (0.0 to 1.0)",
|
||||
"type": [
|
||||
|
|
@ -693,7 +605,7 @@
|
|||
]
|
||||
},
|
||||
"CreateTaskResult": {
|
||||
"description": "Wrapper returned by task-augmented requests (CreateTaskResult in SEP-1686).",
|
||||
"description": "Result returned in lieu of a standard result to indicate the request will\nbe processed asynchronously (spec `CreateTaskResult`, `resultType: \"task\"`).\n\nThe embedded task is the seed state for the task; the client uses\n`task.task_id` for all subsequent `tasks/get`, `tasks/update`, and\n`tasks/cancel` calls.",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"_meta": {
|
||||
|
|
@ -706,12 +618,66 @@
|
|||
}
|
||||
]
|
||||
},
|
||||
"task": {
|
||||
"$ref": "#/definitions/Task"
|
||||
"createdAt": {
|
||||
"description": "ISO 8601 timestamp when the task was created.",
|
||||
"type": "string"
|
||||
},
|
||||
"lastUpdatedAt": {
|
||||
"description": "ISO 8601 timestamp when the task was last updated.",
|
||||
"type": "string"
|
||||
},
|
||||
"pollIntervalMs": {
|
||||
"description": "Suggested polling interval in integer milliseconds. Clients SHOULD honor\nthis value to avoid overwhelming the server. This value MAY change over\nthe lifetime of a task.",
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
],
|
||||
"format": "uint64",
|
||||
"minimum": 0
|
||||
},
|
||||
"resultType": {
|
||||
"description": "Always `\"task\"`.",
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/ResultType"
|
||||
}
|
||||
]
|
||||
},
|
||||
"status": {
|
||||
"description": "Current task status.",
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/TaskStatus"
|
||||
}
|
||||
]
|
||||
},
|
||||
"statusMessage": {
|
||||
"description": "Optional message describing the current task state.\nThis MAY be exposed to the end-user or model.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"taskId": {
|
||||
"description": "Stable identifier for this task, generated by the server.",
|
||||
"type": "string"
|
||||
},
|
||||
"ttlMs": {
|
||||
"description": "Time-to-live duration from creation in integer milliseconds; `None`\n(serialized as `null`) means unlimited. The server may discard the task\nafter the TTL elapses. This value MAY change over the lifetime of a task.",
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
],
|
||||
"format": "uint64",
|
||||
"minimum": 0
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"task"
|
||||
"resultType",
|
||||
"taskId",
|
||||
"status",
|
||||
"createdAt",
|
||||
"lastUpdatedAt"
|
||||
]
|
||||
},
|
||||
"CustomNotification": {
|
||||
|
|
@ -1047,18 +1013,6 @@
|
|||
"properties"
|
||||
]
|
||||
},
|
||||
"ElicitationTaskCapability": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"create": {
|
||||
"type": [
|
||||
"object",
|
||||
"null"
|
||||
],
|
||||
"additionalProperties": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"EmbeddedResource": {
|
||||
"description": "Embedded resource content (spec `EmbeddedResource`).",
|
||||
"type": "object",
|
||||
|
|
@ -1199,11 +1153,8 @@
|
|||
"messages"
|
||||
]
|
||||
},
|
||||
"GetTaskPayloadResult": {
|
||||
"description": "Response to a `tasks/result` request.\n\nPer spec, the result structure matches the original request type\n(e.g., `CallToolResult` for `tools/call`). This is represented as\nan open object. The payload is the original request's result\nserialized as a JSON value."
|
||||
},
|
||||
"GetTaskResult": {
|
||||
"description": "Response to a `tasks/get` request.\n\nPer spec, `GetTaskResult = allOf[Result, Task]` — the Task fields are\nflattened at the top level, not nested under a `task` key.",
|
||||
"description": "Response to a `tasks/get` request (spec `GetTaskResult = Result & DetailedTask`).\n\n`resultType` is `\"complete\"` — this is the standard result shape for\n`tasks/get`, not a task handle.",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"_meta": {
|
||||
|
|
@ -1217,15 +1168,31 @@
|
|||
]
|
||||
},
|
||||
"createdAt": {
|
||||
"description": "ISO-8601 creation timestamp.",
|
||||
"description": "ISO 8601 timestamp when the task was created.",
|
||||
"type": "string"
|
||||
},
|
||||
"error": {
|
||||
"type": [
|
||||
"object",
|
||||
"null"
|
||||
],
|
||||
"additionalProperties": true
|
||||
},
|
||||
"inputRequests": {
|
||||
"type": [
|
||||
"object",
|
||||
"null"
|
||||
],
|
||||
"additionalProperties": {
|
||||
"$ref": "#/definitions/InputRequest"
|
||||
}
|
||||
},
|
||||
"lastUpdatedAt": {
|
||||
"description": "ISO-8601 timestamp for the most recent status change.",
|
||||
"description": "ISO 8601 timestamp when the task was last updated.",
|
||||
"type": "string"
|
||||
},
|
||||
"pollInterval": {
|
||||
"description": "Suggested polling interval (milliseconds).",
|
||||
"pollIntervalMs": {
|
||||
"description": "Suggested polling interval in integer milliseconds. Clients SHOULD honor\nthis value to avoid overwhelming the server. This value MAY change over\nthe lifetime of a task.",
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
|
|
@ -1233,8 +1200,24 @@
|
|||
"format": "uint64",
|
||||
"minimum": 0
|
||||
},
|
||||
"result": {
|
||||
"type": [
|
||||
"object",
|
||||
"null"
|
||||
],
|
||||
"additionalProperties": true
|
||||
},
|
||||
"resultType": {
|
||||
"description": "Result type discriminator. `tasks/get` responses are standard results:\n`\"complete\"` (SEP-2322). Absent values deserialize as `\"complete\"`.",
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/ResultType"
|
||||
}
|
||||
],
|
||||
"default": "complete"
|
||||
},
|
||||
"status": {
|
||||
"description": "Current lifecycle status (see [`TaskStatus`]).",
|
||||
"description": "Current task status.",
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/TaskStatus"
|
||||
|
|
@ -1242,18 +1225,18 @@
|
|||
]
|
||||
},
|
||||
"statusMessage": {
|
||||
"description": "Optional human-readable status message for UI surfaces.",
|
||||
"description": "Optional message describing the current task state.\nThis MAY be exposed to the end-user or model.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"taskId": {
|
||||
"description": "Unique task identifier generated by the receiver.",
|
||||
"description": "Stable identifier for this task, generated by the server.",
|
||||
"type": "string"
|
||||
},
|
||||
"ttl": {
|
||||
"description": "Retention window in milliseconds that the receiver agreed to honor.\n`None` (serialized as `null`) means unlimited retention.",
|
||||
"ttlMs": {
|
||||
"description": "Time-to-live duration from creation in integer milliseconds; `None`\n(serialized as `null`) means unlimited. The server may discard the task\nafter the TTL elapses. This value MAY change over the lifetime of a task.",
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
|
|
@ -1945,36 +1928,6 @@
|
|||
"format": "const",
|
||||
"const": "roots/list"
|
||||
},
|
||||
"ListTasksResult": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"_meta": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/MetaObject"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"nextCursor": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"tasks": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/Task"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"tasks"
|
||||
]
|
||||
},
|
||||
"ListToolsResult": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
|
@ -2246,7 +2199,7 @@
|
|||
"$ref": "#/definitions/TaskStatusNotificationMethod"
|
||||
},
|
||||
"params": {
|
||||
"$ref": "#/definitions/TaskStatusNotificationParam"
|
||||
"$ref": "#/definitions/TaskStatusNotificationParams"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
|
|
@ -3191,19 +3144,6 @@
|
|||
}
|
||||
]
|
||||
},
|
||||
"SamplingTaskCapability": {
|
||||
"description": "Sampling task capability. Deprecated by SEP-2577; remains functional and\nwill be removed in a future release.\nSee <https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2577>.",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"createMessage": {
|
||||
"type": [
|
||||
"object",
|
||||
"null"
|
||||
],
|
||||
"additionalProperties": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"ServerCapabilities": {
|
||||
"title": "Builder",
|
||||
"description": "```rust\n# use rmcp::model::ServerCapabilities;\nlet cap = ServerCapabilities::builder()\n .enable_experimental()\n .enable_prompts()\n .enable_resources()\n .enable_tools()\n .enable_tool_list_changed()\n .build();\n```",
|
||||
|
|
@ -3265,16 +3205,6 @@
|
|||
}
|
||||
]
|
||||
},
|
||||
"tasks": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/TasksCapability"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"tools": {
|
||||
"anyOf": [
|
||||
{
|
||||
|
|
@ -3325,15 +3255,9 @@
|
|||
{
|
||||
"$ref": "#/definitions/CreateTaskResult"
|
||||
},
|
||||
{
|
||||
"$ref": "#/definitions/ListTasksResult"
|
||||
},
|
||||
{
|
||||
"$ref": "#/definitions/GetTaskResult"
|
||||
},
|
||||
{
|
||||
"$ref": "#/definitions/CancelTaskResult"
|
||||
},
|
||||
{
|
||||
"$ref": "#/definitions/CallToolResult"
|
||||
},
|
||||
|
|
@ -3341,7 +3265,7 @@
|
|||
"$ref": "#/definitions/InputRequiredResult"
|
||||
},
|
||||
{
|
||||
"$ref": "#/definitions/GetTaskPayloadResult"
|
||||
"$ref": "#/definitions/TaskAckResult"
|
||||
},
|
||||
{
|
||||
"$ref": "#/definitions/EmptyObject"
|
||||
|
|
@ -3528,138 +3452,58 @@
|
|||
"io.modelcontextprotocol/subscriptionId"
|
||||
]
|
||||
},
|
||||
"Task": {
|
||||
"description": "Primary Task object that surfaces metadata during the task lifecycle.\n\nPer spec, `lastUpdatedAt` and `ttl` are required fields.\n`ttl` is nullable (`null` means unlimited retention).",
|
||||
"TaskAckResult": {
|
||||
"description": "Empty acknowledgement for `tasks/update` and `tasks/cancel` (SEP-2663).\n\nThe spec requires these acks to be empty results carrying the SEP-2322\n`resultType: \"complete\"` discriminator; task state changes are observed\nvia the next `tasks/get`.",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"createdAt": {
|
||||
"description": "ISO-8601 creation timestamp.",
|
||||
"type": "string"
|
||||
},
|
||||
"lastUpdatedAt": {
|
||||
"description": "ISO-8601 timestamp for the most recent status change.",
|
||||
"type": "string"
|
||||
},
|
||||
"pollInterval": {
|
||||
"description": "Suggested polling interval (milliseconds).",
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
],
|
||||
"format": "uint64",
|
||||
"minimum": 0
|
||||
},
|
||||
"status": {
|
||||
"description": "Current lifecycle status (see [`TaskStatus`]).",
|
||||
"allOf": [
|
||||
"_meta": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/TaskStatus"
|
||||
"$ref": "#/definitions/MetaObject"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"statusMessage": {
|
||||
"description": "Optional human-readable status message for UI surfaces.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
"resultType": {
|
||||
"description": "Always `\"complete\"`.",
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/ResultType"
|
||||
}
|
||||
]
|
||||
},
|
||||
"taskId": {
|
||||
"description": "Unique task identifier generated by the receiver.",
|
||||
"type": "string"
|
||||
},
|
||||
"ttl": {
|
||||
"description": "Retention window in milliseconds that the receiver agreed to honor.\n`None` (serialized as `null`) means unlimited retention.",
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
],
|
||||
"format": "uint64",
|
||||
"minimum": 0
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"taskId",
|
||||
"status",
|
||||
"createdAt",
|
||||
"lastUpdatedAt"
|
||||
"resultType"
|
||||
]
|
||||
},
|
||||
"TaskMetadata": {
|
||||
"description": "Metadata for augmenting a request with task execution (spec `TaskMetadata`).",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"ttl": {
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
],
|
||||
"format": "uint64",
|
||||
"minimum": 0
|
||||
}
|
||||
}
|
||||
},
|
||||
"TaskRequestsCapability": {
|
||||
"description": "Request types that support task-augmented execution.",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"elicitation": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/ElicitationTaskCapability"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"sampling": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/SamplingTaskCapability"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"tools": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/ToolsTaskCapability"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"TaskStatus": {
|
||||
"description": "Canonical task lifecycle status as defined by SEP-1686.",
|
||||
"description": "Canonical task lifecycle status (SEP-2663).",
|
||||
"oneOf": [
|
||||
{
|
||||
"description": "The receiver accepted the request and is currently working on it.",
|
||||
"description": "The request is currently being processed.",
|
||||
"type": "string",
|
||||
"const": "working"
|
||||
},
|
||||
{
|
||||
"description": "The receiver requires additional input before work can continue.",
|
||||
"description": "The server needs input from the client before the task can proceed.",
|
||||
"type": "string",
|
||||
"const": "input_required"
|
||||
},
|
||||
{
|
||||
"description": "The underlying operation completed successfully and the result is ready.",
|
||||
"description": "The request completed successfully and the result is available.\nThis includes tool calls that returned results with `isError: true`.",
|
||||
"type": "string",
|
||||
"const": "completed"
|
||||
},
|
||||
{
|
||||
"description": "The underlying operation failed and will not continue.",
|
||||
"description": "The request failed due to a JSON-RPC error during execution.",
|
||||
"type": "string",
|
||||
"const": "failed"
|
||||
},
|
||||
{
|
||||
"description": "The task was cancelled and will not continue processing.",
|
||||
"description": "The request was cancelled before completion.",
|
||||
"type": "string",
|
||||
"const": "cancelled"
|
||||
}
|
||||
|
|
@ -3668,10 +3512,10 @@
|
|||
"TaskStatusNotificationMethod": {
|
||||
"type": "string",
|
||||
"format": "const",
|
||||
"const": "notifications/tasks/status"
|
||||
"const": "notifications/tasks"
|
||||
},
|
||||
"TaskStatusNotificationParam": {
|
||||
"description": "Parameters for a task status notification (spec `TaskStatusNotificationParams`).\n\nThe task fields are flattened at the top level: `NotificationParams & Task`.",
|
||||
"TaskStatusNotificationParams": {
|
||||
"description": "Parameters for a task status notification (spec `TaskStatusNotificationParams`).\n\nCarries a complete [`DetailedTask`] for the current status, identical to\nwhat `tasks/get` would have returned at that moment. The task fields are\nflattened at the top level: `NotificationParams & Task`.",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"_meta": {
|
||||
|
|
@ -3685,15 +3529,31 @@
|
|||
]
|
||||
},
|
||||
"createdAt": {
|
||||
"description": "ISO-8601 creation timestamp.",
|
||||
"description": "ISO 8601 timestamp when the task was created.",
|
||||
"type": "string"
|
||||
},
|
||||
"error": {
|
||||
"type": [
|
||||
"object",
|
||||
"null"
|
||||
],
|
||||
"additionalProperties": true
|
||||
},
|
||||
"inputRequests": {
|
||||
"type": [
|
||||
"object",
|
||||
"null"
|
||||
],
|
||||
"additionalProperties": {
|
||||
"$ref": "#/definitions/InputRequest"
|
||||
}
|
||||
},
|
||||
"lastUpdatedAt": {
|
||||
"description": "ISO-8601 timestamp for the most recent status change.",
|
||||
"description": "ISO 8601 timestamp when the task was last updated.",
|
||||
"type": "string"
|
||||
},
|
||||
"pollInterval": {
|
||||
"description": "Suggested polling interval (milliseconds).",
|
||||
"pollIntervalMs": {
|
||||
"description": "Suggested polling interval in integer milliseconds. Clients SHOULD honor\nthis value to avoid overwhelming the server. This value MAY change over\nthe lifetime of a task.",
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
|
|
@ -3701,8 +3561,15 @@
|
|||
"format": "uint64",
|
||||
"minimum": 0
|
||||
},
|
||||
"result": {
|
||||
"type": [
|
||||
"object",
|
||||
"null"
|
||||
],
|
||||
"additionalProperties": true
|
||||
},
|
||||
"status": {
|
||||
"description": "Current lifecycle status (see [`TaskStatus`]).",
|
||||
"description": "Current task status.",
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/TaskStatus"
|
||||
|
|
@ -3710,18 +3577,18 @@
|
|||
]
|
||||
},
|
||||
"statusMessage": {
|
||||
"description": "Optional human-readable status message for UI surfaces.",
|
||||
"description": "Optional message describing the current task state.\nThis MAY be exposed to the end-user or model.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"taskId": {
|
||||
"description": "Unique task identifier generated by the receiver.",
|
||||
"description": "Stable identifier for this task, generated by the server.",
|
||||
"type": "string"
|
||||
},
|
||||
"ttl": {
|
||||
"description": "Retention window in milliseconds that the receiver agreed to honor.\n`None` (serialized as `null`) means unlimited retention.",
|
||||
"ttlMs": {
|
||||
"description": "Time-to-live duration from creation in integer milliseconds; `None`\n(serialized as `null`) means unlimited. The server may discard the task\nafter the TTL elapses. This value MAY change over the lifetime of a task.",
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
|
|
@ -3737,56 +3604,6 @@
|
|||
"lastUpdatedAt"
|
||||
]
|
||||
},
|
||||
"TaskSupport": {
|
||||
"description": "Per-tool task support mode as defined in the MCP specification.\n\nThis enum indicates whether a tool supports task-based invocation,\nallowing clients to know how to properly call the tool.\n\nSee [Tool-Level Negotiation](https://modelcontextprotocol.io/specification/2025-11-25/basic/utilities/tasks#tool-level-negotiation).",
|
||||
"oneOf": [
|
||||
{
|
||||
"description": "Clients MUST NOT invoke this tool as a task (default behavior).",
|
||||
"type": "string",
|
||||
"const": "forbidden"
|
||||
},
|
||||
{
|
||||
"description": "Clients MAY invoke this tool as either a task or a normal call.",
|
||||
"type": "string",
|
||||
"const": "optional"
|
||||
},
|
||||
{
|
||||
"description": "Clients MUST invoke this tool as a task.",
|
||||
"type": "string",
|
||||
"const": "required"
|
||||
}
|
||||
]
|
||||
},
|
||||
"TasksCapability": {
|
||||
"description": "Task capabilities shared by client and server.",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"cancel": {
|
||||
"type": [
|
||||
"object",
|
||||
"null"
|
||||
],
|
||||
"additionalProperties": true
|
||||
},
|
||||
"list": {
|
||||
"type": [
|
||||
"object",
|
||||
"null"
|
||||
],
|
||||
"additionalProperties": true
|
||||
},
|
||||
"requests": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/TaskRequestsCapability"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"TextContent": {
|
||||
"description": "Text content block (spec `TextContent`).",
|
||||
"type": "object",
|
||||
|
|
@ -3960,17 +3777,6 @@
|
|||
"null"
|
||||
]
|
||||
},
|
||||
"execution": {
|
||||
"description": "Execution-related configuration including task support mode.",
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/ToolExecution"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"icons": {
|
||||
"description": "Optional list of icons for the tool",
|
||||
"type": [
|
||||
|
|
@ -4089,23 +3895,6 @@
|
|||
}
|
||||
]
|
||||
},
|
||||
"ToolExecution": {
|
||||
"description": "Execution-related configuration for a tool.\n\nThis struct contains settings that control how a tool should be executed,\nincluding task support configuration.",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"taskSupport": {
|
||||
"description": "Indicates whether this tool supports task-based invocation.\n\nWhen not present or set to `Forbidden`, clients MUST NOT invoke this tool as a task.\nWhen set to `Optional`, clients MAY invoke this tool as a task or normal call.\nWhen set to `Required`, clients MUST invoke this tool as a task.",
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/TaskSupport"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"ToolListChangedNotificationMethod": {
|
||||
"type": "string",
|
||||
"format": "const",
|
||||
|
|
@ -4191,18 +3980,6 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"ToolsTaskCapability": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"call": {
|
||||
"type": [
|
||||
"object",
|
||||
"null"
|
||||
],
|
||||
"additionalProperties": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"UntitledItems": {
|
||||
"description": "Items for untitled multi-select options",
|
||||
"type": "object",
|
||||
|
|
|
|||
|
|
@ -1,119 +1,423 @@
|
|||
use std::{any::Any, time::Duration};
|
||||
//! End-to-end tests for the MCP Tasks extension (SEP-2663,
|
||||
//! `io.modelcontextprotocol/tasks`).
|
||||
#![cfg(all(feature = "server", feature = "client", not(feature = "local")))]
|
||||
|
||||
use rmcp::{
|
||||
model::TaskStatusNotificationParam,
|
||||
task_manager::{
|
||||
OperationDescriptor, OperationMessage, OperationProcessor, OperationResultTransport,
|
||||
},
|
||||
ErrorData as McpError, ServerHandler, ServiceExt,
|
||||
handler::server::{router::tool::ToolRouter, wrapper::Parameters},
|
||||
model::*,
|
||||
service::{RequestContext, RoleServer},
|
||||
task_manager::{TaskExit, TaskManager, TaskOptions},
|
||||
tool, tool_router,
|
||||
};
|
||||
use serde_json::json;
|
||||
|
||||
struct DummyTransport {
|
||||
id: String,
|
||||
value: u32,
|
||||
#[derive(Debug, serde::Deserialize, rmcp::schemars::JsonSchema)]
|
||||
pub struct SumArgs {
|
||||
pub a: i32,
|
||||
pub b: i32,
|
||||
}
|
||||
|
||||
impl OperationResultTransport for DummyTransport {
|
||||
fn operation_id(&self) -> &String {
|
||||
&self.id
|
||||
#[derive(Clone)]
|
||||
struct TaskServer {
|
||||
tool_router: ToolRouter<TaskServer>,
|
||||
tasks: TaskManager,
|
||||
}
|
||||
|
||||
#[tool_router]
|
||||
impl TaskServer {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
tool_router: Self::tool_router(),
|
||||
tasks: TaskManager::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn as_any(&self) -> &dyn Any {
|
||||
self
|
||||
#[tool(description = "Sum two numbers")]
|
||||
async fn sum(
|
||||
&self,
|
||||
Parameters(SumArgs { a, b }): Parameters<SumArgs>,
|
||||
) -> Result<CallToolResult, McpError> {
|
||||
Ok(CallToolResult::success(vec![ContentBlock::text(
|
||||
(a + b).to_string(),
|
||||
)]))
|
||||
}
|
||||
}
|
||||
|
||||
impl ServerHandler for TaskServer {
|
||||
async fn call_tool(
|
||||
&self,
|
||||
request: CallToolRequestParams,
|
||||
context: RequestContext<RoleServer>,
|
||||
) -> Result<CallToolResponse, McpError> {
|
||||
let client_supports_tasks = context
|
||||
.client_capabilities()
|
||||
.is_some_and(|caps| caps.supports_tasks());
|
||||
|
||||
if request.name == "sum" && client_supports_tasks {
|
||||
let args: SumArgs = serde_json::from_value(serde_json::Value::Object(
|
||||
request.arguments.clone().unwrap_or_default(),
|
||||
))
|
||||
.map_err(|e| McpError::invalid_params(e.to_string(), None))?;
|
||||
let task = self
|
||||
.tasks
|
||||
.spawn(TaskOptions::new().with_poll_interval_ms(10), move |ctx| {
|
||||
Box::pin(async move {
|
||||
tokio::select! {
|
||||
_ = ctx.cancelled() => {
|
||||
Err(TaskExit::Cancelled)
|
||||
}
|
||||
_ = tokio::time::sleep(std::time::Duration::from_millis(50)) => {
|
||||
Ok(CallToolResult::success(vec![ContentBlock::text(
|
||||
(args.a + args.b).to_string(),
|
||||
)]))
|
||||
}
|
||||
}
|
||||
})
|
||||
});
|
||||
return Ok(CallToolResponse::Task(CreateTaskResult::new(task)));
|
||||
}
|
||||
|
||||
let tcc = rmcp::handler::server::tool::ToolCallContext::new(self, request, context);
|
||||
self.tool_router.call(tcc).await
|
||||
}
|
||||
|
||||
async fn get_task(
|
||||
&self,
|
||||
request: GetTaskParams,
|
||||
_context: RequestContext<RoleServer>,
|
||||
) -> Result<GetTaskResult, McpError> {
|
||||
Ok(GetTaskResult::new(self.tasks.get_task(&request.task_id)?))
|
||||
}
|
||||
|
||||
async fn update_task(
|
||||
&self,
|
||||
request: UpdateTaskParams,
|
||||
_context: RequestContext<RoleServer>,
|
||||
) -> Result<(), McpError> {
|
||||
self.tasks
|
||||
.update_task(&request.task_id, request.input_responses)
|
||||
}
|
||||
|
||||
async fn cancel_task(
|
||||
&self,
|
||||
request: CancelTaskParams,
|
||||
_context: RequestContext<RoleServer>,
|
||||
) -> Result<(), McpError> {
|
||||
self.tasks.cancel_task(&request.task_id)
|
||||
}
|
||||
|
||||
fn get_info(&self) -> ServerInfo {
|
||||
ServerInfo::new(
|
||||
ServerCapabilities::builder()
|
||||
.enable_tools()
|
||||
.enable_tasks()
|
||||
.build(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fn tasks_client_info() -> ClientInfo {
|
||||
ClientInfo::new(
|
||||
ClientCapabilities::builder().enable_tasks().build(),
|
||||
Implementation::from_build_env(),
|
||||
)
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn executes_enqueued_future() {
|
||||
let mut processor = OperationProcessor::new();
|
||||
let descriptor = OperationDescriptor::new("op1", "dummy");
|
||||
let future = Box::pin(async {
|
||||
tokio::time::sleep(Duration::from_millis(10)).await;
|
||||
Ok(Box::new(DummyTransport {
|
||||
id: "op1".to_string(),
|
||||
value: 42,
|
||||
}) as Box<dyn OperationResultTransport>)
|
||||
async fn task_lifecycle_create_poll_complete() {
|
||||
let (server_transport, client_transport) = tokio::io::duplex(4096);
|
||||
let server = tokio::spawn(async move {
|
||||
let service = TaskServer::new().serve(server_transport).await?;
|
||||
service.waiting().await?;
|
||||
anyhow::Ok(())
|
||||
});
|
||||
|
||||
processor
|
||||
.submit_operation(OperationMessage::new(descriptor, future))
|
||||
.expect("submit operation");
|
||||
let client = tasks_client_info().serve(client_transport).await.unwrap();
|
||||
|
||||
tokio::time::sleep(Duration::from_millis(30)).await;
|
||||
let results = processor.peek_completed();
|
||||
assert_eq!(results.len(), 1);
|
||||
let payload = results[0]
|
||||
.result
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.as_any()
|
||||
.downcast_ref::<DummyTransport>()
|
||||
// Server materializes a task because we declared the extension.
|
||||
let response = client
|
||||
.call_tool_once(
|
||||
CallToolRequestParams::new("sum")
|
||||
.with_arguments(serde_json::from_value(json!({"a": 40, "b": 2})).unwrap()),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(payload.value, 42);
|
||||
}
|
||||
let create = match response {
|
||||
CallToolResponse::Task(create) => create,
|
||||
other => panic!("expected CreateTaskResult, got {other:?}"),
|
||||
};
|
||||
assert_eq!(create.result_type, ResultType::TASK);
|
||||
let task_id = create.task.task_id.clone();
|
||||
|
||||
#[tokio::test]
|
||||
async fn rejects_duplicate_operation_ids() {
|
||||
let mut processor = OperationProcessor::new();
|
||||
let descriptor = OperationDescriptor::new("dup", "dummy");
|
||||
let future = Box::pin(async {
|
||||
Ok(Box::new(DummyTransport {
|
||||
id: "dup".to_string(),
|
||||
value: 1,
|
||||
}) as Box<dyn OperationResultTransport>)
|
||||
});
|
||||
processor
|
||||
.submit_operation(OperationMessage::new(descriptor, future))
|
||||
.expect("first submit");
|
||||
// Poll until terminal.
|
||||
let final_task = loop {
|
||||
tokio::time::sleep(std::time::Duration::from_millis(
|
||||
create.task.poll_interval_ms.unwrap_or(10),
|
||||
))
|
||||
.await;
|
||||
let info = client
|
||||
.peer()
|
||||
.get_task(GetTaskParams::new(task_id.clone()))
|
||||
.await
|
||||
.unwrap();
|
||||
if info.task.status().is_terminal() {
|
||||
break info.task;
|
||||
}
|
||||
};
|
||||
|
||||
let descriptor_dup = OperationDescriptor::new("dup", "dummy");
|
||||
let future_dup = Box::pin(async {
|
||||
Ok(Box::new(DummyTransport {
|
||||
id: "dup".to_string(),
|
||||
value: 2,
|
||||
}) as Box<dyn OperationResultTransport>)
|
||||
});
|
||||
|
||||
let err = processor
|
||||
.submit_operation(OperationMessage::new(descriptor_dup, future_dup))
|
||||
.expect_err("duplicate should fail");
|
||||
assert!(format!("{err}").contains("already running"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn ttl_is_interpreted_as_milliseconds() {
|
||||
let mut processor = OperationProcessor::new();
|
||||
let descriptor = OperationDescriptor::new("slow", "dummy").with_ttl(50);
|
||||
let future = Box::pin(async {
|
||||
tokio::time::sleep(Duration::from_millis(500)).await;
|
||||
Ok(Box::new(DummyTransport {
|
||||
id: "slow".to_string(),
|
||||
value: 0,
|
||||
}) as Box<dyn OperationResultTransport>)
|
||||
});
|
||||
|
||||
processor
|
||||
.submit_operation(OperationMessage::new(descriptor, future))
|
||||
.expect("submit operation");
|
||||
|
||||
tokio::time::sleep(Duration::from_millis(200)).await;
|
||||
let results = processor.peek_completed();
|
||||
assert_eq!(
|
||||
results.len(),
|
||||
1,
|
||||
"50ms ttl should have timed out the operation well within 200ms"
|
||||
);
|
||||
match &results[0].result {
|
||||
Err(err) => assert!(
|
||||
err.to_string().contains("timed out"),
|
||||
"unexpected error: {err}"
|
||||
),
|
||||
Ok(_) => panic!("expected the operation to time out, but it completed"),
|
||||
match final_task.payload {
|
||||
TaskPayload::Completed { result } => {
|
||||
let result: CallToolResult =
|
||||
serde_json::from_value(serde_json::Value::Object(result)).unwrap();
|
||||
let text = result.content[0].as_text().unwrap();
|
||||
assert_eq!(text.text, "42");
|
||||
}
|
||||
other => panic!("expected completed task, got {other:?}"),
|
||||
}
|
||||
|
||||
client.cancel().await.unwrap();
|
||||
server.abort();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn task_cancel_acknowledged() {
|
||||
let (server_transport, client_transport) = tokio::io::duplex(4096);
|
||||
let server = tokio::spawn(async move {
|
||||
let service = TaskServer::new().serve(server_transport).await?;
|
||||
service.waiting().await?;
|
||||
anyhow::Ok(())
|
||||
});
|
||||
|
||||
let client = tasks_client_info().serve(client_transport).await.unwrap();
|
||||
|
||||
let response = client
|
||||
.call_tool_once(
|
||||
CallToolRequestParams::new("sum")
|
||||
.with_arguments(serde_json::from_value(json!({"a": 1, "b": 1})).unwrap()),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
let create = match response {
|
||||
CallToolResponse::Task(create) => create,
|
||||
other => panic!("expected CreateTaskResult, got {other:?}"),
|
||||
};
|
||||
|
||||
client
|
||||
.peer()
|
||||
.cancel_task(CancelTaskParams::new(create.task.task_id.clone()))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Cancellation is cooperative (SEP-2663): the ack is immediate, and the
|
||||
// operation settles the terminal status; poll until it does.
|
||||
let mut final_status = None;
|
||||
for _ in 0..100 {
|
||||
tokio::time::sleep(std::time::Duration::from_millis(10)).await;
|
||||
let info = client
|
||||
.peer()
|
||||
.get_task(GetTaskParams::new(create.task.task_id.clone()))
|
||||
.await
|
||||
.unwrap();
|
||||
if info.task.status().is_terminal() {
|
||||
final_status = Some(info.task.status());
|
||||
break;
|
||||
}
|
||||
}
|
||||
assert_eq!(final_status, Some(TaskStatus::Cancelled));
|
||||
|
||||
client.cancel().await.unwrap();
|
||||
server.abort();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn no_task_without_extension_capability() {
|
||||
let (server_transport, client_transport) = tokio::io::duplex(4096);
|
||||
let server = tokio::spawn(async move {
|
||||
let service = TaskServer::new().serve(server_transport).await?;
|
||||
service.waiting().await?;
|
||||
anyhow::Ok(())
|
||||
});
|
||||
|
||||
// Plain client: no tasks extension declared.
|
||||
let client = ().serve(client_transport).await.unwrap();
|
||||
let result = client
|
||||
.call_tool(
|
||||
CallToolRequestParams::new("sum")
|
||||
.with_arguments(serde_json::from_value(json!({"a": 2, "b": 3})).unwrap()),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
let text = result.content[0].as_text().unwrap();
|
||||
assert_eq!(text.text, "5");
|
||||
|
||||
client.cancel().await.unwrap();
|
||||
server.abort();
|
||||
}
|
||||
|
||||
/// A misbehaving handler that materializes a task without checking the
|
||||
/// client's capabilities. The SDK dispatch must catch this and reject with
|
||||
/// -32021 rather than sending a task handle the client cannot parse.
|
||||
#[derive(Clone)]
|
||||
struct AlwaysTaskServer {
|
||||
tasks: TaskManager,
|
||||
}
|
||||
|
||||
impl ServerHandler for AlwaysTaskServer {
|
||||
async fn call_tool(
|
||||
&self,
|
||||
_request: CallToolRequestParams,
|
||||
_context: RequestContext<RoleServer>,
|
||||
) -> Result<CallToolResponse, McpError> {
|
||||
let task = self.tasks.spawn(TaskOptions::default(), |_ctx| {
|
||||
Box::pin(async { Ok(CallToolResult::success(vec![ContentBlock::text("late")])) })
|
||||
});
|
||||
Ok(CallToolResponse::Task(CreateTaskResult::new(task)))
|
||||
}
|
||||
|
||||
fn get_info(&self) -> ServerInfo {
|
||||
ServerInfo::new(
|
||||
ServerCapabilities::builder()
|
||||
.enable_tools()
|
||||
.enable_tasks()
|
||||
.build(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn dispatch_rejects_task_result_for_non_declaring_client() {
|
||||
let (server_transport, client_transport) = tokio::io::duplex(4096);
|
||||
let server = tokio::spawn(async move {
|
||||
let service = AlwaysTaskServer {
|
||||
tasks: TaskManager::new(),
|
||||
}
|
||||
.serve(server_transport)
|
||||
.await?;
|
||||
service.waiting().await?;
|
||||
anyhow::Ok(())
|
||||
});
|
||||
|
||||
// Plain client: no tasks extension declared. The handler tries to return
|
||||
// a CreateTaskResult anyway; dispatch must reject with -32021.
|
||||
let client = ().serve(client_transport).await.unwrap();
|
||||
let err = client
|
||||
.call_tool(CallToolRequestParams::new("anything"))
|
||||
.await
|
||||
.unwrap_err();
|
||||
match err {
|
||||
rmcp::ServiceError::McpError(e) => {
|
||||
assert_eq!(e.code, ErrorCode::MISSING_REQUIRED_CLIENT_CAPABILITY);
|
||||
}
|
||||
other => panic!("expected McpError, got {other:?}"),
|
||||
}
|
||||
|
||||
client.cancel().await.unwrap();
|
||||
server.abort();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn tasks_methods_without_capability_return_missing_capability_error() {
|
||||
let (server_transport, client_transport) = tokio::io::duplex(4096);
|
||||
let server = tokio::spawn(async move {
|
||||
let service = TaskServer::new().serve(server_transport).await?;
|
||||
service.waiting().await?;
|
||||
anyhow::Ok(())
|
||||
});
|
||||
|
||||
// Plain client: no tasks extension declared. tasks/* must be rejected
|
||||
// with -32021 Missing Required Client Capability (SEP-2663), not -32601.
|
||||
let client = ().serve(client_transport).await.unwrap();
|
||||
let err = client
|
||||
.peer()
|
||||
.get_task(GetTaskParams::new("whatever"))
|
||||
.await
|
||||
.unwrap_err();
|
||||
match err {
|
||||
rmcp::ServiceError::McpError(e) => {
|
||||
assert_eq!(e.code, ErrorCode::MISSING_REQUIRED_CLIENT_CAPABILITY);
|
||||
let data = e.data.expect("error data should be present");
|
||||
assert!(
|
||||
data["requiredCapabilities"]["extensions"]
|
||||
.as_object()
|
||||
.is_some_and(|ext| ext.contains_key("io.modelcontextprotocol/tasks")),
|
||||
"error data should name the tasks extension: {data}"
|
||||
);
|
||||
}
|
||||
other => panic!("expected McpError, got {other:?}"),
|
||||
}
|
||||
|
||||
let err = client
|
||||
.peer()
|
||||
.cancel_task(CancelTaskParams::new("whatever"))
|
||||
.await
|
||||
.unwrap_err();
|
||||
match err {
|
||||
rmcp::ServiceError::McpError(e) => {
|
||||
assert_eq!(e.code, ErrorCode::MISSING_REQUIRED_CLIENT_CAPABILITY);
|
||||
}
|
||||
other => panic!("expected McpError, got {other:?}"),
|
||||
}
|
||||
|
||||
client.cancel().await.unwrap();
|
||||
server.abort();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn unknown_task_id_returns_invalid_params() {
|
||||
let (server_transport, client_transport) = tokio::io::duplex(4096);
|
||||
let server = tokio::spawn(async move {
|
||||
let service = TaskServer::new().serve(server_transport).await?;
|
||||
service.waiting().await?;
|
||||
anyhow::Ok(())
|
||||
});
|
||||
|
||||
let client = tasks_client_info().serve(client_transport).await.unwrap();
|
||||
let err = client
|
||||
.peer()
|
||||
.get_task(GetTaskParams::new("no-such-task"))
|
||||
.await
|
||||
.unwrap_err();
|
||||
match err {
|
||||
rmcp::ServiceError::McpError(e) => {
|
||||
// SEP-2663: unknown taskId is -32602 Invalid params.
|
||||
assert_eq!(e.code, ErrorCode::INVALID_PARAMS);
|
||||
}
|
||||
other => panic!("expected McpError, got {other:?}"),
|
||||
}
|
||||
|
||||
client.cancel().await.unwrap();
|
||||
server.abort();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn legacy_task_param_is_ignored() {
|
||||
let (server_transport, client_transport) = tokio::io::duplex(4096);
|
||||
let server = tokio::spawn(async move {
|
||||
let service = TaskServer::new().serve(server_transport).await?;
|
||||
service.waiting().await?;
|
||||
anyhow::Ok(())
|
||||
});
|
||||
|
||||
// Plain client sending a legacy 2025-11-25 `task: {...}` param: it must
|
||||
// be silently ignored and the call answered synchronously (SEP-2663).
|
||||
let client = ().serve(client_transport).await.unwrap();
|
||||
let params: CallToolRequestParams = serde_json::from_value(json!({
|
||||
"name": "sum",
|
||||
"arguments": {"a": 2, "b": 3},
|
||||
"task": {"ttl": 60000}
|
||||
}))
|
||||
.expect("legacy task param must not break deserialization");
|
||||
let result = client.call_tool(params).await.unwrap();
|
||||
let text = result.content[0].as_text().unwrap();
|
||||
assert_eq!(text.text, "5");
|
||||
|
||||
client.cancel().await.unwrap();
|
||||
server.abort();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn task_status_notification_param_preserves_meta() {
|
||||
fn task_status_notification_params_preserve_meta() {
|
||||
let raw = json!({
|
||||
"_meta": {
|
||||
"traceId": "trace-1"
|
||||
|
|
@ -122,17 +426,17 @@ fn task_status_notification_param_preserves_meta() {
|
|||
"status": "working",
|
||||
"createdAt": "2026-06-24T00:00:00Z",
|
||||
"lastUpdatedAt": "2026-06-24T00:00:01Z",
|
||||
"ttl": null
|
||||
"ttlMs": null
|
||||
});
|
||||
|
||||
let params: TaskStatusNotificationParam = serde_json::from_value(raw).unwrap();
|
||||
let params: TaskStatusNotificationParams = serde_json::from_value(raw).unwrap();
|
||||
|
||||
assert_eq!(params.task.task_id, "task-1");
|
||||
assert_eq!(params.task_id, "task-1");
|
||||
assert_eq!(params.task.task.task_id, "task-1");
|
||||
assert_eq!(params.status(), TaskStatus::Working);
|
||||
assert_eq!(params.meta.as_ref().unwrap().0["traceId"], json!("trace-1"));
|
||||
|
||||
let serialized = serde_json::to_value(¶ms).unwrap();
|
||||
|
||||
assert_eq!(serialized["_meta"]["traceId"], json!("trace-1"));
|
||||
assert_eq!(serialized["taskId"], json!("task-1"));
|
||||
assert_eq!(serialized["ttlMs"], serde_json::Value::Null);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,251 +0,0 @@
|
|||
#![cfg(not(feature = "local"))]
|
||||
//! Tests for task support validation in tool calls.
|
||||
//!
|
||||
//! Verifies that the server correctly validates `execution.taskSupport` settings
|
||||
//! per the MCP specification:
|
||||
//! - `Required`: MUST be invoked as a task, returns -32601 otherwise
|
||||
//! - `Forbidden`: MUST NOT be invoked as a task, returns error otherwise
|
||||
//! - `Optional`: MAY be invoked either way
|
||||
#![cfg(feature = "client")]
|
||||
|
||||
use rmcp::{
|
||||
ClientHandler, ServerHandler, ServiceError, ServiceExt,
|
||||
handler::server::router::tool::ToolRouter,
|
||||
model::{CallToolRequestParams, ClientInfo, ErrorCode, TaskMetadata},
|
||||
tool, tool_handler, tool_router,
|
||||
};
|
||||
|
||||
/// Server with tools having different task support modes.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct TaskSupportTestServer {
|
||||
#[expect(dead_code, reason = "tool_handler macro accesses this router field")]
|
||||
tool_router: ToolRouter<Self>,
|
||||
}
|
||||
|
||||
impl Default for TaskSupportTestServer {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl TaskSupportTestServer {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
tool_router: Self::tool_router(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[tool_router]
|
||||
impl TaskSupportTestServer {
|
||||
#[tool(
|
||||
description = "Tool that requires task-based invocation",
|
||||
execution(task_support = "required")
|
||||
)]
|
||||
async fn required_task_tool(&self) -> String {
|
||||
"required task executed".to_string()
|
||||
}
|
||||
|
||||
#[tool(
|
||||
description = "Tool that forbids task-based invocation",
|
||||
execution(task_support = "forbidden")
|
||||
)]
|
||||
async fn forbidden_task_tool(&self) -> String {
|
||||
"forbidden task executed".to_string()
|
||||
}
|
||||
|
||||
#[tool(
|
||||
description = "Tool that optionally supports task-based invocation",
|
||||
execution(task_support = "optional")
|
||||
)]
|
||||
async fn optional_task_tool(&self) -> String {
|
||||
"optional task executed".to_string()
|
||||
}
|
||||
}
|
||||
|
||||
#[tool_handler]
|
||||
impl ServerHandler for TaskSupportTestServer {}
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
struct DummyClientHandler {}
|
||||
|
||||
impl ClientHandler for DummyClientHandler {
|
||||
fn get_info(&self) -> ClientInfo {
|
||||
ClientInfo::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// Helper to create a task object for tool calls
|
||||
fn make_task() -> TaskMetadata {
|
||||
TaskMetadata::new()
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_required_task_tool_without_task_returns_method_not_found() -> anyhow::Result<()> {
|
||||
let (server_transport, client_transport) = tokio::io::duplex(4096);
|
||||
|
||||
let server = TaskSupportTestServer::new();
|
||||
let server_handle = tokio::spawn(async move {
|
||||
server.serve(server_transport).await?.waiting().await?;
|
||||
anyhow::Ok(())
|
||||
});
|
||||
|
||||
let client_handler = DummyClientHandler::default();
|
||||
let client = client_handler.serve(client_transport).await?;
|
||||
|
||||
// Call the task-required tool without a task - should fail with -32601
|
||||
let result = client
|
||||
.call_tool(CallToolRequestParams::new("required_task_tool"))
|
||||
.await;
|
||||
|
||||
// Should be an error with code -32601 (METHOD_NOT_FOUND)
|
||||
assert!(
|
||||
result.is_err(),
|
||||
"Expected error for required task tool without task"
|
||||
);
|
||||
let error = result.unwrap_err();
|
||||
|
||||
// Check the error data contains the expected code
|
||||
match error {
|
||||
ServiceError::McpError(error_data) => {
|
||||
assert_eq!(
|
||||
error_data.code,
|
||||
ErrorCode::METHOD_NOT_FOUND,
|
||||
"Expected METHOD_NOT_FOUND error code (-32601)"
|
||||
);
|
||||
assert!(
|
||||
error_data
|
||||
.message
|
||||
.contains("requires task-based invocation"),
|
||||
"Error message should indicate task-based invocation is required, got: {}",
|
||||
error_data.message
|
||||
);
|
||||
}
|
||||
_ => panic!("Expected McpError variant, got: {:?}", error),
|
||||
}
|
||||
|
||||
client.cancel().await?;
|
||||
server_handle.await??;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_forbidden_task_tool_with_task_returns_error() -> anyhow::Result<()> {
|
||||
let (server_transport, client_transport) = tokio::io::duplex(4096);
|
||||
|
||||
let server = TaskSupportTestServer::new();
|
||||
let server_handle = tokio::spawn(async move {
|
||||
server.serve(server_transport).await?.waiting().await?;
|
||||
anyhow::Ok(())
|
||||
});
|
||||
|
||||
let client_handler = DummyClientHandler::default();
|
||||
let client = client_handler.serve(client_transport).await?;
|
||||
|
||||
// Call the forbidden task tool WITH a task - should fail
|
||||
let result = client
|
||||
.call_tool(CallToolRequestParams::new("forbidden_task_tool").with_task(make_task()))
|
||||
.await;
|
||||
|
||||
// Should be an error with code INVALID_PARAMS
|
||||
assert!(
|
||||
result.is_err(),
|
||||
"Expected error for forbidden task tool with task"
|
||||
);
|
||||
let error = result.unwrap_err();
|
||||
|
||||
// Check the error data contains the expected code
|
||||
match error {
|
||||
ServiceError::McpError(error_data) => {
|
||||
assert_eq!(
|
||||
error_data.code,
|
||||
ErrorCode::INVALID_PARAMS,
|
||||
"Expected INVALID_PARAMS error code"
|
||||
);
|
||||
assert!(
|
||||
error_data
|
||||
.message
|
||||
.contains("does not support task-based invocation"),
|
||||
"Error message should indicate task-based invocation is not supported, got: {}",
|
||||
error_data.message
|
||||
);
|
||||
}
|
||||
_ => panic!("Expected McpError variant, got: {:?}", error),
|
||||
}
|
||||
|
||||
client.cancel().await?;
|
||||
server_handle.await??;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_forbidden_task_tool_without_task_succeeds() -> anyhow::Result<()> {
|
||||
let (server_transport, client_transport) = tokio::io::duplex(4096);
|
||||
|
||||
let server = TaskSupportTestServer::new();
|
||||
let server_handle = tokio::spawn(async move {
|
||||
server.serve(server_transport).await?.waiting().await?;
|
||||
anyhow::Ok(())
|
||||
});
|
||||
|
||||
let client_handler = DummyClientHandler::default();
|
||||
let client = client_handler.serve(client_transport).await?;
|
||||
|
||||
// Call the forbidden task tool WITHOUT a task - should succeed
|
||||
let result = client
|
||||
.call_tool(CallToolRequestParams::new("forbidden_task_tool"))
|
||||
.await;
|
||||
|
||||
assert!(
|
||||
result.is_ok(),
|
||||
"Forbidden task tool without task should succeed"
|
||||
);
|
||||
let result = result.unwrap();
|
||||
let text = result
|
||||
.content
|
||||
.first()
|
||||
.and_then(|c| c.as_text())
|
||||
.map(|t| t.text.as_str())
|
||||
.unwrap_or("");
|
||||
assert_eq!(text, "forbidden task executed");
|
||||
|
||||
client.cancel().await?;
|
||||
server_handle.await??;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_optional_task_tool_without_task_succeeds() -> anyhow::Result<()> {
|
||||
let (server_transport, client_transport) = tokio::io::duplex(4096);
|
||||
|
||||
let server = TaskSupportTestServer::new();
|
||||
let server_handle = tokio::spawn(async move {
|
||||
server.serve(server_transport).await?.waiting().await?;
|
||||
anyhow::Ok(())
|
||||
});
|
||||
|
||||
let client_handler = DummyClientHandler::default();
|
||||
let client = client_handler.serve(client_transport).await?;
|
||||
|
||||
// Call the optional task tool WITHOUT a task - should succeed
|
||||
let result = client
|
||||
.call_tool(CallToolRequestParams::new("optional_task_tool"))
|
||||
.await;
|
||||
|
||||
assert!(
|
||||
result.is_ok(),
|
||||
"Optional task tool without task should succeed"
|
||||
);
|
||||
let result = result.unwrap();
|
||||
let text = result
|
||||
.content
|
||||
.first()
|
||||
.and_then(|c| c.as_text())
|
||||
.map(|t| t.text.as_str())
|
||||
.unwrap_or("");
|
||||
assert_eq!(text, "optional task executed");
|
||||
|
||||
client.cancel().await?;
|
||||
server_handle.await??;
|
||||
Ok(())
|
||||
}
|
||||
|
|
@ -397,8 +397,8 @@ fn test_minimal_server_get_info_auto_generated() {
|
|||
"prompts should not be auto-enabled"
|
||||
);
|
||||
assert!(
|
||||
info.capabilities.tasks.is_none(),
|
||||
"tasks should not be auto-enabled"
|
||||
!info.capabilities.supports_tasks(),
|
||||
"tasks extension should not be auto-enabled"
|
||||
);
|
||||
assert!(
|
||||
!info.server_info.name.is_empty(),
|
||||
|
|
|
|||
Loading…
Reference in a new issue