177 lines
4.2 KiB
Markdown
177 lines
4.2 KiB
Markdown
# FastMCP Proxy ResourceLink Issue Guide
|
|
|
|
Use this as a copy/rewrite starting point for an upstream FastMCP issue. Keep
|
|
the code blocks and concrete examples; rewrite the prose however you want.
|
|
|
|
## Suggested Title
|
|
|
|
```text
|
|
Namespace transform rewrites listed resource URIs but not ResourceLink URIs returned from proxied tools
|
|
```
|
|
|
|
## Short Summary
|
|
|
|
```text
|
|
When a FastMCP proxy has a Namespace transform, listed resources and resource
|
|
templates are correctly namespaced, and namespaced resource reads work.
|
|
However, ResourceLink content returned inside a proxied tools/call result keeps
|
|
the raw upstream URI instead of the downstream namespaced URI.
|
|
```
|
|
|
|
## Why This Looks Like A Proxy Bug
|
|
|
|
```text
|
|
The proxy is already doing the hard part correctly for normal resource surfaces:
|
|
|
|
- resources/list returns namespaced URIs
|
|
- resource_templates/list returns namespaced URI templates
|
|
- resources/read accepts the namespaced URI and reverses it upstream
|
|
|
|
Only ResourceLink content inside tools/call results escapes with the upstream
|
|
URI unchanged. That makes the returned link unusable from the downstream client
|
|
even though the same resource is available through the proxy under its
|
|
namespaced URI.
|
|
```
|
|
|
|
## Minimal Example
|
|
|
|
```python
|
|
from fastmcp.server import create_proxy
|
|
from fastmcp.server.transforms import Namespace
|
|
|
|
proxy = create_proxy(upstream_client, name="Proxy-everything.default")
|
|
proxy.add_transform(Namespace("everything.default"))
|
|
```
|
|
|
|
Given an upstream resource:
|
|
|
|
```text
|
|
demo://resource/dynamic/text/2
|
|
```
|
|
|
|
The proxy correctly exposes it as:
|
|
|
|
```text
|
|
demo://everything.default/resource/dynamic/text/2
|
|
```
|
|
|
|
But if an upstream tool returns:
|
|
|
|
```python
|
|
mcp.types.ResourceLink(
|
|
type="resource_link",
|
|
name="dynamic-text",
|
|
uri="demo://resource/dynamic/text/2",
|
|
)
|
|
```
|
|
|
|
the proxied tool result still contains:
|
|
|
|
```text
|
|
demo://resource/dynamic/text/2
|
|
```
|
|
|
|
instead of:
|
|
|
|
```text
|
|
demo://everything.default/resource/dynamic/text/2
|
|
```
|
|
|
|
## Observed Behavior
|
|
|
|
```text
|
|
resources/list:
|
|
upstream: demo://resource/dynamic/text/2
|
|
proxied: demo://everything.default/resource/dynamic/text/2
|
|
|
|
resources/read:
|
|
demo://everything.default/resource/dynamic/text/2
|
|
-> works
|
|
|
|
tools/call result content:
|
|
ResourceLink.uri == demo://resource/dynamic/text/2
|
|
-> raw upstream URI leaks through unchanged
|
|
```
|
|
|
|
## Expected Behavior
|
|
|
|
```text
|
|
If a proxy transform rewrites a resource URI for resources/list and
|
|
resources/read, ResourceLink content emitted by proxied tools should expose the
|
|
same downstream-facing URI.
|
|
```
|
|
|
|
## Relevant FastMCP Code Paths
|
|
|
|
`Namespace` already owns the URI mapping:
|
|
|
|
```python
|
|
class Namespace(Transform):
|
|
async def list_resources(...):
|
|
...
|
|
|
|
async def get_resource(...):
|
|
...
|
|
```
|
|
|
|
`ProxyTool.run(...)` currently returns upstream tool content unchanged:
|
|
|
|
```python
|
|
result = await client.call_tool_mcp(
|
|
name=backend_name,
|
|
arguments=arguments,
|
|
meta=meta,
|
|
)
|
|
|
|
return ToolResult(
|
|
content=result.content,
|
|
structured_content=result.structuredContent,
|
|
meta=result.meta,
|
|
)
|
|
```
|
|
|
|
FastMCP's `Transform` base class has hooks for listing/getting tools,
|
|
resources, templates, and prompts, but there does not appear to be a hook for
|
|
transforming tool results.
|
|
|
|
## Suggested Direction
|
|
|
|
```text
|
|
Prefer a general result-transform hook over special-casing Namespace logic in
|
|
ProxyTool.
|
|
```
|
|
|
|
For example, something in this shape:
|
|
|
|
```python
|
|
class Transform:
|
|
async def tool_result(self, result: ToolResult) -> ToolResult:
|
|
return result
|
|
```
|
|
|
|
Then `Namespace` could rewrite only typed MCP resource-link content:
|
|
|
|
```python
|
|
if isinstance(content, mcp.types.ResourceLink):
|
|
content = content.model_copy(update={"uri": self._transform_uri(str(content.uri))})
|
|
```
|
|
|
|
That would keep URI rewriting logic in `Namespace`, where the forward and
|
|
reverse resource URI mapping already lives.
|
|
|
|
## Important Boundary
|
|
|
|
```text
|
|
This is specifically about mcp.types.ResourceLink content inside tools/call
|
|
results. Normal resources/list, resource_templates/list, and resources/read
|
|
already behave correctly through the proxy.
|
|
```
|
|
|
|
## Extra Note
|
|
|
|
```text
|
|
Session-scoped resources returned by some tools may have additional lifecycle
|
|
constraints beyond URI rewriting. This issue is about ordinary ResourceLink URI
|
|
projection only.
|
|
```
|