Skip to content

Commit 1cd49af

Browse files
committed
feat(ext-tasks): server dispatch wiring — SEP-2663 runtime on the 2026 lane
Opt-in ext-tasks feature on turul-mcp-server (off by default per SEP-2133); dispatcher design recorded in ADR-028 (2026-06-12 entry): - .with_ext_tasks(store): advertises io.modelcontextprotocol/tasks in discover capabilities.extensions, registers tasks/get|update|cancel - .ext_task_tool(_required): task election per the upstream lifecycle — declared per-request extension → durable CreateTaskResult (UUIDv4 bearer-grade id, store written BEFORE the response) + spawned worker; undeclared → sync fallback (progressive enhancement) or -32003 with data.requiredCapabilities.extensions for required tools - MRTR bridge: a task tool returning McpError::InputRequired parks in input_required; tasks/update validates keys against outstanding requests (partial delivery keeps it parked) and resumes the worker with responses injected through the same session-extension keys as the sync retry leg — tool code identical under both execution models - tasks/cancel cooperative (aborts worker, drops waiters, acks terminal); notifications/tasks rides subscriptions/listen with taskIds honored iff the extension is advertised (capability-map keyed — no transport dependency on the ext crate), echoed in the ack - turul-mcp-ext-tasks: TaskStore trait + TaskState + InMemoryTaskStore (state machine enforced; no tokio in the public API) 9 wire e2e tests (ext_tasks_2026.rs; gates + CI steps added): discover advertisement, create→poll→completed, sync fallback, -32003 shape, input_required⇄update round trip incl. requestState replay, cancel, unknown-task -32602, UUIDv4 ids, taskIds-filtered listen delivery. Revert-and-fail recorded: neutralizing the capability check fails declared_call_returns_task_and_polls_to_completion with a sync result. Driver-doc Major-6 (SEP-2663) promoted to ✅ (G1 closed). Full ci-gates.sh all green.
1 parent deadbb4 commit 1cd49af

17 files changed

Lines changed: 1544 additions & 10 deletions

File tree

.github/workflows/ci.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,10 @@ jobs:
7272
run: cargo build -p streamable-http-client
7373
- name: Client-using examples (not in default-members)
7474
run: cargo build -p mrtr-elicitation-server --bins -p bilingual-fleet-client
75+
- name: Tasks extension (SEP-2663, opt-in feature)
76+
run: cargo test -p turul-mcp-server --no-default-features --features http,sse,ext-tasks --test ext_tasks_2026
77+
- name: Extension crates standalone
78+
run: cargo test -p turul-mcp-ext-tasks -p turul-mcp-ext-apps
7579

7680
opt-in-2025:
7781
name: Opt-in lane (2025-11-25)

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
2020
2121
### Added
2222

23+
- **Tasks-extension server runtime (2026-06-12, SEP-2663 — closes driver gap G1).** New opt-in `ext-tasks` feature on `turul-mcp-server` (off by default per SEP-2133): `.with_ext_tasks(store)` advertises `io.modelcontextprotocol/tasks` in `server/discover`'s `capabilities.extensions` and registers `tasks/get`/`tasks/update`/`tasks/cancel`; `.ext_task_tool(tool)` / `.ext_task_tool_required(tool)` mark tools for task election — a request whose per-request `_meta` `clientCapabilities.extensions` declares the extension gets a durable `CreateTaskResult` (UUIDv4 bearer-grade id, store written BEFORE the response) with a spawned worker; undeclared requests run synchronously (progressive enhancement) or, for `_required` tools, get `-32003` with the upstream `data.requiredCapabilities.extensions` shape. **MRTR bridge**: a task tool returning `McpError::InputRequired` parks its task in `input_required`; `tasks/update` validates response keys against outstanding requests (partial delivery keeps it parked) and resumes the worker with the responses injected through the same session-extension keys as the sync retry leg — tool code is identical under both execution models. `tasks/cancel` is cooperative (aborts the worker, drops input waiters, acks terminal tasks). `notifications/tasks` rides `subscriptions/listen`: the transport honors a `taskIds` filter iff the extension is advertised (keyed off the capability map — no transport dependency on the ext crate), echoes it in the ack, and delivers per-taskId. `turul-mcp-ext-tasks` gains the `TaskStore` trait + `TaskState` + `InMemoryTaskStore` (no tokio in the public API). 9 wire e2e tests (`ext_tasks_2026.rs`, wired into gates + CI); revert-and-fail recorded. Dispatcher design recorded in ADR-028 (2026-06-12 entry).
2324
- **`turul-mcp-ext-apps` 0.1.0 scaffold (2026-06-12, SEP-1865).** Spec-neutral extension crate binding the MCP-side Apps surface: extension identifier `io.modelcontextprotocol/ui` (the ADR-028 table's `/apps` guess corrected against upstream), client capability (`UiClientCapabilities.mimeTypes` + the `text/html;profile=mcp-app` HTML-views gate), tool `_meta.ui` (`UiToolMeta`: `resourceUri`, `visibility` model/app), and UI-resource `_meta.ui` (`UiResourceMeta`: CSP domain lists, sandbox permissions, dedicated origin, `prefersBorder`). The host↔view iframe protocol is deliberately not bound (app/host SDK scope). Vendored spec pinned at `modelcontextprotocol/ext-apps@ca1d2989`; 5 wire-shape compliance tests.
2425
- **Versioning/cancellation/elicitation P2 trio (2026-06-12).** (1) **VER-4**: the headerless-`initialize` rejection (400 + `-32001`) now carries `error.data.supported` naming this build's protocol versions — a true legacy client's only diagnostic; wire test `headerless_initialize_rejection_names_supported_versions`, red-phase recorded. (2) **PAT/G10**: dedicated `CancelledNotificationHandler` extracts `requestId` + `reason` from inbound `notifications/cancelled` into a structured log line ("Both parties SHOULD log cancellation reasons"); accepted-and-ignored semantics unchanged. (3) **CF/GAP-CF-8**: new `turul_mcp_builders::validate_elicit_content(schema, content)` validates elicited form content against the requesting schema (required/unknown keys, primitive types, string-length/numeric bounds, integer-ness, enum membership across the 2026 enum-union shapes; format assertions annotation-only by design) — central enforcement is impossible on the stateless lane (leg-1 schema not retained), so tools call it on the MRTR retry; wired into `mrtr-elicitation-server` and live-verified. Plus **BP-5** (COMPLIANCE.md §"Supported JSON Schema dialects" — the documentation the SHOULD asks for) and **UTIL/COMP-3** (relevance/fuzzy/rate-limit completion SHOULDs dispositioned: provider semantics + middleware rate limiting). Driver summary now 305 ✅ / 68 🟡 / 5 ❌ / 12 🧪 / 100 ➖ — the 5 remaining ❌ all carry recorded dispositions.
2526
- **`turul-mcp-ext-tasks` 0.1.0 scaffold (2026-06-12, SEP-2663).** New spec-neutral extension crate per ADR-028 (2026-06-07 amendment): the `v2026_07_28` module carries the redesigned Tasks-extension surface — status-tagged `DetailedTask` (working/input_required/completed/failed/cancelled with variant fields inlined), `CreateTaskResult` (`resultType: "task"`, flat `Result & Task`), `tasks/get`/`tasks/update`/`tasks/cancel` bindings, `notifications/tasks`, `taskIds` subscription-filter fields, and capability negotiation helpers including SEP-2133 identifier validation. Upstream schema vendored from `modelcontextprotocol/ext-tasks@8966bea9` with a provenance README; 13 wire-shape compliance tests (explicit-null `ttlMs`, snake_case status strings, flat task discriminator). `protocol-2026-07-28` is the default feature; `--no-default-features` compiles empty. Server dispatch wiring and the 2025-11-25 reconciliation module are tracked as separate slices (ADR-028 revision log 2026-06-12). Partially closes driver gap **G1** (SEP-2663 row stays 🟡 until dispatch lands).

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -223,6 +223,7 @@ turul-http-mcp-server = { version = "0.4.0", path = "crates/turul-http-mcp-serve
223223
turul-mcp-server = { version = "0.4.0", path = "crates/turul-mcp-server" }
224224
turul-mcp-derive = { version = "0.4.0", path = "crates/turul-mcp-derive" }
225225
turul-mcp-client = { version = "0.4.0", path = "crates/turul-mcp-client" }
226+
turul-mcp-ext-tasks = { version = "0.1.0", path = "crates/turul-mcp-ext-tasks" }
226227
turul-mcp-builders = { version = "0.4.0", path = "crates/turul-mcp-builders" }
227228
turul-mcp-aws-lambda = { version = "0.4.0", path = "crates/turul-mcp-aws-lambda" }
228229
turul-mcp-oauth = { version = "0.4.0", path = "crates/turul-mcp-oauth" }

crates/turul-http-mcp-server/src/streamable_http.rs

Lines changed: 42 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1993,6 +1993,28 @@ impl StreamableHttpHandler {
19931993
.filter(|uris| !uris.is_empty() && caps.resources.is_some()),
19941994
};
19951995

1996+
// Tasks-extension filter addition (SEP-2663 `taskIds`): rides the same
1997+
// `notifications` object but is not a core-schema field, so it is read
1998+
// from the raw params; honored only when the server advertises
1999+
// `io.modelcontextprotocol/tasks` in `capabilities.extensions`.
2000+
let tasks_extension_advertised = caps
2001+
.extensions
2002+
.as_ref()
2003+
.is_some_and(|m| m.contains_key("io.modelcontextprotocol/tasks"));
2004+
let honored_task_ids: Option<Vec<String>> = request
2005+
.params
2006+
.as_ref()
2007+
.and_then(|p| serde_json::to_value(p).ok())
2008+
.as_ref()
2009+
.and_then(|v| v.pointer("/notifications/taskIds"))
2010+
.and_then(|v| v.as_array())
2011+
.map(|ids| {
2012+
ids.iter()
2013+
.filter_map(|i| i.as_str().map(String::from))
2014+
.collect::<Vec<_>>()
2015+
})
2016+
.filter(|ids| !ids.is_empty() && tasks_extension_advertised);
2017+
19962018
// Gate delivery at the broadcast layer: the subscription registry entry is
19972019
// created even when empty, so an empty filter delivers nothing (the
19982020
// registry's no-entry default is allow-all).
@@ -2009,6 +2031,9 @@ impl StreamableHttpHandler {
20092031
if honored.resource_subscriptions.is_some() {
20102032
allowed_types.push("notifications/resources/updated".to_string());
20112033
}
2034+
if honored_task_ids.is_some() {
2035+
allowed_types.push("notifications/tasks".to_string());
2036+
}
20122037
self.stream_manager
20132038
.subscribe_to_notifications(&session_id, allowed_types)
20142039
.await;
@@ -2033,10 +2058,11 @@ impl StreamableHttpHandler {
20332058

20342059
// Acknowledgement — wire-complete JSON-RPC notification, first frame.
20352060
let mut ack_params = std::collections::HashMap::new();
2036-
ack_params.insert(
2037-
"notifications".to_string(),
2038-
serde_json::to_value(&honored).unwrap_or_default(),
2039-
);
2061+
let mut honored_value = serde_json::to_value(&honored).unwrap_or_default();
2062+
if let (Some(obj), Some(ids)) = (honored_value.as_object_mut(), honored_task_ids.as_ref()) {
2063+
obj.insert("taskIds".to_string(), serde_json::json!(ids));
2064+
}
2065+
ack_params.insert("notifications".to_string(), honored_value);
20402066
ack_params.insert(
20412067
"_meta".to_string(),
20422068
serde_json::json!({ META_KEY_SUBSCRIPTION_ID: subscription_id }),
@@ -2054,6 +2080,7 @@ impl StreamableHttpHandler {
20542080
// drops the rest — this is the per-URI filter plus defense in depth) and
20552081
// stamp the subscription id into every delivered notification.
20562082
let filter = honored;
2083+
let filter_task_ids = honored_task_ids;
20572084
let sub_id = subscription_id;
20582085
let live = ReceiverStream::new(rx).filter_map(move |ev| {
20592086
let mut data = ev.data;
@@ -2077,6 +2104,17 @@ impl StreamableHttpHandler {
20772104
_ => false,
20782105
}
20792106
}
2107+
// Tasks extension (SEP-2663): per-taskId delivery.
2108+
"notifications/tasks" => {
2109+
let task_id = data
2110+
.get("params")
2111+
.and_then(|p| p.get("taskId"))
2112+
.and_then(|t| t.as_str());
2113+
match (task_id, &filter_task_ids) {
2114+
(Some(id), Some(ids)) => ids.iter().any(|x| x == id),
2115+
_ => false,
2116+
}
2117+
}
20802118
_ => false,
20812119
};
20822120
if !allowed {

crates/turul-mcp-ext-tasks/Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,3 +23,6 @@ protocol-2026-07-28 = ["dep:turul-mcp-protocol-2026-07-28"]
2323
turul-mcp-protocol-2026-07-28 = { version = "0.4.0", path = "../turul-mcp-protocol-2026-07-28", optional = true }
2424
serde.workspace = true
2525
serde_json.workspace = true
26+
async-trait.workspace = true
27+
thiserror.workspace = true
28+
chrono.workspace = true

crates/turul-mcp-ext-tasks/src/lib.rs

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,8 @@
2020
//! `subscriptions/listen` with `taskIds` filter fields
2121
//!
2222
//! Adding this crate as a dependency is the opt-in — extensions are disabled
23-
//! by default. Schema provenance: `schema/README.md` (vendored from
23+
//! by default. Server-side dispatch lives in `turul-mcp-server` behind its
24+
//! `ext-tasks` feature (`.with_ext_tasks(store)` + `.ext_task_tool(...)`). Schema provenance: `schema/README.md` (vendored from
2425
//! `modelcontextprotocol/ext-tasks` at a pinned commit).
2526
//!
2627
//! See the SEP: <https://modelcontextprotocol.io/seps/2663-tasks-extension>.
@@ -31,9 +32,10 @@ pub mod v2026_07_28;
3132
#[cfg(feature = "protocol-2026-07-28")]
3233
pub use v2026_07_28::{
3334
CancelTaskParams, CreateTaskResult, DetailedTask, EXTENSION_IDENTIFIER, GetTaskParams,
34-
GetTaskResult, InvalidExtensionIdentifier, METHOD_NOTIFICATIONS_TASKS, METHOD_TASKS_CANCEL,
35-
METHOD_TASKS_GET, METHOD_TASKS_UPDATE, Nullable, RESULT_TYPE_TASK, Task, TaskAckResult,
36-
TaskFields, TaskStatus, TaskStatusNotificationParams,
35+
GetTaskResult, InMemoryTaskStore, InputDelivery, InvalidExtensionIdentifier,
36+
METHOD_NOTIFICATIONS_TASKS, METHOD_TASKS_CANCEL, METHOD_TASKS_GET, METHOD_TASKS_UPDATE,
37+
Nullable, RESULT_TYPE_TASK, Task, TaskAckResult, TaskFields, TaskState, TaskStatus,
38+
TaskStatusNotificationParams, TaskStore, TaskStoreError,
3739
TaskSubscriptionAcknowledgedNotifications, TaskSubscriptionNotifications, UpdateTaskParams,
3840
capability, declared_by_client, declared_by_server, validate_identifier,
3941
};

crates/turul-mcp-ext-tasks/src/v2026_07_28/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
33
pub mod capability;
44
pub mod lifecycle;
5+
pub mod store;
56
pub mod types;
67

78
#[cfg(test)]
@@ -17,6 +18,7 @@ pub use lifecycle::{
1718
TaskStatusNotificationParams, TaskSubscriptionAcknowledgedNotifications,
1819
TaskSubscriptionNotifications, UpdateTaskParams,
1920
};
21+
pub use store::{InMemoryTaskStore, InputDelivery, TaskState, TaskStore, TaskStoreError};
2022
pub use types::{
2123
CreateTaskResult, DetailedTask, Nullable, RESULT_TYPE_TASK, Task, TaskFields, TaskStatus,
2224
};

0 commit comments

Comments
 (0)