Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 1 addition & 7 deletions src/fastmcp/client/transports.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,6 @@
)
from fastmcp.server.dependencies import get_http_headers
from fastmcp.server.server import FastMCP, create_proxy
from fastmcp.server.tasks.capabilities import get_task_capabilities
from fastmcp.utilities.logging import get_logger
from fastmcp.utilities.mcp_server_config.v1.environments.uv import UVEnvironment

Expand Down Expand Up @@ -882,16 +881,11 @@ async def connect_session(
anyio.create_task_group() as tg,
_enter_server_lifespan(server=self.server),
):
# Build experimental capabilities
experimental_capabilities = get_task_capabilities()

tg.start_soon(
lambda: self.server._mcp_server.run(
server_read,
server_write,
self.server._mcp_server.create_initialization_options(
experimental_capabilities=experimental_capabilities
),
self.server._mcp_server.create_initialization_options(),
raise_exceptions=self.raise_exceptions,
)
)
Expand Down
8 changes: 1 addition & 7 deletions src/fastmcp/server/http.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@

from fastmcp.server.auth import AuthProvider
from fastmcp.server.auth.middleware import RequireAuthMiddleware
from fastmcp.server.tasks.capabilities import get_task_capabilities
from fastmcp.utilities.logging import get_logger

if TYPE_CHECKING:
Expand Down Expand Up @@ -169,15 +168,10 @@ def create_sse_app(
# Create handler for SSE connections
async def handle_sse(scope: Scope, receive: Receive, send: Send) -> Response:
async with sse.connect_sse(scope, receive, send) as streams:
# Build experimental capabilities
experimental_capabilities = get_task_capabilities()

await server._mcp_server.run(
streams[0],
streams[1],
server._mcp_server.create_initialization_options(
experimental_capabilities=experimental_capabilities
),
server._mcp_server.create_initialization_options(),
)
return Response()

Expand Down
25 changes: 25 additions & 0 deletions src/fastmcp/server/low_level.py
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,31 @@ def create_initialization_options(
**kwargs,
)

def get_capabilities(
self,
notification_options: NotificationOptions,
experimental_capabilities: dict[str, dict[str, Any]],
) -> mcp.types.ServerCapabilities:
"""Override to set capabilities.tasks as a first-class field per SEP-1686.

This ensures task capabilities appear in capabilities.tasks instead of
capabilities.experimental.tasks, which is required by the MCP spec and
enables proper task detection by clients like VS Code Copilot 1.107+.
"""
from fastmcp.server.tasks.capabilities import get_task_capabilities

# Get base capabilities from SDK (pass empty dict for experimental)
# since we'll set tasks as a first-class field instead
capabilities = super().get_capabilities(
notification_options,
experimental_capabilities or {},
)

# Set tasks as a first-class field (not experimental) per SEP-1686
capabilities.tasks = get_task_capabilities()

return capabilities

async def run(
self,
read_stream: MemoryObjectReceiveStream[SessionMessage | Exception],
Expand Down
5 changes: 0 additions & 5 deletions src/fastmcp/server/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,6 @@
from fastmcp.server.middleware import Middleware, MiddlewareContext
from fastmcp.server.providers import LocalProvider, Provider
from fastmcp.server.providers.aggregate import AggregateProvider
from fastmcp.server.tasks.capabilities import get_task_capabilities
from fastmcp.server.tasks.config import TaskConfig, TaskMeta
from fastmcp.server.transforms import (
Namespace,
Expand Down Expand Up @@ -2383,17 +2382,13 @@ async def run_stdio_async(
f"Starting MCP server {self.name!r} with transport 'stdio'{mode}"
)

# Build experimental capabilities
experimental_capabilities = get_task_capabilities()

await self._mcp_server.run(
read_stream,
write_stream,
self._mcp_server.create_initialization_options(
notification_options=NotificationOptions(
tools_changed=True
),
experimental_capabilities=experimental_capabilities,
),
stateless=stateless,
)
Expand Down
48 changes: 28 additions & 20 deletions src/fastmcp/server/tasks/capabilities.py
Original file line number Diff line number Diff line change
@@ -1,34 +1,42 @@
"""SEP-1686 task capabilities declaration."""

from importlib.util import find_spec
from typing import Any

from mcp.types import (
ServerTasksCapability,
ServerTasksRequestsCapability,
TasksCallCapability,
TasksCancelCapability,
TasksListCapability,
TasksToolsCapability,
)


def _is_docket_available() -> bool:
"""Check if pydocket is installed (local to avoid circular import)."""
return find_spec("docket") is not None


def get_task_capabilities() -> dict[str, Any]:
"""Return the SEP-1686 task capabilities structure.
def get_task_capabilities() -> ServerTasksCapability | None:
"""Return the SEP-1686 task capabilities.

Returns task capabilities as a first-class ServerCapabilities field,
declaring support for list, cancel, and request operations per SEP-1686.

This is the standard capabilities map advertised to clients,
declaring support for list, cancel, and request operations.
Returns None if pydocket is not installed (no task support).

Returns empty dict if pydocket is not installed, so clients
won't see task support advertised.
Note: prompts/resources are passed via extra_data since the SDK types
don't include them yet (FastMCP supports them ahead of the spec).
"""
if not _is_docket_available():
return {}

return {
"tasks": {
"list": {},
"cancel": {},
"requests": {
"tools": {"call": {}},
"prompts": {"get": {}},
"resources": {"read": {}},
},
}
}
return None

return ServerTasksCapability(
list=TasksListCapability(),
cancel=TasksCancelCapability(),
requests=ServerTasksRequestsCapability(
tools=TasksToolsCapability(call=TasksCallCapability()),
prompts={"get": {}}, # type: ignore[call-arg] # extra_data for forward compat
resources={"read": {}}, # type: ignore[call-arg] # extra_data for forward compat
),
)
12 changes: 6 additions & 6 deletions tests/server/tasks/test_task_capabilities.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@


async def test_capabilities_include_tasks():
"""Server capabilities always include tasks."""
"""Server capabilities always include tasks in first-class field (SEP-1686)."""
mcp = FastMCP("capability-test")

@mcp.tool()
Expand All @@ -22,11 +22,11 @@ async def test_tool() -> str:
# Get server initialization result which includes capabilities
init_result = client.initialize_result

# Verify tasks capability is present
assert init_result.capabilities.experimental is not None
assert "tasks" in init_result.capabilities.experimental
tasks_cap = init_result.capabilities.experimental["tasks"]
assert tasks_cap == get_task_capabilities()["tasks"]
# Verify tasks capability is present as a first-class field (not experimental)
assert init_result.capabilities.tasks is not None
assert init_result.capabilities.tasks == get_task_capabilities()
# Verify it's NOT in experimental
assert "tasks" not in (init_result.capabilities.experimental or {})


async def test_client_uses_task_capable_session():
Expand Down