60e0ffc959
Upgrade checks / Notify on failure (push) Has been cancelled
Upgrade checks / Close issue on success (push) Has been cancelled
Schema Crash Test / Real-world schema crash test (232K schemas) (push) Has been cancelled
Run static analysis / static_analysis (push) Has been cancelled
Tests / Tests: Python 3.10 on ubuntu-latest (push) Has been cancelled
Tests / Tests: Python 3.13 on ubuntu-latest (push) Has been cancelled
Tests / Tests: Python 3.10 on windows-latest (push) Has been cancelled
Tests / Tests with lowest-direct dependencies (push) Has been cancelled
Tests / MCP conformance tests (push) Has been cancelled
Tests / Integration tests (push) Has been cancelled
Tests / Package install smoke (push) Has been cancelled
Upgrade checks / Static analysis (push) Has been cancelled
Upgrade checks / Tests: Python 3.10 on ubuntu-latest (push) Has been cancelled
Upgrade checks / Tests: Python 3.13 on ubuntu-latest (push) Has been cancelled
Upgrade checks / Tests: Python 3.10 on windows-latest (push) Has been cancelled
Upgrade checks / Integration tests (push) Has been cancelled
Update MCPServerConfig Schema / update-config-schema (push) Has been cancelled
Update SDK Documentation / update-sdk-docs (push) Has been cancelled
193 lines
7.7 KiB
Python
193 lines
7.7 KiB
Python
"""
|
|
Tests for MCP SEP-1686 task protocol behavior through proxy servers.
|
|
|
|
Proxy servers explicitly forbid task-augmented execution. All proxy components
|
|
(tools, prompts, resources) have task_config.mode="forbidden".
|
|
|
|
Clients connecting through proxies can:
|
|
- Execute tools/prompts/resources normally (sync execution)
|
|
- NOT use task-augmented execution (task=True fails gracefully for tools,
|
|
raises MCPError for prompts/resources)
|
|
"""
|
|
|
|
import pytest
|
|
from mcp.shared.exceptions import MCPError
|
|
from mcp_types import TextContent, TextResourceContents
|
|
|
|
from fastmcp import FastMCP
|
|
from fastmcp.client import Client
|
|
from fastmcp.client.transports import FastMCPTransport
|
|
from fastmcp.server import create_proxy
|
|
|
|
|
|
@pytest.fixture
|
|
def backend_server() -> FastMCP:
|
|
"""Create a backend server with task-enabled components.
|
|
|
|
The backend has tasks enabled, but the proxy should NOT forward
|
|
task execution - it should treat all components as forbidden.
|
|
"""
|
|
mcp = FastMCP("backend-server")
|
|
|
|
@mcp.tool(task=True)
|
|
async def add_numbers(a: int, b: int) -> int:
|
|
"""Add two numbers together."""
|
|
return a + b
|
|
|
|
@mcp.tool(task=False)
|
|
async def sync_only_tool(message: str) -> str:
|
|
"""Tool that only supports synchronous execution."""
|
|
return f"sync: {message}"
|
|
|
|
@mcp.prompt(task=True)
|
|
async def greeting_prompt(name: str) -> str:
|
|
"""A prompt that can execute as a task."""
|
|
return f"Hello, {name}! Welcome to the system."
|
|
|
|
@mcp.resource("data://info.txt", task=True)
|
|
async def info_resource() -> str:
|
|
"""A resource that can be read as a task."""
|
|
return "Important information from the backend"
|
|
|
|
@mcp.resource("data://user/{user_id}.json", task=True)
|
|
async def user_resource(user_id: str) -> str:
|
|
"""A resource template that can execute as a task."""
|
|
return f'{{"id": "{user_id}", "name": "User {user_id}"}}'
|
|
|
|
return mcp
|
|
|
|
|
|
@pytest.fixture
|
|
def proxy_server(backend_server: FastMCP) -> FastMCP:
|
|
"""Create a proxy server that forwards to the backend."""
|
|
return create_proxy(FastMCPTransport(backend_server))
|
|
|
|
|
|
class TestProxyToolsSyncExecution:
|
|
"""Test that tools work normally through proxy (sync execution)."""
|
|
|
|
async def test_tool_sync_execution_works(self, proxy_server: FastMCP):
|
|
"""Tool called without task=True works through proxy."""
|
|
async with Client(proxy_server) as client:
|
|
result = await client.call_tool("add_numbers", {"a": 5, "b": 3})
|
|
assert "8" in str(result)
|
|
|
|
async def test_sync_only_tool_works(self, proxy_server: FastMCP):
|
|
"""Sync-only tool works through proxy."""
|
|
async with Client(proxy_server) as client:
|
|
result = await client.call_tool("sync_only_tool", {"message": "test"})
|
|
assert "sync: test" in str(result)
|
|
|
|
|
|
class TestProxyToolsTaskForbidden:
|
|
"""Test that tools with task=True are forbidden through proxy."""
|
|
|
|
async def test_tool_task_returns_error_immediately(self, proxy_server: FastMCP):
|
|
"""Tool called with task=True through proxy returns error immediately."""
|
|
async with Client(proxy_server) as client:
|
|
task = await client.call_tool(
|
|
"add_numbers", {"a": 5, "b": 3}, task=True, raise_on_error=False
|
|
)
|
|
|
|
# Should return immediately (forbidden behavior)
|
|
assert task.returned_immediately
|
|
|
|
# Result should be an error
|
|
result = await task.result()
|
|
assert result.is_error
|
|
|
|
async def test_sync_only_tool_task_returns_error_immediately(
|
|
self, proxy_server: FastMCP
|
|
):
|
|
"""Sync-only tool with task=True also returns error immediately."""
|
|
async with Client(proxy_server) as client:
|
|
task = await client.call_tool(
|
|
"sync_only_tool",
|
|
{"message": "test"},
|
|
task=True,
|
|
raise_on_error=False,
|
|
)
|
|
|
|
assert task.returned_immediately
|
|
result = await task.result()
|
|
assert result.is_error
|
|
|
|
|
|
class TestProxyPromptsSyncExecution:
|
|
"""Test that prompts work normally through proxy (sync execution)."""
|
|
|
|
async def test_prompt_sync_execution_works(self, proxy_server: FastMCP):
|
|
"""Prompt called without task=True works through proxy."""
|
|
async with Client(proxy_server) as client:
|
|
result = await client.get_prompt("greeting_prompt", {"name": "Alice"})
|
|
assert isinstance(result.messages[0].content, TextContent)
|
|
assert "Hello, Alice!" in result.messages[0].content.text
|
|
|
|
|
|
class TestProxyPromptsTaskForbidden:
|
|
"""Test that prompts with task=True are forbidden through proxy."""
|
|
|
|
@pytest.mark.xfail(
|
|
reason="SDK v2 has no `task` field on GetPromptRequestParams / "
|
|
"ReadResourceRequestParams; prompt/resource task submission is not "
|
|
"wire-expressible and always graceful-degrades (sdk-feedback #3).",
|
|
strict=True,
|
|
)
|
|
async def test_prompt_task_raises_mcp_error(self, proxy_server: FastMCP):
|
|
"""Prompt called with task=True through proxy raises MCPError."""
|
|
async with Client(proxy_server) as client:
|
|
with pytest.raises(MCPError) as exc_info:
|
|
await client.get_prompt("greeting_prompt", {"name": "Alice"}, task=True)
|
|
|
|
assert "does not support task-augmented execution" in str(exc_info.value)
|
|
|
|
|
|
class TestProxyResourcesSyncExecution:
|
|
"""Test that resources work normally through proxy (sync execution)."""
|
|
|
|
async def test_resource_sync_execution_works(self, proxy_server: FastMCP):
|
|
"""Resource read without task=True works through proxy."""
|
|
async with Client(proxy_server) as client:
|
|
result = await client.read_resource("data://info.txt")
|
|
assert isinstance(result[0], TextResourceContents)
|
|
assert "Important information from the backend" in result[0].text
|
|
|
|
async def test_resource_template_sync_execution_works(self, proxy_server: FastMCP):
|
|
"""Resource template without task=True works through proxy."""
|
|
async with Client(proxy_server) as client:
|
|
result = await client.read_resource("data://user/42.json")
|
|
assert isinstance(result[0], TextResourceContents)
|
|
assert '"id": "42"' in result[0].text
|
|
|
|
|
|
class TestProxyResourcesTaskForbidden:
|
|
"""Test that resources with task=True are forbidden through proxy."""
|
|
|
|
@pytest.mark.xfail(
|
|
reason="SDK v2 has no `task` field on GetPromptRequestParams / "
|
|
"ReadResourceRequestParams; prompt/resource task submission is not "
|
|
"wire-expressible and always graceful-degrades (sdk-feedback #3).",
|
|
strict=True,
|
|
)
|
|
async def test_resource_task_raises_mcp_error(self, proxy_server: FastMCP):
|
|
"""Resource read with task=True through proxy raises MCPError."""
|
|
async with Client(proxy_server) as client:
|
|
with pytest.raises(MCPError) as exc_info:
|
|
await client.read_resource("data://info.txt", task=True)
|
|
|
|
assert "does not support task-augmented execution" in str(exc_info.value)
|
|
|
|
@pytest.mark.xfail(
|
|
reason="SDK v2 has no `task` field on GetPromptRequestParams / "
|
|
"ReadResourceRequestParams; prompt/resource task submission is not "
|
|
"wire-expressible and always graceful-degrades (sdk-feedback #3).",
|
|
strict=True,
|
|
)
|
|
async def test_resource_template_task_raises_mcp_error(self, proxy_server: FastMCP):
|
|
"""Resource template with task=True through proxy raises MCPError."""
|
|
async with Client(proxy_server) as client:
|
|
with pytest.raises(MCPError) as exc_info:
|
|
await client.read_resource("data://user/42.json", task=True)
|
|
|
|
assert "does not support task-augmented execution" in str(exc_info.value)
|