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 @@ -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

Expand Down Expand Up @@ -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,
)
)
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 @@ -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()

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 @@ -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],
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 @@ -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,
Expand Down Expand Up @@ -2508,17 +2507,13 @@ 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,
self._mcp_server.create_initialization_options(
notification_options=NotificationOptions(
tools_changed=True
),
experimental_capabilities=experimental_capabilities,
),
)

Expand Down
40 changes: 24 additions & 16 deletions src/fastmcp/server/tasks/capabilities.py
Original file line number Diff line number Diff line change
@@ -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
),
)
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