---
phase: 03-handler-middleware-and-server-integration
plan: 03
type: execute
wave: 3
depends_on: ["03-02"]
files_modified:
  - crates/pmcp-tasks/tests/lifecycle_integration.rs
  - examples/60_tasks_basic.rs
  - Cargo.toml
autonomous: true
requirements:
  - TEST-08
  - EXMP-01

must_haves:
  truths:
    - "A full lifecycle integration test passes: create task via tools/call with task field -> poll via tasks/get until terminal -> retrieve result via tasks/result"
    - "The tasks/list endpoint returns tasks scoped to the requesting owner"
    - "The tasks/cancel endpoint cancels a non-terminal task"
    - "The basic tasks example (60_tasks_basic.rs) compiles and demonstrates the complete task lifecycle"
  artifacts:
    - path: "crates/pmcp-tasks/tests/lifecycle_integration.rs"
      provides: "Full lifecycle integration tests"
      min_lines: 100
    - path: "examples/60_tasks_basic.rs"
      provides: "Basic task-augmented tool call example"
      min_lines: 80
  key_links:
    - from: "crates/pmcp-tasks/tests/lifecycle_integration.rs"
      to: "src/server/core.rs"
      via: "ServerCore::handle_request() for end-to-end request processing"
      pattern: "handle_request"
    - from: "examples/60_tasks_basic.rs"
      to: "crates/pmcp-tasks/src/router.rs"
      via: "TaskRouterImpl used with ServerCoreBuilder"
      pattern: "TaskRouterImpl"
---

<objective>
Write full lifecycle integration tests and the basic tasks example. The integration tests exercise the complete create-poll-complete flow through ServerCore. The example demonstrates the simplest possible task-enabled server.

Purpose: Verify end-to-end correctness and provide a working example for developers.
Output: Integration test file, basic example file.
</objective>

<execution_context>
@./.claude/get-shit-done/workflows/execute-plan.md
@./.claude/get-shit-done/templates/summary.md
</execution_context>

<context>
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/STATE.md
@.planning/phases/03-handler-middleware-and-server-integration/03-RESEARCH.md
@.planning/phases/03-handler-middleware-and-server-integration/03-CONTEXT.md
@.planning/phases/03-handler-middleware-and-server-integration/03-01-SUMMARY.md
@.planning/phases/03-handler-middleware-and-server-integration/03-02-SUMMARY.md
@crates/pmcp-tasks/tests/
@examples/
</context>

<tasks>

<task type="auto">
  <name>Task 1: Write full lifecycle integration tests</name>
  <files>crates/pmcp-tasks/tests/lifecycle_integration.rs</files>
  <action>
Create `crates/pmcp-tasks/tests/lifecycle_integration.rs` with end-to-end tests that exercise task lifecycle through `ServerCore::handle_request()`.

**Test setup helper:**
Create a helper function that builds a `ServerCore` with:
- InMemoryTaskStore with allow_anonymous=true (for testing without auth)
- TaskRouterImpl wrapping the store
- A simple test tool registered
- Stateless mode enabled (skip initialize for simpler tests) OR initialize in each test

```rust
use pmcp::server::builder::ServerCoreBuilder;
use pmcp::server::core::ProtocolHandler;
use pmcp::types::{Request, ClientRequest, RequestId};
use pmcp::types::jsonrpc::ResponsePayload;
use pmcp_tasks::{InMemoryTaskStore, TaskRouterImpl, TaskSecurityConfig};
use serde_json::{json, Value};
use std::sync::Arc;

fn build_task_server() -> pmcp::server::core::ServerCore {
    let store = Arc::new(
        InMemoryTaskStore::new()
            .with_security(TaskSecurityConfig::default().with_allow_anonymous(true)),
    );
    let router = Arc::new(TaskRouterImpl::new(store.clone()));

    ServerCoreBuilder::new()
        .name("test-tasks")
        .version("1.0.0")
        .tool("long_running_tool", TestTool { store: store.clone() })
        .with_task_store(router)
        .stateless_mode(true)
        .build()
        .unwrap()
}
```

The `TestTool` is a simple tool handler that, in a real scenario, would trigger external work. For testing, it can directly complete the task using the store reference it holds (simulating what an external service would do).

**Test 1: Full lifecycle -- create, poll, complete, get_result**
1. Send `tools/call` with `task: { "ttl": 60000 }` -> expect `CreateTaskResult` with `task.status == "working"`
2. Extract `taskId` from response
3. Send `tasks/get` with the taskId -> expect `Task` with `status == "working"`
4. Simulate background completion: directly call `store.complete_with_result(task_id, owner, result_value)`
5. Send `tasks/get` again -> expect `status == "completed"`
6. Send `tasks/result` -> expect the stored result value with `_meta` containing related-task

**Test 2: tasks/list returns owner-scoped results**
1. Create two tasks via `tools/call` with task field
2. Send `tasks/list` -> expect both tasks in the response
3. Verify pagination cursor handling

**Test 3: tasks/cancel transitions to cancelled**
1. Create a task via `tools/call`
2. Send `tasks/cancel` -> expect `status == "cancelled"`
3. Send `tasks/get` -> confirm still cancelled
4. Send `tasks/cancel` again -> expect error (already terminal)

**Test 4: Task-augmented call without task field on required tool**
1. Register a tool that returns `ToolInfo` with `execution: Some(json!({"taskSupport": "required"}))`
2. Send `tools/call` WITHOUT task field -> expect `CreateTaskResult` (auto-created)

**Test 5: Normal tool call still works (no task field, no required)**
1. Send `tools/call` without task field to a normal tool -> expect normal `CallToolResult`
2. Verify the task system does not interfere

**Test 6: tasks/get with wrong owner returns error**
1. Create a task as anonymous (local owner)
2. If possible, send tasks/get with different auth context -> expect NotFound error

**Test 7: tasks/result on non-terminal task returns error**
1. Create a task (still working)
2. Send `tasks/result` -> expect error (task not in terminal state)

**Test 8: TTL is respected from task params**
1. Create task with `ttl: 1` (1ms)
2. Sleep briefly
3. Verify task is expired (mutations fail, but get may still work per Phase 2 decision)

Each test should construct proper JSON-RPC requests, send them through `server.handle_request()`, and validate the JSON response structure.

Add pmcp as a dev-dependency of pmcp-tasks if not already present for test access to ServerCore, or put tests in a separate integration test that depends on both crates. The integration test file is in `crates/pmcp-tasks/tests/` which can access both pmcp and pmcp-tasks as dependencies.

Update `crates/pmcp-tasks/Cargo.toml` dev-dependencies if needed:
```toml
[dev-dependencies]
pmcp = { path = "../..", features = ["full"] }
```
  </action>
  <verify>
Run `cargo test --package pmcp-tasks --test lifecycle_integration` to verify all integration tests pass. Run with `-- --test-threads=1` per CLAUDE.md requirement for race condition prevention.
  </verify>
  <done>
At least 5 integration tests pass covering: full create-poll-complete lifecycle, tasks/list, tasks/cancel, auto-task for required tools, normal tool calls unaffected. All tests exercise the real ServerCore handle_request path.
  </done>
</task>

<task type="auto">
  <name>Task 2: Create 60_tasks_basic.rs example</name>
  <files>examples/60_tasks_basic.rs, Cargo.toml</files>
  <action>
Create `examples/60_tasks_basic.rs` per CONTEXT.md locked decisions:
- Minimal viable example -- simplest possible task-enabled server
- Uses InMemoryTaskStore (self-contained, no external dependencies)
- Simulates background execution with tokio::spawn + sleep for demonstration
- One tool demonstrating create-poll-complete lifecycle

**Structure:**

```rust
//! Example: Basic Task-Augmented Tool Call
//!
//! Demonstrates the complete MCP Tasks lifecycle:
//! 1. Server advertises task support via experimental.tasks
//! 2. Client sends tools/call with a task field
//! 3. Server creates task, simulates background work
//! 4. Client polls tasks/get until completion
//! 5. Client retrieves result via tasks/result

use async_trait::async_trait;
use pmcp::server::builder::ServerCoreBuilder;
use pmcp::server::core::ProtocolHandler;
use pmcp::{RequestHandlerExtra, ToolHandler};
use pmcp::types::*;
use pmcp_tasks::{InMemoryTaskStore, TaskRouterImpl, TaskSecurityConfig};
use serde_json::{json, Value};
use std::sync::Arc;

/// A tool that simulates a long-running operation.
///
/// In production, this would trigger an AWS Step Functions execution
/// or enqueue a message to SQS. For demonstration, we simulate
/// background work with tokio::spawn and sleep.
struct LongRunningAnalysis {
    store: Arc<InMemoryTaskStore>,
}

#[async_trait]
impl ToolHandler for LongRunningAnalysis {
    async fn handle(&self, args: Value, _extra: RequestHandlerExtra) -> pmcp::Result<Value> {
        // In production: trigger external service, return immediately
        // For demo: return tool metadata (the task system handles the lifecycle)
        Ok(json!({
            "message": "Analysis tool invoked",
            "input": args
        }))
    }

    fn metadata(&self) -> Option<ToolInfo> {
        Some(ToolInfo::new(
            "analyze_data",
            Some("Analyze a dataset (long-running operation)".to_string()),
            json!({
                "type": "object",
                "properties": {
                    "dataset": { "type": "string", "description": "Dataset name" }
                },
                "required": ["dataset"]
            }),
        ))
    }
}

#[tokio::main]
async fn main() {
    // 1. Create task store and router
    let store = Arc::new(
        InMemoryTaskStore::new()
            .with_security(TaskSecurityConfig::default().with_allow_anonymous(true)),
    );
    let router = Arc::new(TaskRouterImpl::new(store.clone()));

    // 2. Build server with task support
    let server = ServerCoreBuilder::new()
        .name("tasks-basic-example")
        .version("1.0.0")
        .tool("analyze_data", LongRunningAnalysis { store: store.clone() })
        .with_task_store(router)
        .stateless_mode(true)
        .build()
        .unwrap();

    println!("=== MCP Tasks Basic Example ===\n");

    // 3. Send tools/call with task augmentation
    println!("Step 1: Call tool with task augmentation...");
    let call_req = Request::Client(Box::new(ClientRequest::CallTool(CallToolParams {
        name: "analyze_data".to_string(),
        arguments: json!({"dataset": "sales_2024"}),
        _meta: None,
        task: Some(json!({"ttl": 60000})),
    })));

    let response = server.handle_request(RequestId::from(1i64), call_req, None).await;
    let create_result: Value = match response.payload {
        pmcp::types::jsonrpc::ResponsePayload::Result(v) => v,
        pmcp::types::jsonrpc::ResponsePayload::Error(e) => panic!("Create failed: {}", e.message),
    };

    let task_id = create_result["task"]["taskId"].as_str().unwrap();
    println!("  Task created: {}", task_id);
    println!("  Status: {}", create_result["task"]["status"]);

    // 4. Poll task status
    println!("\nStep 2: Poll task status...");
    let get_req = Request::Client(Box::new(ClientRequest::TasksGet(
        json!({"taskId": task_id}),
    )));
    let response = server.handle_request(RequestId::from(2i64), get_req, None).await;
    let task: Value = match response.payload {
        pmcp::types::jsonrpc::ResponsePayload::Result(v) => v,
        pmcp::types::jsonrpc::ResponsePayload::Error(e) => panic!("Get failed: {}", e.message),
    };
    println!("  Status: {}", task["status"]);

    // 5. Simulate background completion with tokio::spawn + sleep
    //    (per locked decision: "Simulates background execution with tokio::spawn + sleep for demonstration")
    //    In production, an external service (Step Functions, SQS consumer) does this.
    println!("\nStep 3: Simulate background work (tokio::spawn + sleep)...");
    let owner_id = pmcp_tasks::DEFAULT_LOCAL_OWNER.to_string();
    let bg_store = store.clone();
    let bg_task_id = task_id.to_string();
    let bg_handle = tokio::spawn(async move {
        // Simulate work taking some time
        tokio::time::sleep(std::time::Duration::from_millis(500)).await;
        bg_store.complete_with_result(
            &bg_task_id,
            &owner_id,
            json!({
                "analysis": "complete",
                "rows_processed": 1_500_000,
                "anomalies_found": 42
            }),
        ).await.unwrap();
    });
    // Wait for the background task to finish (in production, client would poll)
    bg_handle.await.unwrap();
    println!("  Task completed by background service");

    // 6. Poll again to see completion
    println!("\nStep 4: Poll after completion...");
    let get_req = Request::Client(Box::new(ClientRequest::TasksGet(
        json!({"taskId": task_id}),
    )));
    let response = server.handle_request(RequestId::from(3i64), get_req, None).await;
    let task: Value = match response.payload {
        pmcp::types::jsonrpc::ResponsePayload::Result(v) => v,
        pmcp::types::jsonrpc::ResponsePayload::Error(e) => panic!("Get failed: {}", e.message),
    };
    println!("  Status: {}", task["status"]);

    // 7. Get the result
    println!("\nStep 5: Retrieve task result...");
    let result_req = Request::Client(Box::new(ClientRequest::TasksResult(
        json!({"taskId": task_id}),
    )));
    let response = server.handle_request(RequestId::from(4i64), result_req, None).await;
    let result: Value = match response.payload {
        pmcp::types::jsonrpc::ResponsePayload::Result(v) => v,
        pmcp::types::jsonrpc::ResponsePayload::Error(e) => panic!("Result failed: {}", e.message),
    };
    println!("  Result: {}", serde_json::to_string_pretty(&result).unwrap());

    println!("\n=== Lifecycle Complete ===");
}
```

**Update root `Cargo.toml`** to add the example if it uses `[[example]]` sections, or verify it auto-discovers from examples/ directory.

Ensure the example depends on both `pmcp` and `pmcp-tasks` -- check if root Cargo.toml has `pmcp-tasks` as a dev-dependency or if examples can access workspace members.

The example should compile and run with `cargo run --example 60_tasks_basic`.

Per CONTEXT.md locked decision: "Simulates background execution with tokio::spawn + sleep for demonstration." The example uses `tokio::spawn` with a `tokio::time::sleep(Duration::from_millis(500))` to simulate background work, then completes the task via `store.complete_with_result()`. This demonstrates the pattern where an external service picks up work asynchronously.
  </action>
  <verify>
Run `cargo run --example 60_tasks_basic` to verify the example compiles and executes successfully, printing the complete lifecycle output. Run `cargo check --example 60_tasks_basic` for a faster compilation check.
  </verify>
  <done>
60_tasks_basic.rs compiles and runs, demonstrating: task creation via tools/call, polling via tasks/get, background completion simulation, and result retrieval via tasks/result. Output is clear and educational.
  </done>
</task>

</tasks>

<verification>
1. `cargo test --package pmcp-tasks --test lifecycle_integration -- --test-threads=1` all tests pass
2. `cargo run --example 60_tasks_basic` runs successfully
3. Full lifecycle: create -> poll -> complete -> get_result works end-to-end
4. tasks/list returns owner-scoped results
5. tasks/cancel transitions to cancelled
6. Normal tool calls still work when tasks are enabled
</verification>

<success_criteria>
- Integration tests cover all task endpoints and lifecycle patterns
- Basic example demonstrates complete task lifecycle
- All tests pass through real ServerCore handle_request path
- Example output is clear and educational
- No regressions in existing tests
</success_criteria>

<output>
After completion, create `.planning/phases/03-handler-middleware-and-server-integration/03-03-SUMMARY.md`
</output>
