#![cfg(not(target_arch = "wasm32"))]
#![allow(clippy::redundant_pub_crate)]
use crate::error::{Error, Result};
use crate::server::auth::AuthContext;
use crate::server::task_store::TaskStore;
use crate::server::tasks::TaskRouter;
use crate::types::capabilities::{ServerCapabilities, ServerTasksCapability};
use crate::types::jsonrpc::ResponsePayload;
use crate::types::tasks::{TaskStatus, RELATED_TASK_META_KEY};
use crate::types::tools::TaskSupport;
use crate::types::{
CallToolResult, ClientRequest, Content, JSONRPCError, JSONRPCResponse, RequestId, ToolInfo,
};
use serde_json::Value;
use std::collections::HashMap;
use std::sync::Arc;
pub(crate) fn default_tasks_capability() -> ServerTasksCapability {
ServerTasksCapability {
list: Some(serde_json::json!({})),
cancel: Some(serde_json::json!({})),
requests: Some(crate::types::capabilities::ServerTasksRequestCapability {
tools: Some(crate::types::capabilities::ServerTasksToolsCapability {
call: Some(serde_json::json!({})),
}),
}),
}
}
pub(crate) fn apply_tasks_capability_rule(
capabilities: &mut ServerCapabilities,
tool_infos: &HashMap<String, ToolInfo>,
has_backend: bool,
) -> Result<()> {
let has_required_task_tool = tool_infos.values().any(|info| {
info.execution
.as_ref()
.and_then(|e| e.task_support)
.is_some_and(|ts| matches!(ts, TaskSupport::Required))
});
if has_required_task_tool && !has_backend {
return Err(Error::validation(
"a tool declares TaskSupport::Required but no TaskStore or TaskRouter \
is configured to back the tasks/* endpoints",
));
}
if capabilities.tasks.is_none() && has_backend {
capabilities.tasks = Some(default_tasks_capability());
}
Ok(())
}
pub(crate) fn success_response(id: RequestId, result: Value) -> JSONRPCResponse {
JSONRPCResponse {
jsonrpc: "2.0".to_string(),
id,
payload: ResponsePayload::Result(result),
}
}
pub(crate) fn error_response(id: RequestId, code: i32, message: String) -> JSONRPCResponse {
JSONRPCResponse {
jsonrpc: "2.0".to_string(),
id,
payload: ResponsePayload::Error(JSONRPCError {
code,
message,
data: None,
}),
}
}
pub(crate) enum DispatchOutput {
Verbatim(CallToolResult),
Middleware(Result<Value>),
}
pub(crate) fn resolve_tool_output(output: Result<crate::server::ToolOutput>) -> DispatchOutput {
match output {
Ok(crate::server::ToolOutput::Result(call_result)) => DispatchOutput::Verbatim(call_result),
Ok(crate::server::ToolOutput::Payload(value)) => DispatchOutput::Middleware(Ok(value)),
Err(e) => DispatchOutput::Middleware(Err(e)),
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DoubleWrapMarker {
RelatedTaskMeta,
ContentArray,
}
pub fn looks_like_call_tool_result(v: &Value) -> Option<DoubleWrapMarker> {
const RESULT_ENVELOPE_KEYS: [&str; 4] = ["content", "isError", "structuredContent", "_meta"];
let obj = v.as_object()?;
if obj
.get("_meta")
.and_then(Value::as_object)
.is_some_and(|meta| meta.contains_key(RELATED_TASK_META_KEY))
{
return Some(DoubleWrapMarker::RelatedTaskMeta);
}
if obj
.keys()
.all(|k| RESULT_ENVELOPE_KEYS.contains(&k.as_str()))
&& obj
.get("content")
.and_then(Value::as_array)
.is_some_and(|arr| {
!arr.is_empty()
&& arr
.iter()
.all(|e| serde_json::from_value::<Content>(e.clone()).is_ok())
})
{
return Some(DoubleWrapMarker::ContentArray);
}
None
}
pub fn double_wrap_tripwire(
tool_name: &str,
value: &Value,
suppressed: bool,
) -> Option<DoubleWrapMarker> {
if suppressed {
return None;
}
let marker = looks_like_call_tool_result(value)?;
tracing::warn!(
tool = %tool_name,
?marker,
"value being text-wrapped structurally resembles a built CallToolResult \
— did you mean ToolOutput::Result? (TOUT-02)"
);
debug_assert!(
false,
"double-wrap tripwire (TOUT-02): tool `{tool_name}` produced a value that \
structurally resembles a built CallToolResult ({marker:?}); return \
ToolOutput::Result to send it verbatim, or call \
suppress_double_wrap_check(\"{tool_name}\") if this payload is legitimate"
);
Some(marker)
}
pub(crate) struct TaskDispatch<'a> {
pub(crate) task_store: &'a Option<Arc<dyn TaskStore>>,
pub(crate) task_router: &'a Option<Arc<dyn TaskRouter>>,
}
impl TaskDispatch<'_> {
pub(crate) fn resolve_owner(&self, auth_context: Option<&AuthContext>) -> Option<String> {
if let Some(router) = self.task_router {
return Some(match auth_context {
Some(ctx) => {
router.resolve_owner(Some(&ctx.subject), ctx.client_id.as_deref(), None)
},
None => router.resolve_owner(None, None, None),
});
}
if self.task_store.is_some() {
return Some(match auth_context {
Some(ctx) => ctx.subject.clone(),
None => "local".to_string(),
});
}
None
}
pub(crate) fn extract_terminal_result(value: &Value) -> Option<CallToolResult> {
if let Some(result_value) = value.get("result") {
return serde_json::from_value::<CallToolResult>(result_value.clone()).ok();
}
if value.get("content").is_some() {
return serde_json::from_value::<CallToolResult>(value.clone()).ok();
}
None
}
pub(crate) async fn build_task_created_response(
&self,
id: RequestId,
value: Value,
auth_context: Option<&AuthContext>,
) -> JSONRPCResponse {
let Some(store) = self.task_store.as_ref() else {
let tool_task_id = value
.get("taskId")
.and_then(|v| v.as_str())
.unwrap_or_default()
.to_string();
let result_value = serde_json::json!({
"task": value,
"_meta": { RELATED_TASK_META_KEY: { "taskId": tool_task_id } }
});
return success_response(id, result_value);
};
let owner_id = self
.resolve_owner(auth_context)
.unwrap_or_else(|| "local".to_string());
let ttl = value.get("ttl").and_then(serde_json::Value::as_u64);
let created = match store.create(&owner_id, ttl).await {
Ok(task) => task,
Err(e) => return error_response(id, -32603, e.to_string()),
};
let store_id = created.task_id.clone();
let terminal_result = Self::extract_terminal_result(&value);
let final_task = if let Some(call_result) = terminal_result {
if let Err(e) = store.set_result(&store_id, &owner_id, call_result).await {
return error_response(id, -32603, e.to_string());
}
match store
.update_status(&store_id, &owner_id, TaskStatus::Completed, None)
.await
{
Ok(task) => task,
Err(e) => return error_response(id, -32603, e.to_string()),
}
} else {
created
};
let create_result = crate::types::tasks::CreateTaskResult::new(final_task);
let mut envelope = serde_json::to_value(create_result).unwrap_or_default();
if let Some(obj) = envelope.as_object_mut() {
obj.insert(
"_meta".to_string(),
serde_json::json!({ RELATED_TASK_META_KEY: { "taskId": store_id } }),
);
}
success_response(id, envelope)
}
pub(crate) async fn maybe_build_task_created(
&self,
id: RequestId,
value: &Value,
task_support: Option<TaskSupport>,
task_requested: bool,
auth_context: Option<&AuthContext>,
) -> Option<JSONRPCResponse> {
let gate_open = task_requested
&& self.task_store.is_some()
&& task_support
.is_some_and(|ts| matches!(ts, TaskSupport::Required | TaskSupport::Optional));
if !gate_open {
return None;
}
let is_task_shaped =
value.get("taskId").and_then(Value::as_str).is_some() && value.get("status").is_some();
if !is_task_shaped {
return None;
}
Some(
self.build_task_created_response(id, value.clone(), auth_context)
.await,
)
}
pub(crate) async fn handle_tasks_result(
&self,
id: RequestId,
params: &crate::types::tasks::GetTaskPayloadRequest,
auth_context: Option<&AuthContext>,
) -> JSONRPCResponse {
let owner_id = self
.resolve_owner(auth_context)
.unwrap_or_else(|| "local".to_string());
if let Some(store) = self.task_store {
if store.supports_results() {
match store.get_result(¶ms.task_id, &owner_id).await {
Ok(call_result) => {
return success_response(
id,
serde_json::to_value(call_result).unwrap_or_default(),
);
},
Err(crate::server::task_store::TaskStoreError::NotFound { .. }) => {},
Err(e) => return error_response(id, -32603, e.to_string()),
}
}
}
if let Some(task_router) = self.task_router {
return match task_router
.handle_tasks_result(serde_json::to_value(params).unwrap_or_default(), &owner_id)
.await
{
Ok(result) => success_response(id, result),
Err(e) => error_response(id, -32603, e.to_string()),
};
}
if self.task_store.is_some() {
error_response(
id,
-32002,
"task result not available: task not completed".to_string(),
)
} else {
error_response(id, -32601, "tasks/result not supported".to_string())
}
}
async fn route_tasks_get(
&self,
id: RequestId,
params: &crate::types::tasks::GetTaskRequest,
auth_context: Option<&AuthContext>,
) -> JSONRPCResponse {
let owner_id = self
.resolve_owner(auth_context)
.unwrap_or_else(|| "local".to_string());
if let Some(store) = self.task_store {
match store.get(¶ms.task_id, &owner_id).await {
Ok(task) => {
let result = crate::types::tasks::GetTaskResult::new(task);
success_response(id, serde_json::to_value(result).unwrap_or_default())
},
Err(e) => error_response(id, -32603, e.to_string()),
}
} else if let Some(task_router) = self.task_router {
match task_router
.handle_tasks_get(serde_json::to_value(params).unwrap_or_default(), &owner_id)
.await
{
Ok(result) => success_response(id, result),
Err(e) => error_response(id, -32603, e.to_string()),
}
} else {
error_response(id, -32601, "Tasks not enabled".to_string())
}
}
async fn route_tasks_list(
&self,
id: RequestId,
params: &crate::types::tasks::ListTasksRequest,
auth_context: Option<&AuthContext>,
) -> JSONRPCResponse {
let owner_id = self
.resolve_owner(auth_context)
.unwrap_or_else(|| "local".to_string());
if let Some(store) = self.task_store {
match store.list(&owner_id, params.cursor.as_deref()).await {
Ok((tasks, next_cursor)) => {
let mut result = crate::types::tasks::ListTasksResult::new(tasks);
if let Some(cursor) = next_cursor {
result = result.with_next_cursor(cursor);
}
success_response(id, serde_json::to_value(result).unwrap_or_default())
},
Err(e) => error_response(id, -32603, e.to_string()),
}
} else if let Some(task_router) = self.task_router {
match task_router
.handle_tasks_list(serde_json::to_value(params).unwrap_or_default(), &owner_id)
.await
{
Ok(result) => success_response(id, result),
Err(e) => error_response(id, -32603, e.to_string()),
}
} else {
error_response(id, -32601, "Tasks not enabled".to_string())
}
}
async fn route_tasks_cancel(
&self,
id: RequestId,
params: &crate::types::tasks::CancelTaskRequest,
auth_context: Option<&AuthContext>,
) -> JSONRPCResponse {
let owner_id = self
.resolve_owner(auth_context)
.unwrap_or_else(|| "local".to_string());
if let Some(store) = self.task_store {
match store.cancel(¶ms.task_id, &owner_id).await {
Ok(task) => {
let result = crate::types::tasks::CancelTaskResult::new(task);
success_response(id, serde_json::to_value(result).unwrap_or_default())
},
Err(e) => error_response(id, -32603, e.to_string()),
}
} else if let Some(task_router) = self.task_router {
match task_router
.handle_tasks_cancel(serde_json::to_value(params).unwrap_or_default(), &owner_id)
.await
{
Ok(result) => success_response(id, result),
Err(e) => error_response(id, -32603, e.to_string()),
}
} else {
error_response(id, -32601, "Tasks not enabled".to_string())
}
}
pub(crate) async fn route_tasks_endpoint(
&self,
id: RequestId,
request: &ClientRequest,
auth_context: Option<&AuthContext>,
) -> JSONRPCResponse {
match request {
ClientRequest::TasksGet(params) => self.route_tasks_get(id, params, auth_context).await,
ClientRequest::TasksResult(params) => {
self.handle_tasks_result(id, params, auth_context).await
},
ClientRequest::TasksList(params) => {
self.route_tasks_list(id, params, auth_context).await
},
ClientRequest::TasksCancel(params) => {
self.route_tasks_cancel(id, params, auth_context).await
},
_ => error_response(id, -32601, "Method not supported".to_string()),
}
}
}
#[cfg(test)]
#[allow(clippy::doc_markdown, clippy::unnecessary_wraps)]
mod gate_tests {
use super::*;
use crate::server::task_store::InMemoryTaskStore;
use crate::types::RequestId;
fn store_backend() -> Option<Arc<dyn TaskStore>> {
Some(Arc::new(InMemoryTaskStore::new()) as Arc<dyn TaskStore>)
}
fn task_shaped_value() -> Value {
serde_json::json!({
"taskId": "tool-fabricated",
"status": "completed",
"result": { "content": [{ "type": "text", "text": "done" }] }
})
}
fn id() -> RequestId {
RequestId::from(1i64)
}
#[tokio::test]
async fn gate_rejects_when_not_task_requested() {
let store = store_backend();
let router = None;
let dispatch = TaskDispatch {
task_store: &store,
task_router: &router,
};
let value = task_shaped_value();
let out = dispatch
.maybe_build_task_created(id(), &value, Some(TaskSupport::Required), false, None)
.await;
assert!(out.is_none(), "task_requested=false must yield None");
}
#[tokio::test]
async fn gate_rejects_when_no_backend() {
let store = None;
let router = None;
let dispatch = TaskDispatch {
task_store: &store,
task_router: &router,
};
let value = task_shaped_value();
let out = dispatch
.maybe_build_task_created(id(), &value, Some(TaskSupport::Required), true, None)
.await;
assert!(out.is_none(), "no backend must yield None");
}
#[tokio::test]
async fn gate_rejects_forbidden_no_error_leak() {
let store = store_backend();
let router = None;
let dispatch = TaskDispatch {
task_store: &store,
task_router: &router,
};
let value = task_shaped_value();
let out = dispatch
.maybe_build_task_created(id(), &value, Some(TaskSupport::Forbidden), true, None)
.await;
assert!(out.is_none(), "Forbidden must yield None, never an error");
}
#[tokio::test]
async fn gate_rejects_no_task_support() {
let store = store_backend();
let router = None;
let dispatch = TaskDispatch {
task_store: &store,
task_router: &router,
};
let value = task_shaped_value();
let out = dispatch
.maybe_build_task_created(id(), &value, None, true, None)
.await;
assert!(out.is_none(), "no task_support must yield None");
}
#[tokio::test]
async fn gate_rejects_non_task_shaped_value() {
let store = store_backend();
let router = None;
let dispatch = TaskDispatch {
task_store: &store,
task_router: &router,
};
let value = serde_json::json!({ "foo": "bar" });
let out = dispatch
.maybe_build_task_created(id(), &value, Some(TaskSupport::Required), true, None)
.await;
assert!(out.is_none(), "non-task-shaped value must yield None");
}
fn assert_store_minted(resp: &JSONRPCResponse) {
let ResponsePayload::Result(value) = &resp.payload else {
panic!("expected a success result envelope");
};
let wire_task_id = value
.get("task")
.and_then(|t| t.get("taskId"))
.and_then(Value::as_str)
.expect("task.taskId present");
let meta_id = value
.get("_meta")
.and_then(|m| m.get(RELATED_TASK_META_KEY))
.and_then(|r| r.get("taskId"))
.and_then(Value::as_str)
.expect("_meta.relatedTask.taskId present");
assert_eq!(
wire_task_id, meta_id,
"three-way invariant: task.taskId == _meta.relatedTask.taskId"
);
assert_ne!(
wire_task_id, "tool-fabricated",
"wire id must be store-minted, not the tool-fabricated id"
);
}
#[tokio::test]
async fn gate_accepts_optional_task_shaped() {
let store = store_backend();
let router = None;
let dispatch = TaskDispatch {
task_store: &store,
task_router: &router,
};
let value = task_shaped_value();
let out = dispatch
.maybe_build_task_created(id(), &value, Some(TaskSupport::Optional), true, None)
.await;
let resp = out.expect("Optional + task-shaped must yield Some");
assert_store_minted(&resp);
}
#[tokio::test]
async fn gate_accepts_required_task_shaped() {
let store = store_backend();
let router = None;
let dispatch = TaskDispatch {
task_store: &store,
task_router: &router,
};
let value = task_shaped_value();
let out = dispatch
.maybe_build_task_created(id(), &value, Some(TaskSupport::Required), true, None)
.await;
let resp = out.expect("Required + task-shaped must yield Some");
assert_store_minted(&resp);
}
}