helper to specifially rewrite proxied tool answer
This commit is contained in:
@@ -100,6 +100,10 @@ Tool simulate-research-query requires task augmentation (taskSupport: 'required'
|
|||||||
templates.
|
templates.
|
||||||
- Tool-result resource links: incomplete; embedded resource URIs need explicit
|
- Tool-result resource links: incomplete; embedded resource URIs need explicit
|
||||||
rewrite or a documented limitation.
|
rewrite or a documented limitation.
|
||||||
|
- `wf_mcp.proxy_results` now contains pure typed helpers for rewriting
|
||||||
|
`mcp.types.ResourceLink` content inside `mcp.types.CallToolResult`. These
|
||||||
|
helpers are not wired into the FastMCP proxy runtime yet because FastMCP does
|
||||||
|
not currently expose a result-transform hook.
|
||||||
- Session resources: unresolved; needs a focused test because session affinity
|
- Session resources: unresolved; needs a focused test because session affinity
|
||||||
may matter.
|
may matter.
|
||||||
- Tasks: unsupported; task-required tools should remain clearly diagnosed until
|
- Tasks: unsupported; task-required tools should remain clearly diagnosed until
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
from .resource_links import (
|
||||||
|
ResourceUriRewriter,
|
||||||
|
rewrite_call_tool_result_resource_links,
|
||||||
|
rewrite_resource_link_content,
|
||||||
|
)
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"ResourceUriRewriter",
|
||||||
|
"rewrite_call_tool_result_resource_links",
|
||||||
|
"rewrite_resource_link_content",
|
||||||
|
]
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from collections.abc import Callable
|
||||||
|
|
||||||
|
import mcp.types as mcp_types
|
||||||
|
|
||||||
|
ResourceUriRewriter = Callable[[str], str]
|
||||||
|
|
||||||
|
|
||||||
|
def rewrite_resource_link_content(
|
||||||
|
content: mcp_types.ContentBlock,
|
||||||
|
rewrite_uri: ResourceUriRewriter,
|
||||||
|
) -> mcp_types.ContentBlock:
|
||||||
|
"""Return content with ResourceLink URI rewritten through a proxy mapper.
|
||||||
|
|
||||||
|
FastMCP's Namespace transform rewrites listed resource URIs, but tool-call
|
||||||
|
results can also contain typed ResourceLink content. This helper is pure and
|
||||||
|
uses the official MCP models so it can be reused by a future proxy hook.
|
||||||
|
"""
|
||||||
|
if not isinstance(content, mcp_types.ResourceLink):
|
||||||
|
return content
|
||||||
|
return content.model_copy(update={"uri": rewrite_uri(str(content.uri))})
|
||||||
|
|
||||||
|
|
||||||
|
def rewrite_call_tool_result_resource_links(
|
||||||
|
result: mcp_types.CallToolResult,
|
||||||
|
rewrite_uri: ResourceUriRewriter,
|
||||||
|
) -> mcp_types.CallToolResult:
|
||||||
|
"""Return a copy of a tool result with ResourceLink content URIs rewritten."""
|
||||||
|
return result.model_copy(
|
||||||
|
update={
|
||||||
|
"content": [
|
||||||
|
rewrite_resource_link_content(content, rewrite_uri)
|
||||||
|
for content in result.content
|
||||||
|
]
|
||||||
|
}
|
||||||
|
)
|
||||||
@@ -145,4 +145,3 @@ async def _send_reload_notifications(ctx: Context, result: ProxyReloadResult) ->
|
|||||||
sink = FastMcpContextNotificationSink(ctx)
|
sink = FastMcpContextNotificationSink(ctx)
|
||||||
for event in reload_change_events(result):
|
for event in reload_change_events(result):
|
||||||
await sink.send_event(event)
|
await sink.send_event(event)
|
||||||
|
|
||||||
|
|||||||
@@ -38,7 +38,9 @@ def test_maps_capability_change_events_to_mcp_list_changed_notifications() -> No
|
|||||||
resource_notifications[0].root,
|
resource_notifications[0].root,
|
||||||
mcp_types.ResourceListChangedNotification,
|
mcp_types.ResourceListChangedNotification,
|
||||||
)
|
)
|
||||||
assert resource_notifications[0].root.method == "notifications/resources/list_changed"
|
assert (
|
||||||
|
resource_notifications[0].root.method == "notifications/resources/list_changed"
|
||||||
|
)
|
||||||
assert isinstance(
|
assert isinstance(
|
||||||
prompt_notifications[0].root,
|
prompt_notifications[0].root,
|
||||||
mcp_types.PromptListChangedNotification,
|
mcp_types.PromptListChangedNotification,
|
||||||
@@ -75,7 +77,9 @@ def test_fastmcp_context_notification_sink_sends_projected_notifications() -> No
|
|||||||
await sink.send_event(
|
await sink.send_event(
|
||||||
make_event("resources_changed", connection_id="demo.personal")
|
make_event("resources_changed", connection_id="demo.personal")
|
||||||
)
|
)
|
||||||
await sink.send_event(make_event("workflow_artifact_saved", workflow_name="demo"))
|
await sink.send_event(
|
||||||
|
make_event("workflow_artifact_saved", workflow_name="demo")
|
||||||
|
)
|
||||||
|
|
||||||
asyncio.run(run())
|
asyncio.run(run())
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,75 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import mcp.types as mcp_types
|
||||||
|
|
||||||
|
from wf_mcp.proxy_results import (
|
||||||
|
rewrite_call_tool_result_resource_links,
|
||||||
|
rewrite_resource_link_content,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_rewrites_resource_link_content_with_official_mcp_type() -> None:
|
||||||
|
content = _resource_link("demo://resource/dynamic/text/2")
|
||||||
|
|
||||||
|
rewritten = rewrite_resource_link_content(
|
||||||
|
content,
|
||||||
|
lambda uri: uri.replace("demo://", "demo://everything.default/"),
|
||||||
|
)
|
||||||
|
|
||||||
|
assert isinstance(rewritten, mcp_types.ResourceLink)
|
||||||
|
assert str(rewritten.uri) == "demo://everything.default/resource/dynamic/text/2"
|
||||||
|
assert rewritten.name == "dynamic-text"
|
||||||
|
assert rewritten.mimeType == "text/plain"
|
||||||
|
assert str(content.uri) == "demo://resource/dynamic/text/2"
|
||||||
|
|
||||||
|
|
||||||
|
def test_non_resource_link_content_is_returned_unchanged() -> None:
|
||||||
|
content = mcp_types.TextContent(type="text", text="ordinary text")
|
||||||
|
|
||||||
|
rewritten = rewrite_resource_link_content(
|
||||||
|
content,
|
||||||
|
lambda uri: f"rewritten:{uri}",
|
||||||
|
)
|
||||||
|
|
||||||
|
assert rewritten is content
|
||||||
|
|
||||||
|
|
||||||
|
def test_rewrites_resource_links_inside_call_tool_result() -> None:
|
||||||
|
result = mcp_types.CallToolResult(
|
||||||
|
content=[
|
||||||
|
mcp_types.TextContent(type="text", text="see linked resource"),
|
||||||
|
_resource_link("demo://resource/dynamic/text/2"),
|
||||||
|
],
|
||||||
|
structuredContent={"ok": True},
|
||||||
|
_meta={"source": "fixture"},
|
||||||
|
)
|
||||||
|
|
||||||
|
rewritten = rewrite_call_tool_result_resource_links(
|
||||||
|
result,
|
||||||
|
lambda uri: uri.replace("demo://", "demo://everything.default/"),
|
||||||
|
)
|
||||||
|
|
||||||
|
assert rewritten is not result
|
||||||
|
assert rewritten.structuredContent == {"ok": True}
|
||||||
|
assert rewritten.meta == {"source": "fixture"}
|
||||||
|
assert rewritten.content[0] is result.content[0]
|
||||||
|
rewritten_link = rewritten.content[1]
|
||||||
|
original_link = result.content[1]
|
||||||
|
assert isinstance(rewritten_link, mcp_types.ResourceLink)
|
||||||
|
assert isinstance(original_link, mcp_types.ResourceLink)
|
||||||
|
assert str(rewritten_link.uri) == (
|
||||||
|
"demo://everything.default/resource/dynamic/text/2"
|
||||||
|
)
|
||||||
|
assert str(original_link.uri) == "demo://resource/dynamic/text/2"
|
||||||
|
|
||||||
|
|
||||||
|
def _resource_link(uri: str) -> mcp_types.ResourceLink:
|
||||||
|
"""Build ResourceLink through validation because Pydantic accepts URI strings."""
|
||||||
|
return mcp_types.ResourceLink.model_validate(
|
||||||
|
{
|
||||||
|
"type": "resource_link",
|
||||||
|
"name": "dynamic-text",
|
||||||
|
"uri": uri,
|
||||||
|
"mimeType": "text/plain",
|
||||||
|
}
|
||||||
|
)
|
||||||
@@ -553,9 +553,7 @@ def test_transparent_proxy_runtime_reload_publishes_local_change_events() -> Non
|
|||||||
|
|
||||||
events = sink.list_events()[initial_event_count:]
|
events = sink.list_events()[initial_event_count:]
|
||||||
event_kinds = [event.kind for event in events]
|
event_kinds = [event.kind for event in events]
|
||||||
catalog_changed = [
|
catalog_changed = [event for event in events if event.kind == "catalog_changed"]
|
||||||
event for event in events if event.kind == "catalog_changed"
|
|
||||||
]
|
|
||||||
assert result["reloaded"] is True
|
assert result["reloaded"] is True
|
||||||
assert "tools_changed" in event_kinds
|
assert "tools_changed" in event_kinds
|
||||||
assert "resources_changed" in event_kinds
|
assert "resources_changed" in event_kinds
|
||||||
|
|||||||
Reference in New Issue
Block a user