From b61c76b19b583d1f958d5c3f8ccb9ed470cc4110 Mon Sep 17 00:00:00 2001 From: Jeremiah Lowin <153965+jlowin@users.noreply.github.com> Date: Tue, 13 Jan 2026 22:14:08 -0500 Subject: [PATCH 1/4] Fix task capabilities location (issue #2870) Tasks belong in capabilities.tasks (first-class field) per SEP-1686, not capabilities.experimental.tasks. This fixes VS Code Copilot 1.107+ integration which checks capabilities.tasks?.requests?.tools?.call. Changes: - Update get_task_capabilities() to return ServerTasksCapability types - Override get_capabilities() in LowLevelServer to set tasks field - Remove experimental_capabilities parameter usage - Update test to verify correct location Fixes #2870 --- src/fastmcp/client/transports.py | 8 +--- src/fastmcp/server/http.py | 8 +--- src/fastmcp/server/low_level.py | 25 +++++++++++ src/fastmcp/server/server.py | 23 ++++------ src/fastmcp/server/tasks/capabilities.py | 45 +++++++++++--------- tests/server/tasks/test_task_capabilities.py | 12 +++--- 6 files changed, 68 insertions(+), 53 deletions(-) diff --git a/src/fastmcp/client/transports.py b/src/fastmcp/client/transports.py index c534642ef..9d0e30158 100644 --- a/src/fastmcp/client/transports.py +++ b/src/fastmcp/client/transports.py @@ -44,7 +44,6 @@ from fastmcp.mcp_config import ( ) 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 @@ -882,16 +881,11 @@ class FastMCPTransport(ClientTransport): 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 e897c0faa..530b6c0c2 100644 --- a/src/fastmcp/server/http.py +++ b/src/fastmcp/server/http.py @@ -21,7 +21,6 @@ from starlette.types import Lifespan, Receive, Scope, Send 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: @@ -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() diff --git a/src/fastmcp/server/low_level.py b/src/fastmcp/server/low_level.py index f6b579e25..7bbaffb7f 100644 --- a/src/fastmcp/server/low_level.py +++ b/src/fastmcp/server/low_level.py @@ -165,6 +165,31 @@ class LowLevelServer(_Server[LifespanResultT, RequestT]): **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 ff5c9a992..d20c10dd6 100644 --- a/src/fastmcp/server/server.py +++ b/src/fastmcp/server/server.py @@ -83,7 +83,6 @@ from fastmcp.server.low_level import LowLevelServer 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, @@ -2383,20 +2382,16 @@ class FastMCP(Generic[LifespanResultT]): 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, + await self._mcp_server.run( + read_stream, + write_stream, + self._mcp_server.create_initialization_options( + notification_options=NotificationOptions( + tools_changed=True ), - stateless=stateless, - ) + ), + stateless=stateless, + ) finally: reset_transport(token) diff --git a/src/fastmcp/server/tasks/capabilities.py b/src/fastmcp/server/tasks/capabilities.py index 8f49fcc9e..d270d2bf3 100644 --- a/src/fastmcp/server/tasks/capabilities.py +++ b/src/fastmcp/server/tasks/capabilities.py @@ -1,7 +1,14 @@ """SEP-1686 task capabilities declaration.""" from importlib.util import find_spec -from typing import Any + +from mcp.types import ( + ServerTasksCapability, + ServerTasksRequestsCapability, + TasksCancelCapability, + TasksListCapability, + TasksToolsCapability, +) def _is_docket_available() -> bool: @@ -9,26 +16,26 @@ def _is_docket_available() -> bool: 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. - 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. - Returns empty dict if pydocket is not installed, so clients - won't see task support advertised. + Returns None if pydocket is not installed (no task support). + + 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 None - return { - "tasks": { - "list": {}, - "cancel": {}, - "requests": { - "tools": {"call": {}}, - "prompts": {"get": {}}, - "resources": {"read": {}}, - }, - } - } + return ServerTasksCapability( + list=TasksListCapability(), + cancel=TasksCancelCapability(), + requests=ServerTasksRequestsCapability( + tools=TasksToolsCapability(call={}), + prompts={"get": {}}, + resources={"read": {}}, + ), + ) diff --git a/tests/server/tasks/test_task_capabilities.py b/tests/server/tasks/test_task_capabilities.py index 2bafcb707..146d80d41 100644 --- a/tests/server/tasks/test_task_capabilities.py +++ b/tests/server/tasks/test_task_capabilities.py @@ -11,7 +11,7 @@ from fastmcp.server.tasks import get_task_capabilities 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_capabilities_include_tasks(): # 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(): From 3c3a62ee7d30779a932f1099d92f63f192791783 Mon Sep 17 00:00:00 2001 From: Jeremiah Lowin <153965+jlowin@users.noreply.github.com> Date: Tue, 13 Jan 2026 22:51:47 -0500 Subject: [PATCH 2/4] Fix type: use TasksCallCapability instead of dict --- src/fastmcp/server/tasks/capabilities.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/fastmcp/server/tasks/capabilities.py b/src/fastmcp/server/tasks/capabilities.py index d270d2bf3..aa803677a 100644 --- a/src/fastmcp/server/tasks/capabilities.py +++ b/src/fastmcp/server/tasks/capabilities.py @@ -5,6 +5,7 @@ from importlib.util import find_spec from mcp.types import ( ServerTasksCapability, ServerTasksRequestsCapability, + TasksCallCapability, TasksCancelCapability, TasksListCapability, TasksToolsCapability, @@ -34,7 +35,7 @@ def get_task_capabilities() -> ServerTasksCapability | None: list=TasksListCapability(), cancel=TasksCancelCapability(), requests=ServerTasksRequestsCapability( - tools=TasksToolsCapability(call={}), + tools=TasksToolsCapability(call=TasksCallCapability()), prompts={"get": {}}, resources={"read": {}}, ), From 4028b648bc28d386923aac87dfcb6723ee82f626 Mon Sep 17 00:00:00 2001 From: Jeremiah Lowin <153965+jlowin@users.noreply.github.com> Date: Tue, 13 Jan 2026 22:54:28 -0500 Subject: [PATCH 3/4] Add type ignores for extra_data kwargs --- src/fastmcp/server/tasks/capabilities.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/fastmcp/server/tasks/capabilities.py b/src/fastmcp/server/tasks/capabilities.py index aa803677a..9f7d4b4f9 100644 --- a/src/fastmcp/server/tasks/capabilities.py +++ b/src/fastmcp/server/tasks/capabilities.py @@ -36,7 +36,7 @@ def get_task_capabilities() -> ServerTasksCapability | None: cancel=TasksCancelCapability(), requests=ServerTasksRequestsCapability( tools=TasksToolsCapability(call=TasksCallCapability()), - prompts={"get": {}}, - resources={"read": {}}, + prompts={"get": {}}, # type: ignore[call-arg] # extra_data for forward compat + resources={"read": {}}, # type: ignore[call-arg] # extra_data for forward compat ), ) From 91df79d43c1a6b98189d23b06b49a2a3e4a1fb78 Mon Sep 17 00:00:00 2001 From: Chris Guidry Date: Wed, 14 Jan 2026 09:33:59 -0500 Subject: [PATCH 4/4] Fix run() call indentation to be inside stdio_server context MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The run() call was outside the async with stdio_server() block, meaning the streams would be closed before being used. 🤖 Generated with Claude Code Co-Authored-By: Claude Opus 4.5 --- src/fastmcp/server/server.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/fastmcp/server/server.py b/src/fastmcp/server/server.py index d20c10dd6..b10f5071f 100644 --- a/src/fastmcp/server/server.py +++ b/src/fastmcp/server/server.py @@ -2382,16 +2382,16 @@ class FastMCP(Generic[LifespanResultT]): f"Starting MCP server {self.name!r} with transport 'stdio'{mode}" ) - await self._mcp_server.run( - read_stream, - write_stream, - self._mcp_server.create_initialization_options( - notification_options=NotificationOptions( - tools_changed=True + await self._mcp_server.run( + read_stream, + write_stream, + self._mcp_server.create_initialization_options( + notification_options=NotificationOptions( + tools_changed=True + ), ), - ), - stateless=stateless, - ) + stateless=stateless, + ) finally: reset_transport(token)