diff --git a/src/fastmcp/client/transports.py b/src/fastmcp/client/transports.py index 85b3dacfb2..62c7f1e210 100644 --- a/src/fastmcp/client/transports.py +++ b/src/fastmcp/client/transports.py @@ -36,7 +36,6 @@ from fastmcp.mcp_config import MCPConfig, infer_transport_type_from_url from fastmcp.server.dependencies import get_http_headers from fastmcp.server.server import FastMCP -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 @@ -900,16 +899,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, ) ) diff --git a/src/fastmcp/server/http.py b/src/fastmcp/server/http.py index 825fcd0aa0..6bd6f6d40d 100644 --- a/src/fastmcp/server/http.py +++ b/src/fastmcp/server/http.py @@ -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: @@ -160,15 +159,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() diff --git a/src/fastmcp/server/low_level.py b/src/fastmcp/server/low_level.py index 536b6aa1f4..fcc9cfb783 100644 --- a/src/fastmcp/server/low_level.py +++ b/src/fastmcp/server/low_level.py @@ -163,6 +163,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], diff --git a/src/fastmcp/server/server.py b/src/fastmcp/server/server.py index 44ac0aab20..14d9183972 100644 --- a/src/fastmcp/server/server.py +++ b/src/fastmcp/server/server.py @@ -75,7 +75,6 @@ ) from fastmcp.server.low_level import LowLevelServer from fastmcp.server.middleware import Middleware, MiddlewareContext -from fastmcp.server.tasks.capabilities import get_task_capabilities from fastmcp.server.tasks.config import TaskConfig from fastmcp.server.tasks.handlers import ( handle_prompt_as_task, @@ -2508,9 +2507,6 @@ async def run_stdio_async( f"Starting MCP server {self.name!r} with transport 'stdio'" ) - # Build experimental capabilities - experimental_capabilities = get_task_capabilities() - await self._mcp_server.run( read_stream, write_stream, @@ -2518,7 +2514,6 @@ async def run_stdio_async( notification_options=NotificationOptions( tools_changed=True ), - experimental_capabilities=experimental_capabilities, ), ) diff --git a/src/fastmcp/server/tasks/capabilities.py b/src/fastmcp/server/tasks/capabilities.py index c08371eca7..b1a5d97fbc 100644 --- a/src/fastmcp/server/tasks/capabilities.py +++ b/src/fastmcp/server/tasks/capabilities.py @@ -1,22 +1,30 @@ """SEP-1686 task capabilities declaration.""" -from typing import Any +from mcp.types import ( + ServerTasksCapability, + ServerTasksRequestsCapability, + TasksCallCapability, + TasksCancelCapability, + TasksListCapability, + TasksToolsCapability, +) -def get_task_capabilities() -> dict[str, Any]: - """Return the SEP-1686 task capabilities structure. +def get_task_capabilities() -> ServerTasksCapability: + """Return the SEP-1686 task capabilities. - This is the standard capabilities map advertised to clients, - declaring support for list, cancel, and request operations. + Returns task capabilities as a first-class ServerCapabilities field, + declaring support for list, cancel, and request operations per SEP-1686. + + Note: prompts/resources are passed via extra_data since the SDK types + don't include them yet (FastMCP supports them ahead of the spec). """ - return { - "tasks": { - "list": {}, - "cancel": {}, - "requests": { - "tools": {"call": {}}, - "prompts": {"get": {}}, - "resources": {"read": {}}, - }, - } - } + 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 + ), + ) diff --git a/tests/server/tasks/test_task_capabilities.py b/tests/server/tasks/test_task_capabilities.py index 2bafcb7078..146d80d417 100644 --- a/tests/server/tasks/test_task_capabilities.py +++ b/tests/server/tasks/test_task_capabilities.py @@ -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() @@ -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():