Skip to content

Commit 11ae638

Browse files
authored
Merge pull request #2574 from jlowin/task-proxy-mount-tests
Forbid task execution through proxies, add mount/proxy task tests
2 parents 61108a0 + a335a9d commit 11ae638

File tree

9 files changed

+914
-81
lines changed

9 files changed

+914
-81
lines changed

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ dependencies = [
1010
"mcp>=1.23.1",
1111
"openapi-pydantic>=0.5.1",
1212
"platformdirs>=4.0.0",
13-
"pydocket>=0.14.0",
13+
"pydocket>=0.15.2",
1414
"rich>=13.9.4",
1515
"cyclopts>=4.0.0",
1616
"authlib>=1.6.5",

src/fastmcp/cli/tasks.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ def worker(
8282

8383
# Load server to get task functions
8484
try:
85-
config, resolved_spec = load_and_merge_config(server_spec)
85+
config, _resolved_spec = load_and_merge_config(server_spec)
8686
except FileNotFoundError:
8787
sys.exit(1)
8888

src/fastmcp/client/client.py

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -849,9 +849,12 @@ async def _read_resource_as_task(
849849
)
850850

851851
# Check if server accepted background execution
852-
if result.meta and "modelcontextprotocol.io/task" in result.meta:
852+
# If response includes task metadata with taskId, server accepted background mode
853+
# If response includes returned_immediately=True, server declined and executed sync
854+
task_meta = (result.meta or {}).get("modelcontextprotocol.io/task", {})
855+
if task_meta.get("taskId"):
853856
# Background execution accepted - extract server-generated taskId
854-
server_task_id = result.meta["modelcontextprotocol.io/task"]["taskId"]
857+
server_task_id = task_meta["taskId"]
855858
# Track this task ID for list_tasks()
856859
self._submitted_task_ids.add(server_task_id)
857860

@@ -1055,9 +1058,12 @@ async def _get_prompt_as_task(
10551058
)
10561059

10571060
# Check if server accepted background execution
1058-
if result.meta and "modelcontextprotocol.io/task" in result.meta:
1061+
# If response includes task metadata with taskId, server accepted background mode
1062+
# If response includes returned_immediately=True, server declined and executed sync
1063+
task_meta = (result.meta or {}).get("modelcontextprotocol.io/task", {})
1064+
if task_meta.get("taskId"):
10591065
# Background execution accepted - extract server-generated taskId
1060-
server_task_id = result.meta["modelcontextprotocol.io/task"]["taskId"]
1066+
server_task_id = task_meta["taskId"]
10611067
# Track this task ID for list_tasks()
10621068
self._submitted_task_ids.add(server_task_id)
10631069

@@ -1397,10 +1403,12 @@ async def _call_tool_as_task(
13971403
)
13981404

13991405
# Check if server accepted background execution
1400-
# If response includes task metadata, server accepted background mode
1401-
if result.meta and "modelcontextprotocol.io/task" in result.meta:
1406+
# If response includes task metadata with taskId, server accepted background mode
1407+
# If response includes returned_immediately=True, server declined and executed sync
1408+
task_meta = (result.meta or {}).get("modelcontextprotocol.io/task", {})
1409+
if task_meta.get("taskId"):
14021410
# Background execution accepted - extract server-generated taskId
1403-
server_task_id = result.meta["modelcontextprotocol.io/task"]["taskId"]
1411+
server_task_id = task_meta["taskId"]
14041412
# Track this task ID for list_tasks()
14051413
self._submitted_task_ids.add(server_task_id)
14061414

@@ -1415,8 +1423,8 @@ async def _call_tool_as_task(
14151423
return task_obj
14161424
else:
14171425
# Server declined background execution (graceful degradation)
1418-
# Executed synchronously - wrap the immediate result
1419-
# Need to convert mcp.types.CallToolResult to our CallToolResult
1426+
# or returned_immediately=True - executed synchronously
1427+
# Wrap the immediate result
14201428
parsed_result = await self._parse_call_tool_result(name, result)
14211429
# Use a synthetic task ID for the immediate result
14221430
synthetic_task_id = task_id or str(uuid.uuid4())

src/fastmcp/server/proxy.py

Lines changed: 32 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
from fastmcp.server.context import Context
3636
from fastmcp.server.dependencies import get_context
3737
from fastmcp.server.server import FastMCP
38+
from fastmcp.server.tasks.config import TaskConfig
3839
from fastmcp.tools.tool import Tool, ToolResult
3940
from fastmcp.tools.tool_manager import ToolManager
4041
from fastmcp.tools.tool_transform import (
@@ -259,6 +260,8 @@ class ProxyTool(Tool, MirroredComponent):
259260
A Tool that represents and executes a tool on a remote server.
260261
"""
261262

263+
task_config: TaskConfig = TaskConfig(mode="forbidden")
264+
262265
def __init__(self, client: Client, **kwargs: Any):
263266
super().__init__(**kwargs)
264267
self._client = client
@@ -288,21 +291,36 @@ async def run(
288291
"""Executes the tool by making a call through the client."""
289292
async with self._client:
290293
context = get_context()
291-
# try to get request context meta
292-
meta = (
293-
dict(context.request_context.meta)
294-
if hasattr(context, "request_context")
295-
and hasattr(context.request_context, "meta")
296-
else None
297-
)
294+
# Build meta dict from request context
295+
meta: dict[str, Any] | None = None
296+
if hasattr(context, "request_context"):
297+
req_ctx = context.request_context
298+
# Start with existing meta if present
299+
if hasattr(req_ctx, "meta") and req_ctx.meta:
300+
meta = dict(req_ctx.meta)
301+
# Add task metadata if this is a task request
302+
if (
303+
hasattr(req_ctx, "experimental")
304+
and hasattr(req_ctx.experimental, "is_task")
305+
and req_ctx.experimental.is_task
306+
):
307+
task_metadata = req_ctx.experimental.task_metadata
308+
if task_metadata:
309+
meta = meta or {}
310+
meta["modelcontextprotocol.io/task"] = task_metadata.model_dump(
311+
exclude_none=True
312+
)
313+
298314
result = await self._client.call_tool_mcp(
299315
name=self.name, arguments=arguments, meta=meta
300316
)
301317
if result.isError:
302318
raise ToolError(cast(mcp.types.TextContent, result.content[0]).text)
319+
# Preserve backend's meta (includes task metadata for background tasks)
303320
return ToolResult(
304321
content=result.content,
305322
structured_content=result.structuredContent,
323+
meta=result.meta,
306324
)
307325

308326

@@ -311,6 +329,7 @@ class ProxyResource(Resource, MirroredComponent):
311329
A Resource that represents and reads a resource from a remote server.
312330
"""
313331

332+
task_config: TaskConfig = TaskConfig(mode="forbidden")
314333
_client: Client
315334
_value: str | bytes | None = None
316335

@@ -343,6 +362,7 @@ def from_mcp_resource(
343362
icons=mcp_resource.icons,
344363
meta=mcp_resource.meta,
345364
tags=(mcp_resource.meta or {}).get("_fastmcp", {}).get("tags", []),
365+
task_config=TaskConfig(mode="forbidden"),
346366
_mirrored=True,
347367
)
348368

@@ -366,6 +386,8 @@ class ProxyTemplate(ResourceTemplate, MirroredComponent):
366386
A ResourceTemplate that represents and creates resources from a remote server template.
367387
"""
368388

389+
task_config: TaskConfig = TaskConfig(mode="forbidden")
390+
369391
def __init__(self, client: Client, **kwargs: Any):
370392
super().__init__(**kwargs)
371393
self._client = client
@@ -386,6 +408,7 @@ def from_mcp_template( # type: ignore[override]
386408
parameters={}, # Remote templates don't have local parameters
387409
meta=mcp_template.meta,
388410
tags=(mcp_template.meta or {}).get("_fastmcp", {}).get("tags", []),
411+
task_config=TaskConfig(mode="forbidden"),
389412
_mirrored=True,
390413
)
391414

@@ -431,6 +454,7 @@ class ProxyPrompt(Prompt, MirroredComponent):
431454
A Prompt that represents and renders a prompt from a remote server.
432455
"""
433456

457+
task_config: TaskConfig = TaskConfig(mode="forbidden")
434458
_client: Client
435459

436460
def __init__(self, client: Client, **kwargs):
@@ -459,6 +483,7 @@ def from_mcp_prompt(
459483
icons=mcp_prompt.icons,
460484
meta=mcp_prompt.meta,
461485
tags=(mcp_prompt.meta or {}).get("_fastmcp", {}).get("tags", []),
486+
task_config=TaskConfig(mode="forbidden"),
462487
_mirrored=True,
463488
)
464489

0 commit comments

Comments
 (0)