Skip to content

Conversation

@chrisguidry
Copy link
Collaborator

This PR brings FastMCP's task implementation into near-complete compliance with the SEP-1686 final specification. We've fixed breaking changes, reconciled with the SDK draft, and implemented optional features.

What Changed

Phase 1 - Breaking Changes:

  • Server-generated task IDs (clients no longer provide them)
  • Tasks start in "working" status instead of "submitted"
  • Replaced keepAlive with ttl field
  • Added required createdAt timestamps

Phase 2 - Protocol Compliance:

  • Implemented all 5 task protocol methods (get/result/list/cancel/delete)
  • Added tasks/cancel support (was missing)
  • Fixed request/response validation with proper SDK union patching
  • Wire format now matches spec exactly

Phase 2.1 - Parameter Structure:

  • Moved task metadata from _meta to spec-compliant params.task field
  • Dual-path extraction handles both HTTP (spec) and in-memory (SDK) transports

Phase 3 - SDK Reconciliation:

  • Renamed all types to match SDK draft conventions
  • Documented where SDK diverges from spec (keepAlive vs ttl, missing createdAt)
  • Added proper SDK-compatible typing throughout

New Features:

  • statusMessage field populated from Docket execution progress
  • taskHint annotation auto-populated from tool.task flag
  • Server-push notifications/tasks/status (optional optimization)
    • Client API: task.on_status_change(callback) for status updates
    • Event-based waiting wakes immediately on notifications
    • Status caching reduces polling load

Test Coverage

Added 15 new test files covering:

  • All protocol methods and error cases
  • Graceful degradation when Docket unavailable
  • Task cancellation/deletion workflows
  • Status notifications and client callbacks
  • TTL behavior and keepalive semantics

Test results: 3261 passed, 2 skipped

Spec Compliance

Created sep-1686-compliance.md documenting compliance with all spec sections. We're at ~95% implementation coverage. The only major gap is input_required workflow (requires UI/sampling integration).

This builds on the foundation from #2412 (Phase 0) and implements everything needed for production-ready background task support.

🤖 Generated with Claude Code

chrisguidry and others added 12 commits November 17, 2025 15:55
Created comprehensive analysis of the final SEP-1686 specification versus
our current implementation. Identified 5 major breaking changes and defined
a phased implementation approach across multiple sessions.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
Brings task implementation in line with final SEP-1686 specification:

- Server generates task IDs (clients no longer provide them)
- Field renames: keepAlive → ttl, pollFrequency → pollInterval
- Add required createdAt timestamp to all task responses
- Tasks begin in "working" status instead of "submitted"
- Update Docket state mappings (SCHEDULED/QUEUED → working)

Client changes:
- Send only {ttl: ...} in task metadata (no taskId)
- Extract server-generated taskId from response
- Update all task types (tools, prompts, resources)

All 147 task tests passing, full suite clean (3241 passed).

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
Per final spec lines 447-448 and 49-63:

- Remove related-task metadata from tasks/get responses (should NOT include per spec)
- Remove related-task metadata from tasks/cancel responses (should NOT include per spec)
- Keep related-task metadata in tasks/result responses (MUST include per spec)
- Update capabilities structure to nested format with list, cancel, and requests keys

All tests passing (3241 passed, 147 task tests passing).

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
Renamed all task protocol types to match MCP SDK draft naming conventions while keeping our spec-compliant field corrections.

Type name changes (old → new):
- TasksGetRequest → GetTaskRequest
- TasksResultRequest → GetTaskPayloadRequest
- TasksListRequest → ListTasksRequest
- TasksCancelRequest → CancelTaskRequest (SDK doesn't have yet)
- TasksDeleteRequest → DeleteTaskRequest

Added heavy documentation explaining where SDK diverges from final spec:
- SDK uses `keepAlive` (wrong, spec requires `ttl`)
- SDK missing `createdAt` field (required per spec line 430)
- SDK allows "submitted" initial status (spec requires "working")

Our shims keep the spec-compliant corrections and will be easy to replace when SDK is updated to match final spec.

Files updated:
- src/fastmcp/server/tasks/_temporary_mcp_shims.py
- src/fastmcp/server/tasks/protocol.py
- src/fastmcp/client/client.py

All 3241 tests passing, zero regressions.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
Updated all protocol handlers to return proper SDK-aligned typed results instead of dicts, improving type safety and preparing for SDK compatibility.

Handler return types:
- tasks_get_handler → GetTaskResult (spec-compliant with ttl, createdAt)
- tasks_list_handler → ListTasksResult
- tasks_cancel_handler → GetTaskResult
- tasks_delete_handler → EmptyResult

The wrappers serialize these typed results back to dicts before wrapping in ServerResult, maintaining compatibility with the MCP SDK's union types while giving us type safety in the handler layer.

This is the right approach: handlers use strong types following SDK conventions, serialization happens at the boundary layer.

All 3241 tests passing, zero regressions.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
Moved task metadata from `_meta: {"modelcontextprotocol.io/task": ...}` to spec-compliant `params.task: {ttl: ...}` as a direct parameter field alongside name/arguments.

Monkeypatch Strategy (Elegant):
- Extended SDK param types (CallToolRequestParams, GetPromptRequestParams, ReadResourceRequestParams) to add `task` field
- Monkeypatched them back into SDK module so ALL SDK code uses extended versions
- Minimal code changes, maximum compatibility

Wire Format Change:
```json
// Before (SDK draft, wrong):
{"params": {"name": "...", "_meta": {"modelcontextprotocol.io/task": {}}}}

// After (final spec, correct):
{"params": {"name": "...", "task": {"ttl": 60000}}}
```

Client changes:
- Send task as direct param field (spec-compliant for wire format)
- Also send _meta for SDK in-memory transport compatibility

Server changes:
- Dual-path extraction handles both HTTP and in-memory transports
- HTTP: extract from request.params.task (spec-compliant)
- In-memory: extract from req_ctx.meta via model_dump (SDK aliasing)

Snapshot updates:
- Updated logging middleware test snapshots to include task field in payload

All 3241 tests passing, 147 task tests passing, zero regressions.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
Quick compliance wins from sep-1686-compliance.md analysis:

1. Add statusMessage field (spec line 403):
   - Added to GetTaskResult type as optional field
   - Populated from Docket's execution.progress.message when available
   - For failed tasks, uses "Task failed: {error}"
   - For cancelled tasks, uses "Task cancelled"
   - For unknown tasks, uses "Task not found"

2. Fix TTL tests (un-skip 2 tests):
   - test_keepalive_returned_in_submitted_state - Now passes
   - test_keepalive_returned_in_completed_state - Now passes
   - Updated assertions to expect server's global TTL (60000ms)
   - Added TODO comments explaining Docket uses global execution_ttl
   - Documented that overriding client TTL is spec-compliant (line 431)
   - Kept test_expired_task_returns_unknown skipped (needs per-task TTL)

Files changed:
- src/fastmcp/server/tasks/_temporary_mcp_shims.py - Added statusMessage field
- src/fastmcp/server/tasks/protocol.py - Populate statusMessage from Docket
- tests/server/tasks/test_task_keepalive.py - Un-skipped and fixed 2 tests

Test results: 3243 passed (up from 3241), 2 skipped (down from 4).

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
Added automatic taskHint annotation population for tools based on their task flag, enabling protocol-level capability negotiation.

Implementation:
- Modified Tool.to_mcp_tool() to auto-populate taskHint from tool.task flag
- Mapping: task=True → taskHint=True (SDK boolean, spec wants "optional" string)
- Mapping: task=False → taskHint=None (omitted, default "never")
- Preserves explicit taskHint annotations (no override)
- Creates ToolAnnotations if needed

Note: SDK currently uses bool type, spec wants string values ("always"/"optional"/"never").
Using bool for now until SDK is updated to match final spec.

Test coverage:
- test_task_hint_auto_populated_for_task_enabled_tool
- test_task_hint_omitted_for_task_disabled_tool
- test_explicit_task_hint_not_overridden

Files changed:
- src/fastmcp/tools/tool.py - Added taskHint auto-population logic
- tests/server/test_tool_annotations.py - Added 3 new test cases

Test results: 3246 passed (up from 3243), all new tests passing.

Closes taskHint gap from sep-1686-compliance.md.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
Added optional proactive status notifications using per-session subscription tasks that monitor Docket execution state changes.

Architecture:
- Per-session subscription tasks spawn when tasks are created
- Each subscription uses Docket's execution.subscribe() AsyncGenerator
- Subscriptions send notifications/tasks/status when state changes
- Auto-cleanup when task completes OR session disconnects
- Uses session._task_group for lifecycle management (no manual cleanup needed)

Implementation:
- Created subscriptions.py with subscribe_to_task_updates() helper
- Spawns subscription in all three task handlers (tools/prompts/resources)
- Notification includes full Task object (taskId, status, createdAt, ttl, etc.)
- No related-task metadata per spec line 454
- Maps Docket states to MCP status values

Test coverage (7 new tests):
- test_subscription_spawned_for_tool_task
- test_subscription_handles_task_completion
- test_subscription_handles_task_failure
- test_subscription_for_prompt_tasks
- test_subscription_for_resource_tasks
- test_subscriptions_cleanup_on_session_disconnect
- test_multiple_concurrent_subscriptions

Files changed:
- src/fastmcp/server/tasks/subscriptions.py (new) - Subscription helper
- src/fastmcp/server/tasks/handlers.py - Spawn subscriptions in 3 handlers
- tests/server/tasks/test_task_status_notifications.py (new) - 7 tests

Test results: 3253 passed (up from 3246), all new tests passing.

This is an optional spec feature that reduces client polling frequency.
Clients still poll via tasks/get (MUST NOT rely on notifications per spec).

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
Server now pushes status updates to clients when tasks change state, reducing
unnecessary polling. Clients can register callbacks via task.on_status_change()
and get immediate notifications when tasks complete, fail, or transition states.

The implementation subscribes to Docket execution events and sends MCP
notifications/tasks/status messages. Client-side caching and event-based waiting
wake up immediately on notifications instead of polling every second.

This is the optional optimization from SEP-1686 lines 436-444. Servers MAY send
these notifications to reduce client polling load.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
@marvin-context-protocol marvin-context-protocol bot added feature Major new functionality. Reserved for 2-4 significant PRs per release. Not for issues. breaking change Breaks backward compatibility. Requires minor version bump. Critical for maintainer attention. server Related to FastMCP server implementation or server-side functionality. client Related to the FastMCP client SDK or client-side functionality. labels Nov 18, 2025
@marvin-context-protocol
Copy link
Contributor

Test Failure Analysis

Summary: The static analysis job failed because detected a formatting issue in .

Root Cause: There's a blank line with trailing whitespace after the docstring in the run_v1_server_async function (line 214). The ruff format hook automatically removes trailing whitespace, but this causes the CI check to fail because files were modified.

Suggested Solution: Remove the trailing whitespace from line 214 in src/fastmcp/cli/run.py. You can fix this by running:

uv run prek run --all-files

This will automatically format the file correctly. Then commit the changes.

Detailed Analysis

The failure occurred in the static_analysis job when running prek run --all-files. The output shows:

ruff format.............................................................Failed
- hook id: ruff-format
- files were modified by this hook
  1 file reformatted, 414 files left unchanged

The diff shows the specific issue:

diff --git a/src/fastmcp/cli/run.py b/src/fastmcp/cli/run.py
index 14a89d7..b0a2e96 100644
--- a/src/fastmcp/cli/run.py
+++ b/src/fastmcp/cli/run.py
@@ -211,7 +211,7 @@ async def run_v1_server_async(
      port: Port to bind to
      transport: Transport protocol to use
  """
--    
-+
  if host:
      server.settings.host = host
  if port:

The line after the docstring closing (""") contains trailing spaces that need to be removed.

Related Files
  • src/fastmcp/cli/run.py:214 - Contains the trailing whitespace that needs to be removed

Comment on lines 111 to 112
class ClientMessageHandler(MessageHandler):
"""MessageHandler that routes task status notifications to Task objects."""
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's rename _temporary_task_capability_shim.py to _temporary_sep_1686_shims.py to match the server-side, and then move this ClientMessageHandler class there instead.

Comment on lines 1466 to 1467
GetTaskParams, # SDK-compatible name (was TasksGetParams)
GetTaskRequest, # SDK-compatible name (was TasksGetRequest)
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't need these comments like "# SDK-compatible name" this is referring to unreleased prior work, no need for these. Same throughout this module

Comment on lines 45 to 47
createdAt: str
ttl: int | None = None
pollInterval: int | None = None
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah okay, we should move all of these temporary low-level SDK pydantic shims for different requests/responses into that new _temporary_sep_1686_shims.py file we're going to create, these shouldn't live here

TasksDeleteRequest,
TasksListRequest,
TasksCancelRequest,
GetTaskRequest, # SDK-compatible name (was TasksGetRequest)
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These comments are supremely unhelpful

Comment on lines +29 to +30
ExecutionState.SCHEDULED: "working", # Initial state per spec
ExecutionState.QUEUED: "working", # Initial state per spec
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

comment is unhelpful

)


async def tasks_delete_handler(server: FastMCP, params: dict[str, Any]):
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Specify the return types


# Create wrappers that adapt Request objects to dict params and wrap results
async def tasks_get_wrapper(req: TasksGetRequest) -> mcp.types.ServerResult:
# Using SDK-compatible type names (Phase 3: SDK Type Reconciliation)
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These comments again, are supremely unhelpful, remove all this about phases, etc

docket: Docket instance for subscribing to execution events
"""
try:
logger.info(f"[SUBSCRIPTION] Starting for task {task_id}, key={task_key}")
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stray log

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Several stray logs in here, keep the warning but remove the info and debug ones

req_ctx = request_ctx.get()
# Get task metadata from req_ctx.meta (Pydantic RequestParams.Meta object)
if req_ctx.meta:
# Per SEP-1686 final spec: task is direct param, not in _meta
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't need comments for changes here

@@ -0,0 +1,1106 @@
# SEP-1686 Tasks Specification Compliance Report
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oops, let's remove this bookkeeping/planning file

chrisguidry and others added 2 commits November 18, 2025 17:31
Cleaned up code style and organization:
- Consolidated client shims into _temporary_sep_1686_shims.py
- Moved ClientMessageHandler, CallToolResult, TaskStatusResponse to shims
- Removed temporal/change-context comments throughout
- Added proper return type annotations to protocol handlers
- Cleaned up verbose logging in subscriptions
- Renamed test_task_keepalive.py to test_task_ttl.py
- Removed outdated test_expired_task_returns_unknown test
- Deleted sep-1686-compliance.md planning file

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
@chrisguidry chrisguidry merged commit 4d42569 into sep-1686 Nov 19, 2025
7 checks passed
@chrisguidry chrisguidry deleted the sep-1686-final-spec branch November 19, 2025 14:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

breaking change Breaks backward compatibility. Requires minor version bump. Critical for maintainer attention. client Related to the FastMCP client SDK or client-side functionality. feature Major new functionality. Reserved for 2-4 significant PRs per release. Not for issues. server Related to FastMCP server implementation or server-side functionality.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants