Skip to content

fix: namespace resource links in tool results#4156

Closed
he-yufeng wants to merge 1 commit into
PrefectHQ:mainfrom
he-yufeng:fix/namespace-resource-link-results
Closed

fix: namespace resource links in tool results#4156
he-yufeng wants to merge 1 commit into
PrefectHQ:mainfrom
he-yufeng:fix/namespace-resource-link-results

Conversation

@he-yufeng
Copy link
Copy Markdown

Summary

  • apply Namespace URI projection to ResourceLink content returned from namespaced tools
  • keep existing resource list/read behavior unchanged
  • add a regression test covering a namespaced provider returning a ResourceLink from tools/call

Fixes #4154.

To verify

  • PYTHONUTF8=1 uv run pytest tests/server/providers/test_transforming_provider.py tests/server/transforms/test_resources_as_tools.py tests/server/transforms/test_prompts_as_tools.py -q --basetemp .tmp\pytest
  • PYTHONUTF8=1 uv run ruff check fastmcp_slim/fastmcp/server/transforms/namespace.py tests/server/providers/test_transforming_provider.py
  • PYTHONUTF8=1 uv run ty check fastmcp_slim/fastmcp/server/transforms/namespace.py tests/server/providers/test_transforming_provider.py
  • PYTHONUTF8=1 uv run python -m py_compile fastmcp_slim/fastmcp/server/transforms/namespace.py tests/server/providers/test_transforming_provider.py

Copy link
Copy Markdown

@claude claude Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Claude Code Review

This pull request is from a fork — automated review is disabled. A repository maintainer can comment @claude review to run a one-time review.

@marvin-context-protocol marvin-context-protocol Bot added bug Something isn't working. Reports of errors, unexpected behavior, or broken functionality. server Related to FastMCP server implementation or server-side functionality. labels May 15, 2026
Copy link
Copy Markdown
Member

@jlowin jlowin left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the focused regression test here. I think this needs one small correction before merge: the ResourceLink copy currently bypasses Pydantic validation and stores the rewritten URI as a plain string.

In fastmcp_slim/fastmcp/server/transforms/namespace.py, _transform_content_block() does:

return block.model_copy(update={"uri": self._transform_uri(str(block.uri))})

model_copy(update=...) does not validate the updated value, so ResourceLink.uri becomes str instead of AnyUrl. Pydantic then emits serializer warnings when the block is dumped. Please wrap the transformed value with mcp.types.AnyUrl(...) or otherwise rebuild the ResourceLink through validation so the field keeps the expected type.

@jlowin
Copy link
Copy Markdown
Member

jlowin commented May 20, 2026

It looks like this introduced some significant test regressions as well. Please make sure to run tests and check all behavior before opening a PR

@marvin-context-protocol
Copy link
Copy Markdown
Contributor

tl;dr: 8 unit tests fail across every matrix job (3.10/3.13, Linux/Windows, lowest-direct). The new Namespace._transform_tool wraps mounted tools in TransformedTool, which short-circuits the child server's dispatch path and breaks every test that depends on it (mounted task context resolution, middleware, and delegate spans).

Root Cause: In fastmcp_slim/fastmcp/server/transforms/namespace.py, get_tool/list_tools previously returned a renamed copy of the original tool (tool.model_copy(update={"name": name})). The PR replaces that with TransformedTool.from_tool(tool, name=name, transform_fn=run_with_namespaced_links). mount() builds a FastMCPProvider and adds Namespace(namespace) via AggregateProvider (aggregate.py:102), so every mounted tool now goes through the new wrapper. TransformedTool.run (fastmcp_slim/fastmcp/tools/tool_transform.py:270) executes the wrapper's fn (which calls forward_raw) directly on the parent — the child server's execution path (where CurrentFastMCP, server dependencies, the child's middleware chain, and the OTEL delegate <tool> span are produced) is never entered. That matches every observed failure:

  • assert 'parent' == 'child' / 'grandchild'CurrentFastMCP() / ctx.fastmcp resolves to parent because the child server never dispatches the call (tests/server/tasks/test_task_mount.py, TestMountedTaskServerContext).
  • 'server-dep-parent' == 'server-dep-child' — server-scoped dependency resolves on the parent for the same reason (TestMountedTaskDependencies::test_mounted_task_receives_server_dependency).
  • Middleware trace shows parent:...grandchild:tool instead of the expected parent → ... → child:after sequence — child middleware is bypassed (TestMiddlewareWithMountedTasks::test_tool_middleware_runs_with_background_task).
  • 'delegate child_tool' in ['tools/call child_tool', ...] — no delegate span is recorded because TransformedTool runs in-process on the parent (tests/server/telemetry/test_provider_tracing.py::TestFastMCPProviderTracing::test_mounted_tool_creates_delegate_span and TestDelegateSpanMethod::test_mounted_tool_delegate_has_method).
  • RuntimeError: coroutine raised StopIteration — the missing span/context teardown also breaks TestProviderSpanHierarchy::test_delegate_span_is_child_of_server_span.

Fix: The URI rewrite needs to happen without replacing the tool's execution model. Options to consider:

  1. Keep get_tool/list_tools returning a renamed copy (as before) and project ResourceLink URIs in a call_tool hook on the transform/provider — i.e. intercept the result downstream, rather than substituting the tool object.
  2. If TransformedTool is the only place a post-call hook can sit today, add one — a ResultTransform or similar — that preserves dispatch through the child server (the wrapper should delegate to the original tool's run path, not replace it with forward_raw from the parent).

Either way, the regression test from this PR (test_namespace_rewrites_tool_resource_link_results) should keep passing, and the eight tests above need to return to green before this merges.

Failure summary (Python 3.10, ubuntu-latest)
FAILED tests/server/tasks/test_task_mount.py::TestMountedTaskDependencies::test_mounted_task_receives_server_dependency - AssertionError: assert 'server-dep-parent' == 'server-dep-child'
FAILED tests/server/tasks/test_task_mount.py::TestMountedTaskServerContext::test_current_fastmcp_resolves_to_child_server - AssertionError: assert 'parent' == 'child'
FAILED tests/server/tasks/test_task_mount.py::TestMountedTaskServerContext::test_context_fastmcp_resolves_to_child_server - AssertionError: assert 'parent' == 'child'
FAILED tests/server/tasks/test_task_mount.py::TestMountedTaskServerContext::test_nested_mount_resolves_to_innermost_server - AssertionError: assert 'parent' == 'grandchild'
FAILED tests/server/tasks/test_task_mount.py::TestMiddlewareWithMountedTasks::test_tool_middleware_runs_with_background_task - AssertionError: assert ['parent:befo...ndchild:tool'] == ['parent:befo...t:after', ...]
FAILED tests/server/telemetry/test_delegate_method.py::TestDelegateSpanMethod::test_mounted_tool_delegate_has_method - assert None is not None
FAILED tests/server/telemetry/test_provider_tracing.py::TestFastMCPProviderTracing::test_mounted_tool_creates_delegate_span - AssertionError: assert 'delegate child_tool' in ['tools/call child_tool', 'tools/call child_child_tool']
FAILED tests/server/telemetry/test_provider_tracing.py::TestProviderSpanHierarchy::test_delegate_span_is_child_of_server_span - RuntimeError: coroutine raised StopIteration
8 failed, 5667 passed, 2 skipped, 1 xfailed in 136.39s

Same failure set on Tests: Python 3.13 on ubuntu-latest, Tests: Python 3.10 on windows-latest, and Tests with lowest-direct dependencies. The MCP conformance tests, Integration tests, and Package install smoke jobs all passed.

Relevant files
  • fastmcp_slim/fastmcp/server/transforms/namespace.py_transform_tool is the regression source.
  • fastmcp_slim/fastmcp/server/server.py:2065-2169FastMCP.mount() adds a FastMCPProvider and namespaces it via add_provider/AggregateProvider.
  • fastmcp_slim/fastmcp/server/providers/aggregate.py:102 — wraps providers with Namespace(namespace).
  • fastmcp_slim/fastmcp/tools/tool_transform.py:241-368TransformedTool.run executes self.fn directly, which is what bypasses the child server's dispatch.
  • tests/server/tasks/test_task_mount.py, tests/server/telemetry/test_delegate_method.py, tests/server/telemetry/test_provider_tracing.py — the failing tests.

🤖 Generated with Claude Code

@he-yufeng he-yufeng force-pushed the fix/namespace-resource-link-results branch from 83bd911 to d331588 Compare May 20, 2026 14:48
Copy link
Copy Markdown
Author

Updated the PR to address the review/CI regression.

What changed:

  • Stopped using TransformedTool + forward_raw for namespaced tools, because that bypassed the mounted child server dispatch path.
  • Added a small wrapper that preserves the wrapped tool's _run / run path and only rewrites ResourceLink result URIs after execution.
  • Wrapped transformed ResourceLink.uri with AnyUrl so the field keeps the MCP/Pydantic URI type.
  • Preserved nested task registration by registering the innermost function under the outer namespaced key.

Validation run locally on Windows:

  • uv run --frozen pytest tests/server/providers/test_transforming_provider.py tests/server/tasks/test_task_mount.py tests/server/telemetry/test_delegate_method.py tests/server/telemetry/test_provider_tracing.py -q --basetemp .tmp/pytest -> 67 passed
  • uv run --frozen ruff check fastmcp_slim/fastmcp/server/transforms/namespace.py tests/server/providers/test_transforming_provider.py
  • uv run --frozen ty check fastmcp_slim/fastmcp/server/transforms/namespace.py
  • uv run --frozen python -m py_compile fastmcp_slim/fastmcp/server/transforms/namespace.py tests/server/providers/test_transforming_provider.py
  • git diff --check

Note: on this Windows environment, pytest needs PYTHONUTF8=1 and a repo-local --basetemp because the default user temp pytest directory returns WinError 5.

@he-yufeng he-yufeng force-pushed the fix/namespace-resource-link-results branch from 72e30e8 to fc1c64f Compare May 21, 2026 04:42
@he-yufeng
Copy link
Copy Markdown
Author

Rebased onto current main and re-ran the focused regression set after the URI-type fix.

Local validation on Windows:

  • uv run --frozen pytest tests/server/providers/test_transforming_provider.py tests/server/tasks/test_task_mount.py tests/server/telemetry/test_delegate_method.py tests/server/telemetry/test_provider_tracing.py -q --basetemp .tmp/pytest-4156 -> 67 passed
  • uv run --frozen ruff check fastmcp_slim/fastmcp/server/transforms/namespace.py tests/server/providers/test_transforming_provider.py -> passed
  • uv run --frozen ty check fastmcp_slim/fastmcp/server/transforms/namespace.py -> passed
  • uv run --frozen python -m py_compile fastmcp_slim/fastmcp/server/transforms/namespace.py tests/server/providers/test_transforming_provider.py -> passed
  • git diff --check -> passed

@he-yufeng he-yufeng force-pushed the fix/namespace-resource-link-results branch from fc1c64f to 33d17cf Compare May 21, 2026 15:38
@he-yufeng
Copy link
Copy Markdown
Author

Rebased onto current main again and re-ran the focused regression set.

Local validation on Windows:

  • uv run --frozen pytest tests/server/providers/test_transforming_provider.py tests/server/tasks/test_task_mount.py tests/server/telemetry/test_delegate_method.py tests/server/telemetry/test_provider_tracing.py -q --basetemp .tmp/pytest-4156 -> 67 passed
  • uv run --frozen ruff check fastmcp_slim/fastmcp/server/transforms/namespace.py tests/server/providers/test_transforming_provider.py -> passed
  • uv run --frozen ty check fastmcp_slim/fastmcp/server/transforms/namespace.py -> passed
  • uv run --frozen python -m py_compile fastmcp_slim/fastmcp/server/transforms/namespace.py tests/server/providers/test_transforming_provider.py -> passed
  • git diff --check -> passed

The latest branch keeps the ResourceLink.uri value wrapped as mcp.types.AnyUrl(...) and preserves the mounted child server execution path.

@he-yufeng he-yufeng force-pushed the fix/namespace-resource-link-results branch from 33d17cf to 574b4bc Compare May 23, 2026 15:15
@he-yufeng
Copy link
Copy Markdown
Author

Rebased onto current main again and re-ran the regression set that previously caught the mounted-server dispatch regressions.

Local validation on Windows:

  • $env:PYTHONUTF8='1'; .\.venv\Scripts\python.exe -m pytest tests\server\providers\test_transforming_provider.py tests\server\tasks\test_task_mount.py tests\server\telemetry\test_delegate_method.py tests\server\telemetry\test_provider_tracing.py -q --basetemp .tmp\pytest-4156-refresh -> 67 passed
  • .\.venv\Scripts\python.exe -m ruff check fastmcp_slim\fastmcp\server\transforms\namespace.py tests\server\providers\test_transforming_provider.py -> passed
  • .\.venv\Scripts\python.exe -m ruff format --check fastmcp_slim\fastmcp\server\transforms\namespace.py tests\server\providers\test_transforming_provider.py -> passed
  • .\.venv\Scripts\python.exe -m ty check fastmcp_slim\fastmcp\server\transforms\namespace.py -> passed
  • .\.venv\Scripts\python.exe -m py_compile fastmcp_slim\fastmcp\server\transforms\namespace.py tests\server\providers\test_transforming_provider.py -> passed
  • git diff --check origin/main...HEAD -> passed

@jlowin
Copy link
Copy Markdown
Member

jlowin commented May 23, 2026

Thanks for chasing this and for iterating after the CI feedback.

The behavior is right, but the implementation is solving too much of the framework inside Namespace. A namespaced tool that rewrites ResourceLink result URIs is a reasonable model; a bespoke _NamespacedResultTool inside namespace.py that has to preserve _run, run, task dispatch, docket registration, span attributes, and nested wrapper behavior is more coupling than I want in this transform.

The amount of code here is out of proportion to the use case because it is filling an abstraction gap. This should either be a reusable result-transforming tool abstraction, or an extension of TransformedTool, so Namespace only provides the small URI projection function and the shared tool wrapper owns the execution invariants.

The regression test is useful and should carry forward. I am closing this PR so the fix can be shaped around that abstraction instead of continuing with a one-off wrapper.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working. Reports of errors, unexpected behavior, or broken functionality. server Related to FastMCP server implementation or server-side functionality.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Namespace transform does not rewrite ResourceLink URIs returned from proxied tools

2 participants