Skip to content

Refactor FastMCPProxy into ProxyProvider#2669

Merged
jlowin merged 2 commits intomainfrom
refactor/proxy-provider
Dec 22, 2025
Merged

Refactor FastMCPProxy into ProxyProvider#2669
jlowin merged 2 commits intomainfrom
refactor/proxy-provider

Conversation

@jlowin
Copy link
Copy Markdown
Member

@jlowin jlowin commented Dec 22, 2025

Refactors proxy functionality from custom manager classes (ProxyToolManager, ProxyResourceManager, etc.) to use the Provider pattern.

Changes

ProxyProvider now implements the Provider interface, returning ProxyTool, ProxyResource, ProxyTemplate, and ProxyPrompt components that forward execution to remote servers.

FastMCPProxy remains as a convenience wrapper that internally uses ProxyProvider:

# These are equivalent:
proxy = FastMCPProxy(client_factory=lambda: ProxyClient(url))
# vs
mcp = FastMCP()
mcp.add_provider(ProxyProvider(client_factory=lambda: ProxyClient(url)))

All proxy code moves to fastmcp.server.providers.proxy. The old fastmcp.server.proxy location emits a deprecation warning.

Move proxy functionality from custom manager classes to the Provider pattern:

- Create ProxyProvider that implements the Provider interface
- Move all proxy code to src/fastmcp/server/providers/proxy.py
- Keep FastMCPProxy as a convenience wrapper using ProxyProvider
- Add deprecation warning when importing from old location
- Convert handler classmethods to module-level functions
- Remove redundant get_* methods (base class defaults work)
@marvin-context-protocol marvin-context-protocol Bot added enhancement Improvement to existing functionality. For issues and smaller PR improvements. breaking change Breaks backward compatibility. Requires minor version bump. Critical for maintainer attention. server Related to FastMCP server implementation or server-side functionality. client Related to the FastMCP client SDK or client-side functionality. labels Dec 22, 2025
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Dec 22, 2025

Walkthrough

Moves proxy implementations into a new module fastmcp.server.providers.proxy, adding a ProxyProvider framework and proxy-wrapped component classes (ProxyTool, ProxyResource, ProxyTemplate, ProxyPrompt, plus clients and helpers). Converts the old fastmcp.server.proxy into a compatibility shim that re-exports the new symbols and emits a deprecation warning. Updates imports throughout the codebase to reference the new module location. Changes MCPConfigTransport mounting to use namespace=name instead of prefix=name. Removes the Components dataclass from providers and adds a lazy-export for ProxyProvider in the providers package root.

Possibly related PRs

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Linked Issues check ⚠️ Warning The PR aims to refactor proxy architecture but does not implement the core objective from issue #2668: parallel loading of tools/resources/prompts from mounted servers to improve startup performance. Implement asyncio.gather-based parallel loading in get_tools(), get_resources(), and get_prompts() methods to address the sequential loading bottleneck described in issue #2668.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely summarizes the main change: refactoring FastMCPProxy into ProxyProvider, using the Provider pattern.
Description check ✅ Passed The PR description is well-structured, explaining the refactor from manager classes to Provider pattern, showing equivalence examples, and noting the deprecation warning for the old location.
Out of Scope Changes check ✅ Passed All changes are directly related to the stated objective of refactoring proxy functionality from manager classes to the Provider pattern, with no extraneous modifications.
Docstring Coverage ✅ Passed Docstring coverage is 81.40% which is sufficient. The required threshold is 80.00%.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch refactor/proxy-provider

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (1)
src/fastmcp/server/proxy.py (1)

19-29: Remove unused noqa directive.

The # noqa: E402 comment is unnecessary here. The deprecation warning on lines 12-16 doesn't trigger the E402 rule (module level import not at top of file) because there are no preceding executable statements that would cause this issue — warnings.warn() is called before the imports, which is intentional and valid.

🔎 Proposed fix
-from fastmcp.server.providers.proxy import (  # noqa: E402
+from fastmcp.server.providers.proxy import (
     ClientFactoryT,
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 3afdb3f and fbdbd1a.

⛔ Files ignored due to path filters (5)
  • tests/server/proxy/test_proxy_client.py is excluded by none and included by none
  • tests/server/proxy/test_proxy_server.py is excluded by none and included by none
  • tests/server/proxy/test_stateful_proxy_client.py is excluded by none and included by none
  • tests/server/tasks/test_task_proxy.py is excluded by none and included by none
  • tests/server/test_mount.py is excluded by none and included by none
📒 Files selected for processing (6)
  • src/fastmcp/client/transports.py
  • src/fastmcp/server/providers/__init__.py
  • src/fastmcp/server/providers/proxy.py
  • src/fastmcp/server/proxy.py
  • src/fastmcp/server/server.py
  • src/fastmcp/utilities/mcp_config.py
🧰 Additional context used
📓 Path-based instructions (2)
**/*.py

📄 CodeRabbit inference engine (AGENTS.md)

**/*.py: Use Python ≥ 3.10 with full type annotations
Never use bare except - be specific with exception types

Files:

  • src/fastmcp/utilities/mcp_config.py
  • src/fastmcp/server/proxy.py
  • src/fastmcp/server/providers/__init__.py
  • src/fastmcp/server/server.py
  • src/fastmcp/client/transports.py
  • src/fastmcp/server/providers/proxy.py
src/fastmcp/**/__init__.py

📄 CodeRabbit inference engine (AGENTS.md)

Core types that define a module's purpose should be exported (e.g., Middleware from fastmcp.server.middleware), while specialized features can live in submodules

Files:

  • src/fastmcp/server/providers/__init__.py
🧠 Learnings (3)
📚 Learning: 2025-12-21T21:37:55.015Z
Learnt from: CR
Repo: jlowin/fastmcp PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-21T21:37:55.015Z
Learning: Applies to src/fastmcp/__init__.py : All module exports should be intentional - only re-export to `fastmcp.*` for fundamental types like `FastMCP` and `Client`, prefer users importing from specific submodules for specialized features

Applied to files:

  • src/fastmcp/utilities/mcp_config.py
  • src/fastmcp/server/proxy.py
  • src/fastmcp/server/providers/__init__.py
  • src/fastmcp/server/server.py
  • src/fastmcp/server/providers/proxy.py
📚 Learning: 2025-12-21T21:37:55.015Z
Learnt from: CR
Repo: jlowin/fastmcp PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-21T21:37:55.015Z
Learning: Applies to src/fastmcp/**/__init__.py : Core types that define a module's purpose should be exported (e.g., `Middleware` from `fastmcp.server.middleware`), while specialized features can live in submodules

Applied to files:

  • src/fastmcp/utilities/mcp_config.py
  • src/fastmcp/server/proxy.py
  • src/fastmcp/server/providers/__init__.py
  • src/fastmcp/server/server.py
📚 Learning: 2025-12-21T21:37:55.015Z
Learnt from: CR
Repo: jlowin/fastmcp PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-21T21:37:55.015Z
Learning: Applies to tests/**/*.py : Use in-memory transport (pass FastMCP servers directly to clients) for testing, only use HTTP transport when explicitly testing network features

Applied to files:

  • src/fastmcp/client/transports.py
🧬 Code graph analysis (5)
src/fastmcp/utilities/mcp_config.py (1)
src/fastmcp/server/providers/proxy.py (2)
  • FastMCPProxy (562-604)
  • ProxyClient (683-717)
src/fastmcp/server/providers/__init__.py (1)
src/fastmcp/server/providers/proxy.py (1)
  • ProxyProvider (408-554)
src/fastmcp/server/server.py (1)
src/fastmcp/server/providers/proxy.py (2)
  • FastMCPProxy (562-604)
  • ProxyClient (683-717)
src/fastmcp/client/transports.py (1)
src/fastmcp/server/server.py (1)
  • name (355-356)
src/fastmcp/server/providers/proxy.py (11)
src/fastmcp/client/client.py (1)
  • Client (127-1627)
src/fastmcp/mcp_config.py (1)
  • MCPConfig (243-294)
src/fastmcp/prompts/prompt.py (2)
  • Prompt (118-322)
  • PromptResult (73-115)
src/fastmcp/resources/resource.py (1)
  • Resource (137-331)
src/fastmcp/resources/template.py (1)
  • ResourceTemplate (97-298)
src/fastmcp/server/context.py (3)
  • Context (117-1003)
  • request_context (212-238)
  • log (309-335)
src/fastmcp/server/dependencies.py (1)
  • get_context (271-277)
src/fastmcp/server/providers/base.py (1)
  • Provider (66-384)
src/fastmcp/tools/tool.py (1)
  • Tool (126-369)
src/fastmcp/tools/tool_transform.py (2)
  • ToolTransformConfig (878-921)
  • apply_transformations_to_tools (924-943)
src/fastmcp/utilities/components.py (1)
  • MirroredComponent (156-192)
🪛 Ruff (0.14.8)
src/fastmcp/server/proxy.py

19-19: Unused noqa directive (non-enabled: E402)

Remove unused noqa directive

(RUF100)

src/fastmcp/server/providers/__init__.py

51-51: Avoid specifying long messages outside the exception class

(TRY003)

src/fastmcp/server/providers/proxy.py

220-222: Avoid specifying long messages outside the exception class

(TRY003)


236-236: Avoid specifying long messages outside the exception class

(TRY003)


288-288: Unused method argument: uri

(ARG002)


290-290: Unused method argument: context

(ARG002)


305-307: Avoid specifying long messages outside the exception class

(TRY003)


321-321: Avoid specifying long messages outside the exception class

(TRY003)


613-613: Unused function argument: context

(ARG001)


623-623: Unused function argument: context

(ARG001)


645-645: Unused function argument: response_type

(ARG001)


647-647: Unused function argument: context

(ARG001)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (4)
  • GitHub Check: Run tests: Python 3.10 on ubuntu-latest
  • GitHub Check: Run tests: Python 3.13 on ubuntu-latest
  • GitHub Check: Run tests with lowest-direct dependencies
  • GitHub Check: Run tests: Python 3.10 on windows-latest
🔇 Additional comments (19)
src/fastmcp/client/transports.py (1)

1018-1020: LGTM!

The parameter rename from prefix to namespace correctly aligns with the updated mount() API signature in FastMCP.server. The conditional logic based on name_as_prefix is preserved.

src/fastmcp/utilities/mcp_config.py (1)

13-13: LGTM!

Import path correctly updated to the new canonical location fastmcp.server.providers.proxy. This aligns with the module reorganization and avoids triggering the deprecation warning from the old module.

src/fastmcp/server/server.py (3)

102-102: LGTM!

TYPE_CHECKING import correctly updated to reference FastMCPProxy from its new canonical location.


2634-2634: LGTM!

Runtime import in mount() correctly updated for backward compatibility handling of the deprecated as_proxy parameter.


2839-2839: LGTM!

Runtime import in as_proxy() correctly updated to import both FastMCPProxy and ProxyClient from the new module location.

src/fastmcp/server/proxy.py (2)

1-16: LGTM!

The compatibility shim is well-structured with a clear docstring explaining the deprecation and proper use of stacklevel=2 for the warning to point to the caller's import statement.


31-41: LGTM!

The __all__ declaration correctly lists all the re-exported symbols, maintaining the same public API surface as the new module location.

src/fastmcp/server/providers/__init__.py (2)

28-42: LGTM!

The TYPE_CHECKING import combined with __all__ export correctly enables static type checkers to recognize ProxyProvider as a public symbol without triggering a runtime import.


45-51: LGTM!

The lazy import pattern via __getattr__ is correctly implemented to avoid circular imports while making ProxyProvider accessible as fastmcp.server.providers.ProxyProvider. This is a standard Python pattern for deferred module loading.

src/fastmcp/server/providers/proxy.py (10)

1-56: LGTM!

Module setup is clean with appropriate imports and a clear type alias for ClientFactoryT. The organization separates concerns well between component proxies, the provider, and client utilities.


67-111: LGTM!

ProxyTool correctly implements the proxy pattern:

  • task_config.mode="forbidden" prevents task execution through proxies
  • _backend_name preserves the original name when namespace transforms are applied via model_copy
  • from_mcp_tool factory properly maps MCP schema including extracting tags from meta

112-152: LGTM!

The run() method correctly forwards tool execution to the remote server, including proper handling of task metadata when present in the request context. Error handling converts remote errors to ToolError appropriately.


154-237: LGTM!

ProxyResource follows the same well-designed pattern as ProxyTool. The read() method correctly handles both text and blob content types from the remote server.


239-335: LGTM!

ProxyTemplate correctly handles parameterized URIs. The create_resource method intentionally ignores the provided uri parameter (as documented in the comment) because the URI may differ after namespace transforms — it reconstructs the backend URI from the original template and provided params.


337-401: LGTM!

ProxyPrompt correctly forwards prompt rendering to the remote server and preserves runtime meta from the result.


408-554: LGTM!

ProxyProvider cleanly implements the Provider interface:

  • list_* methods gracefully handle METHOD_NOT_FOUND for servers that don't support all component types
  • get_tasks() returns empty components since proxied components can't execute as background tasks
  • Tool transformations are applied when configured

562-604: LGTM!

FastMCPProxy is a clean convenience wrapper that creates a FastMCP server with a ProxyProvider. The tool_transformations extraction before calling super().__init__ prevents the parameter from being passed to FastMCP which doesn't accept it.


612-681: LGTM!

The default proxy handlers correctly forward requests from the remote server to the proxy's connected clients. The unused context parameters are intentional — they're part of the handler callback signatures but the actual context is obtained via get_context() which accesses the current server's context.


683-766: LGTM!

ProxyClient and StatefulProxyClient are well-designed:

  • ProxyClient sets up sensible defaults for all proxy handler callbacks
  • StatefulProxyClient correctly manages per-session client caching with proper cleanup via session._exit_stack

@jlowin jlowin merged commit 748d25e into main Dec 22, 2025
11 of 13 checks passed
@jlowin jlowin deleted the refactor/proxy-provider branch December 22, 2025 02:30
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (1)
src/fastmcp/server/providers/proxy.py (1)

308-321: Consider extracting duplicate content conversion logic.

The content type handling logic (lines 308-321) is duplicated from ProxyResource.read() (lines 223-236). Consider extracting this into a shared helper method to reduce duplication and improve maintainability.

🔎 Suggested refactor
def _convert_mcp_resource_content(
    content: TextResourceContents | BlobResourceContents,
) -> ResourceContent:
    """Convert MCP resource content to ResourceContent."""
    if isinstance(content, TextResourceContents):
        return ResourceContent(
            content=content.text,
            mime_type=content.mimeType,
            meta=content.meta,
        )
    elif isinstance(content, BlobResourceContents):
        return ResourceContent(
            content=base64.b64decode(content.blob),
            mime_type=content.mimeType,
            meta=content.meta,
        )
    else:
        raise ResourceError(f"Unsupported content type: {type(content)}")

Then use it in both ProxyResource.read() and ProxyTemplate.create_resource().

📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between fbdbd1a and 8510b17.

📒 Files selected for processing (2)
  • src/fastmcp/server/providers/base.py
  • src/fastmcp/server/providers/proxy.py
💤 Files with no reviewable changes (1)
  • src/fastmcp/server/providers/base.py
🧰 Additional context used
📓 Path-based instructions (1)
**/*.py

📄 CodeRabbit inference engine (AGENTS.md)

**/*.py: Use Python ≥ 3.10 with full type annotations
Never use bare except - be specific with exception types

Files:

  • src/fastmcp/server/providers/proxy.py
🧠 Learnings (1)
📓 Common learnings
Learnt from: CR
Repo: jlowin/fastmcp PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-21T21:37:55.015Z
Learning: Applies to src/fastmcp/__init__.py : All module exports should be intentional - only re-export to `fastmcp.*` for fundamental types like `FastMCP` and `Client`, prefer users importing from specific submodules for specialized features
Learnt from: CR
Repo: jlowin/fastmcp PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-21T21:37:55.015Z
Learning: Applies to src/fastmcp/**/__init__.py : Core types that define a module's purpose should be exported (e.g., `Middleware` from `fastmcp.server.middleware`), while specialized features can live in submodules
🪛 Ruff (0.14.8)
src/fastmcp/server/providers/proxy.py

220-222: Avoid specifying long messages outside the exception class

(TRY003)


236-236: Avoid specifying long messages outside the exception class

(TRY003)


288-288: Unused method argument: uri

(ARG002)


290-290: Unused method argument: context

(ARG002)


305-307: Avoid specifying long messages outside the exception class

(TRY003)


321-321: Avoid specifying long messages outside the exception class

(TRY003)


613-613: Unused function argument: context

(ARG001)


623-623: Unused function argument: context

(ARG001)


645-645: Unused function argument: response_type

(ARG001)


647-647: Unused function argument: context

(ARG001)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Run tests: Python 3.10 on windows-latest
🔇 Additional comments (2)
src/fastmcp/server/providers/proxy.py (2)

764-764: Note: This code relies on private MCP SDK internals that could break with SDK updates.

Line 764 accesses session._exit_stack, a private attribute of ServerSession. The MCP SDK does not expose a public API for registering session exit callbacks. This creates a fragile dependency on internal implementation details. Consider pinning the MCP SDK version and adding a comment documenting this reliance, or investigate whether FastMCP's session event system (connect/disconnect events) provides an alternative approach.


462-538: Consider implementing parallel loading for provider initialization.

The list_tools(), list_resources(), list_resource_templates(), and list_prompts() methods currently load from a single client sequentially. If these are called together during initialization, using asyncio.gather() to load them in parallel could improve startup time.

Note: This should be verified against your PR scope and requirements. Parallel loading may be intentionally deferred or implemented at a different layer (e.g., when multiple providers are mounted to a FastMCP instance).

Comment on lines +112 to +120
async def run(
self,
arguments: dict[str, Any],
context: Context | None = None,
) -> ToolResult:
"""Executes the tool by making a call through the client."""
client = await self._get_client()
async with client:
context = get_context()
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Unused context parameter is immediately overwritten.

The context parameter in the run() method signature is never used—it's immediately overwritten by get_context() at Line 120. This is misleading for callers who might pass a context expecting it to be used.

🔎 Proposed fix

Either remove the parameter if it's not required by the parent signature, or use the provided context when available:

 async def run(
     self,
     arguments: dict[str, Any],
-    context: Context | None = None,
 ) -> ToolResult:
     """Executes the tool by making a call through the client."""
     client = await self._get_client()
     async with client:
         context = get_context()

Or if you want to support both patterns:

 async def run(
     self,
     arguments: dict[str, Any],
     context: Context | None = None,
 ) -> ToolResult:
     """Executes the tool by making a call through the client."""
     client = await self._get_client()
     async with client:
-        context = get_context()
+        context = context or get_context()
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
async def run(
self,
arguments: dict[str, Any],
context: Context | None = None,
) -> ToolResult:
"""Executes the tool by making a call through the client."""
client = await self._get_client()
async with client:
context = get_context()
async def run(
self,
arguments: dict[str, Any],
context: Context | None = None,
) -> ToolResult:
"""Executes the tool by making a call through the client."""
client = await self._get_client()
async with client:
context = context or get_context()
🤖 Prompt for AI Agents
In src/fastmcp/server/providers/proxy.py around lines 112 to 120, the run()
method's context parameter is immediately overwritten by get_context(), making
the passed-in context unused; either remove the parameter if it's not required
by the parent signature, or preserve and use the provided context by replacing
the overwrite with logic like "if context is None: context = get_context()" (or
equivalent) so the caller-supplied Context is honored while still falling back
to get_context() when none is provided.

Comment on lines +144 to +145
if result.isError:
raise ToolError(cast(mcp.types.TextContent, result.content[0]).text)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Validate content type before casting to TextContent.

When result.isError is true, the code assumes content[0] is a TextContent and casts it without validation. If the remote server returns a different content type in the error response, this will fail at runtime when accessing .text.

🔎 Proposed fix
 if result.isError:
-    raise ToolError(cast(mcp.types.TextContent, result.content[0]).text)
+    error_content = result.content[0] if result.content else None
+    if isinstance(error_content, mcp.types.TextContent):
+        raise ToolError(error_content.text)
+    else:
+        raise ToolError("Tool execution failed with non-text error content")
🤖 Prompt for AI Agents
In src/fastmcp/server/providers/proxy.py around lines 144-145, the code
unconditionally casts result.content[0] to mcp.types.TextContent and accesses
.text when result.isError is true; validate the content type before casting
(e.g., check isinstance(content[0], mcp.types.TextContent) or check a
type/member indicator) and only access .text when safe, otherwise fall back to a
safe string representation (like str(content[0]) or content[0].get("message") if
it's a dict) when constructing the ToolError; ensure you handle empty content
lists and include a clear fallback error message so the raise never assumes a
TextContent shape.

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

Labels

client Related to the FastMCP client SDK or client-side functionality. enhancement Improvement to existing functionality. For issues and smaller PR improvements. server Related to FastMCP server implementation or server-side functionality.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant