Spaces:
Running
Running
Jeremiah Lowin commited on
Ensure proxy components forward meta dicts (#1282)
Browse files
src/fastmcp/server/proxy.py
CHANGED
|
@@ -255,6 +255,8 @@ class ProxyTool(Tool, MirroredComponent):
|
|
| 255 |
parameters=mcp_tool.inputSchema,
|
| 256 |
annotations=mcp_tool.annotations,
|
| 257 |
output_schema=mcp_tool.outputSchema,
|
|
|
|
|
|
|
| 258 |
_mirrored=True,
|
| 259 |
)
|
| 260 |
|
|
@@ -309,6 +311,8 @@ class ProxyResource(Resource, MirroredComponent):
|
|
| 309 |
name=mcp_resource.name,
|
| 310 |
description=mcp_resource.description,
|
| 311 |
mime_type=mcp_resource.mimeType or "text/plain",
|
|
|
|
|
|
|
| 312 |
_mirrored=True,
|
| 313 |
)
|
| 314 |
|
|
@@ -348,6 +352,8 @@ class ProxyTemplate(ResourceTemplate, MirroredComponent):
|
|
| 348 |
description=mcp_template.description,
|
| 349 |
mime_type=mcp_template.mimeType or "text/plain",
|
| 350 |
parameters={}, # Remote templates don't have local parameters
|
|
|
|
|
|
|
| 351 |
_mirrored=True,
|
| 352 |
)
|
| 353 |
|
|
@@ -380,6 +386,8 @@ class ProxyTemplate(ResourceTemplate, MirroredComponent):
|
|
| 380 |
name=self.name,
|
| 381 |
description=self.description,
|
| 382 |
mime_type=result[0].mimeType,
|
|
|
|
|
|
|
| 383 |
_value=value,
|
| 384 |
)
|
| 385 |
|
|
@@ -413,6 +421,8 @@ class ProxyPrompt(Prompt, MirroredComponent):
|
|
| 413 |
name=mcp_prompt.name,
|
| 414 |
description=mcp_prompt.description,
|
| 415 |
arguments=arguments,
|
|
|
|
|
|
|
| 416 |
_mirrored=True,
|
| 417 |
)
|
| 418 |
|
|
|
|
| 255 |
parameters=mcp_tool.inputSchema,
|
| 256 |
annotations=mcp_tool.annotations,
|
| 257 |
output_schema=mcp_tool.outputSchema,
|
| 258 |
+
meta=mcp_tool.meta,
|
| 259 |
+
tags=(mcp_tool.meta or {}).get("tags", []),
|
| 260 |
_mirrored=True,
|
| 261 |
)
|
| 262 |
|
|
|
|
| 311 |
name=mcp_resource.name,
|
| 312 |
description=mcp_resource.description,
|
| 313 |
mime_type=mcp_resource.mimeType or "text/plain",
|
| 314 |
+
meta=mcp_resource.meta,
|
| 315 |
+
tags=(mcp_resource.meta or {}).get("tags", []),
|
| 316 |
_mirrored=True,
|
| 317 |
)
|
| 318 |
|
|
|
|
| 352 |
description=mcp_template.description,
|
| 353 |
mime_type=mcp_template.mimeType or "text/plain",
|
| 354 |
parameters={}, # Remote templates don't have local parameters
|
| 355 |
+
meta=mcp_template.meta,
|
| 356 |
+
tags=(mcp_template.meta or {}).get("tags", []),
|
| 357 |
_mirrored=True,
|
| 358 |
)
|
| 359 |
|
|
|
|
| 386 |
name=self.name,
|
| 387 |
description=self.description,
|
| 388 |
mime_type=result[0].mimeType,
|
| 389 |
+
meta=self.meta,
|
| 390 |
+
tags=(self.meta or {}).get("tags", []),
|
| 391 |
_value=value,
|
| 392 |
)
|
| 393 |
|
|
|
|
| 421 |
name=mcp_prompt.name,
|
| 422 |
description=mcp_prompt.description,
|
| 423 |
arguments=arguments,
|
| 424 |
+
meta=mcp_prompt.meta,
|
| 425 |
+
tags=(mcp_prompt.meta or {}).get("tags", []),
|
| 426 |
_mirrored=True,
|
| 427 |
)
|
| 428 |
|
tests/server/proxy/test_proxy_client.py
CHANGED
|
@@ -18,6 +18,10 @@ from fastmcp.server.proxy import ProxyClient
|
|
| 18 |
def fastmcp_server():
|
| 19 |
mcp = FastMCP("TestServer")
|
| 20 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 21 |
@mcp.tool
|
| 22 |
async def list_roots(context: Context) -> list[str]:
|
| 23 |
roots = await context.list_roots()
|
|
@@ -80,6 +84,15 @@ async def proxy_server(fastmcp_server: FastMCP):
|
|
| 80 |
|
| 81 |
|
| 82 |
class TestProxyClient:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 83 |
async def test_forward_error_response(self, proxy_server: FastMCP):
|
| 84 |
"""
|
| 85 |
Test that the proxy client correctly forwards an error response.
|
|
|
|
| 18 |
def fastmcp_server():
|
| 19 |
mcp = FastMCP("TestServer")
|
| 20 |
|
| 21 |
+
@mcp.tool(tags={"echo"})
|
| 22 |
+
def echo(message: str) -> str:
|
| 23 |
+
return f"echo: {message}"
|
| 24 |
+
|
| 25 |
@mcp.tool
|
| 26 |
async def list_roots(context: Context) -> list[str]:
|
| 27 |
roots = await context.list_roots()
|
|
|
|
| 84 |
|
| 85 |
|
| 86 |
class TestProxyClient:
|
| 87 |
+
async def test_forward_tool_meta(self, proxy_server: FastMCP):
|
| 88 |
+
"""
|
| 89 |
+
Test that the proxy client correctly forwards the `echo` tool meta.
|
| 90 |
+
"""
|
| 91 |
+
async with Client(proxy_server) as client:
|
| 92 |
+
tools = await client.list_tools()
|
| 93 |
+
echo_tool = next(t for t in tools if t.name == "echo")
|
| 94 |
+
assert echo_tool.meta == {"tags": ["echo"]}
|
| 95 |
+
|
| 96 |
async def test_forward_error_response(self, proxy_server: FastMCP):
|
| 97 |
"""
|
| 98 |
Test that the proxy client correctly forwards an error response.
|
tests/server/proxy/test_proxy_server.py
CHANGED
|
@@ -29,7 +29,7 @@ def fastmcp_server():
|
|
| 29 |
|
| 30 |
# --- Tools ---
|
| 31 |
|
| 32 |
-
@server.tool
|
| 33 |
def greet(name: str) -> str:
|
| 34 |
"""Greet someone by name."""
|
| 35 |
return f"Hello, {name}!"
|
|
@@ -50,7 +50,7 @@ def fastmcp_server():
|
|
| 50 |
|
| 51 |
# --- Resources ---
|
| 52 |
|
| 53 |
-
@server.resource(uri="resource://wave")
|
| 54 |
def wave() -> str:
|
| 55 |
return "👋"
|
| 56 |
|
|
@@ -58,13 +58,13 @@ def fastmcp_server():
|
|
| 58 |
async def get_users() -> list[dict[str, Any]]:
|
| 59 |
return USERS
|
| 60 |
|
| 61 |
-
@server.resource(uri="data://user/{user_id}")
|
| 62 |
async def get_user(user_id: str) -> dict[str, Any] | None:
|
| 63 |
return next((user for user in USERS if user["id"] == user_id), None)
|
| 64 |
|
| 65 |
# --- Prompts ---
|
| 66 |
|
| 67 |
-
@server.prompt
|
| 68 |
def welcome(name: str) -> str:
|
| 69 |
return f"Welcome to FastMCP, {name}!"
|
| 70 |
|
|
@@ -121,6 +121,11 @@ class TestTools:
|
|
| 121 |
assert "error_tool" in tools
|
| 122 |
assert "tool_without_description" in tools
|
| 123 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 124 |
async def test_get_transformed_tools(
|
| 125 |
self, fastmcp_server: FastMCP, proxy_server: FastMCPProxy
|
| 126 |
):
|
|
@@ -238,6 +243,11 @@ class TestResources:
|
|
| 238 |
)
|
| 239 |
assert [r.name for r in resources.values()] == Contains("get_users", "wave")
|
| 240 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 241 |
async def test_list_resources_same_as_original(self, fastmcp_server, proxy_server):
|
| 242 |
assert (
|
| 243 |
await proxy_server._mcp_list_resources()
|
|
@@ -332,6 +342,11 @@ class TestResourceTemplates:
|
|
| 332 |
templates = await proxy_server.get_resource_templates()
|
| 333 |
assert [t.name for t in templates.values()] == Contains("get_user")
|
| 334 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 335 |
async def test_list_resource_templates_same_as_original(
|
| 336 |
self, fastmcp_server, proxy_server
|
| 337 |
):
|
|
@@ -431,6 +446,11 @@ class TestPrompts:
|
|
| 431 |
prompts = await proxy_server.get_prompts()
|
| 432 |
assert [p.name for p in prompts.values()] == Contains("welcome")
|
| 433 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 434 |
async def test_list_prompts_same_as_original(self, fastmcp_server, proxy_server):
|
| 435 |
async with Client(fastmcp_server) as client:
|
| 436 |
result = await client.list_prompts()
|
|
|
|
| 29 |
|
| 30 |
# --- Tools ---
|
| 31 |
|
| 32 |
+
@server.tool(tags={"greet"})
|
| 33 |
def greet(name: str) -> str:
|
| 34 |
"""Greet someone by name."""
|
| 35 |
return f"Hello, {name}!"
|
|
|
|
| 50 |
|
| 51 |
# --- Resources ---
|
| 52 |
|
| 53 |
+
@server.resource(uri="resource://wave", tags={"wave"})
|
| 54 |
def wave() -> str:
|
| 55 |
return "👋"
|
| 56 |
|
|
|
|
| 58 |
async def get_users() -> list[dict[str, Any]]:
|
| 59 |
return USERS
|
| 60 |
|
| 61 |
+
@server.resource(uri="data://user/{user_id}", tags={"users"})
|
| 62 |
async def get_user(user_id: str) -> dict[str, Any] | None:
|
| 63 |
return next((user for user in USERS if user["id"] == user_id), None)
|
| 64 |
|
| 65 |
# --- Prompts ---
|
| 66 |
|
| 67 |
+
@server.prompt(tags={"welcome"})
|
| 68 |
def welcome(name: str) -> str:
|
| 69 |
return f"Welcome to FastMCP, {name}!"
|
| 70 |
|
|
|
|
| 121 |
assert "error_tool" in tools
|
| 122 |
assert "tool_without_description" in tools
|
| 123 |
|
| 124 |
+
async def test_get_tools_meta(self, proxy_server):
|
| 125 |
+
tools = await proxy_server.get_tools()
|
| 126 |
+
greet_tool = tools["greet"]
|
| 127 |
+
assert greet_tool.meta == {"tags": ["greet"]}
|
| 128 |
+
|
| 129 |
async def test_get_transformed_tools(
|
| 130 |
self, fastmcp_server: FastMCP, proxy_server: FastMCPProxy
|
| 131 |
):
|
|
|
|
| 243 |
)
|
| 244 |
assert [r.name for r in resources.values()] == Contains("get_users", "wave")
|
| 245 |
|
| 246 |
+
async def test_get_resources_meta(self, proxy_server):
|
| 247 |
+
resources = await proxy_server.get_resources()
|
| 248 |
+
wave_resource = resources["resource://wave"]
|
| 249 |
+
assert wave_resource.meta == {"tags": ["wave"]}
|
| 250 |
+
|
| 251 |
async def test_list_resources_same_as_original(self, fastmcp_server, proxy_server):
|
| 252 |
assert (
|
| 253 |
await proxy_server._mcp_list_resources()
|
|
|
|
| 342 |
templates = await proxy_server.get_resource_templates()
|
| 343 |
assert [t.name for t in templates.values()] == Contains("get_user")
|
| 344 |
|
| 345 |
+
async def test_get_resource_templates_meta(self, proxy_server):
|
| 346 |
+
templates = await proxy_server.get_resource_templates()
|
| 347 |
+
get_user_template = templates["data://user/{user_id}"]
|
| 348 |
+
assert get_user_template.meta == {"tags": ["users"]}
|
| 349 |
+
|
| 350 |
async def test_list_resource_templates_same_as_original(
|
| 351 |
self, fastmcp_server, proxy_server
|
| 352 |
):
|
|
|
|
| 446 |
prompts = await proxy_server.get_prompts()
|
| 447 |
assert [p.name for p in prompts.values()] == Contains("welcome")
|
| 448 |
|
| 449 |
+
async def test_get_prompts_meta(self, proxy_server):
|
| 450 |
+
prompts = await proxy_server.get_prompts()
|
| 451 |
+
welcome_prompt = prompts["welcome"]
|
| 452 |
+
assert welcome_prompt.meta == {"tags": ["welcome"]}
|
| 453 |
+
|
| 454 |
async def test_list_prompts_same_as_original(self, fastmcp_server, proxy_server):
|
| 455 |
async with Client(fastmcp_server) as client:
|
| 456 |
result = await client.list_prompts()
|