#![cfg(not(target_arch = "wasm32"))]
use std::net::SocketAddr;
use std::sync::Arc;
use std::time::Duration;
use pmcp::server::streamable_http_server::{StreamableHttpServer, StreamableHttpServerConfig};
use pmcp::server::task_store::{InMemoryTaskStore, TaskInputSnapshot, TaskStore};
use pmcp::server::typed_tool::TypedTool;
use pmcp::types::capabilities::TASKS_EXTENSION_KEY;
use pmcp::types::content::Content;
use pmcp::types::elicitation::ElicitRequestParams;
use pmcp::types::protocol::{
ProtocolVersion, LATEST_PROTOCOL_VERSION, PROTOCOL_VERSION_2026_07_28,
};
use pmcp::types::tasks::TaskStatus;
use pmcp::types::{
CallToolResult, InputRequest, InputRequests, InputResponse, TaskSupport, ToolExecution,
};
use pmcp::Server;
use serde_json::{json, Value};
use tokio::sync::Mutex;
const TOOL_NAME: &str = "research";
const TOPIC_KEY: &str = "topic";
const DEFAULT_ADDR: &str = "127.0.0.1:8150";
const SHARED_ANONYMOUS_OWNER: &str = "";
const WORKER_TICK_MS: u64 = 25;
const TASK_TTL_MS: u64 = 300_000;
const DISCARDED_TASK_ID: &str = "discarded-the-store-mints-the-real-one";
#[tokio::main]
async fn main() -> std::result::Result<(), Box<dyn std::error::Error>> {
tracing_subscriber::fmt()
.with_env_filter("pmcp=warn")
.init();
let requested: SocketAddr = std::env::args()
.nth(1)
.unwrap_or_else(|| DEFAULT_ADDR.to_string())
.parse()?;
let store = Arc::new(InMemoryTaskStore::new());
let worker_store: Arc<dyn TaskStore> = store.clone();
let server_store: Arc<dyn TaskStore> = store;
let task_tool = TypedTool::new_with_schema(
TOOL_NAME,
json!({ "type": "object", "properties": {} }),
|_args: Value, _extra| Box::pin(async { research_task_value() }),
)
.with_description("Research a topic asynchronously, asking which topic first")
.with_execution(ToolExecution::new().with_task_support(TaskSupport::Required));
let server = Server::builder()
.name("s50-v2-tasks-server")
.version("1.0.0")
.with_supported_protocol_versions([
ProtocolVersion(LATEST_PROTOCOL_VERSION.to_string()),
ProtocolVersion(PROTOCOL_VERSION_2026_07_28.to_string()),
])
.tool(TOOL_NAME, task_tool)
.task_store(server_store)
.build()?;
let http = StreamableHttpServer::with_config(
requested,
Arc::new(Mutex::new(server)),
StreamableHttpServerConfig::default(),
);
let (addr, server_handle) = http.start().await?;
let worker_handle = tokio::spawn(run_worker(worker_store));
print_instructions(addr);
let outcome = server_handle.await;
worker_handle.abort();
outcome?;
Ok(())
}
fn research_task_value() -> pmcp::Result<Value> {
let mut requests = InputRequests::new();
requests.insert(
TOPIC_KEY.to_string(),
InputRequest::Elicitation(Box::new(ElicitRequestParams::Form {
message: "Which topic should I research?".to_string(),
requested_schema: json!({
"type": "object",
"properties": { TOPIC_KEY: { "type": "string" } },
"required": [TOPIC_KEY],
}),
})),
);
let requests =
serde_json::to_value(requests).map_err(|error| pmcp::Error::internal(error.to_string()))?;
Ok(json!({
"taskId": DISCARDED_TASK_ID,
"status": "input_required",
"ttl": TASK_TTL_MS,
"inputRequests": requests,
}))
}
async fn run_worker(store: Arc<dyn TaskStore>) {
loop {
tokio::time::sleep(Duration::from_millis(WORKER_TICK_MS)).await;
let Ok((tasks, _cursor)) = store.list(SHARED_ANONYMOUS_OWNER, None).await else {
continue;
};
for task in tasks {
if task.status != TaskStatus::Working {
continue;
}
let Ok(snapshot) = store
.task_input_snapshot(&task.task_id, SHARED_ANONYMOUS_OWNER)
.await
else {
continue;
};
if !snapshot.is_complete() {
continue;
}
println!(
" worker: task {} received its input, completing it",
task.task_id
);
let result = finish(&snapshot);
if store
.set_result(&task.task_id, SHARED_ANONYMOUS_OWNER, result)
.await
.is_err()
{
continue;
}
let _ = store
.update_status(
&task.task_id,
SHARED_ANONYMOUS_OWNER,
TaskStatus::Completed,
None,
)
.await;
}
}
}
fn finish(snapshot: &TaskInputSnapshot) -> CallToolResult {
match answered_topic(snapshot) {
Some(topic) => CallToolResult::new(vec![Content::Text {
text: format!("research on {topic}: 3 sources reviewed, no contradictions found"),
}]),
None => CallToolResult::error(vec![Content::Text {
text: "no usable topic was supplied, so there was nothing to research".to_string(),
}]),
}
}
fn answered_topic(snapshot: &TaskInputSnapshot) -> Option<String> {
let InputResponse::Elicitation(result) = snapshot.input_responses.get(TOPIC_KEY)? else {
return None;
};
result
.content
.as_ref()?
.get(TOPIC_KEY)?
.as_str()
.map(str::to_string)
}
fn print_instructions(addr: SocketAddr) {
println!();
println!("=============================================================");
println!(" v2 (2026-07-28) TASKS SERVER — pause, update, resume");
println!("=============================================================");
println!(" Listening on : {addr}");
println!(" Endpoint : http://{addr}");
println!(
" Versions : {LATEST_PROTOCOL_VERSION} (v1) and {PROTOCOL_VERSION_2026_07_28} (v2)"
);
println!(" Tool : {TOOL_NAME} (TaskSupport::Required)");
println!(" Extension : {TASKS_EXTENSION_KEY}");
println!(" Store : InMemoryTaskStore (no auth provider — SHARED");
println!(" owner bucket; see this example's header)");
println!("-------------------------------------------------------------");
println!(" A v2 caller that DECLARES the tasks extension and calls");
println!(" {TOOL_NAME} receives a task handle that is ALREADY paused on an");
println!(" elicitation for \"{TOPIC_KEY}\". Answering it with tasks/update");
println!(" resumes the task; a worker in this process then completes it.");
println!();
println!(" tasks/list and tasks/result are RETIRED on 2026-07-28 and this");
println!(" server answers both -32601.");
println!("-------------------------------------------------------------");
println!(" Now run the paired autonomous agent:");
println!(" cargo run --example s51_v2_tasks_agent --features full -- {addr}");
println!("=============================================================");
println!();
println!("Press Ctrl+C to stop the server");
}