xsecuremcp / src /fastmcp /server /proxy.py
Jeremiah Lowin
Expand support for various method interactions
0feb19f
Raw
History Blame
8.06 kB
from typing import Any, cast
import mcp.types
from mcp.types import BlobResourceContents, TextResourceContents
import fastmcp
from fastmcp.client import Client
from fastmcp.prompts import Message, Prompt
from fastmcp.resources import Resource, ResourceTemplate
from fastmcp.server.context import Context
from fastmcp.server.server import FastMCP
from fastmcp.tools.tool import Tool
from fastmcp.utilities.func_metadata import func_metadata
from fastmcp.utilities.logging import get_logger
logger = get_logger(__name__)
def _proxy_passthrough():
pass
class ProxyTool(Tool):
def __init__(self, client: "Client", **kwargs):
super().__init__(**kwargs)
self._client = client
@classmethod
async def from_client(cls, client: "Client", tool: mcp.types.Tool) -> "ProxyTool":
return cls(
client=client,
name=tool.name,
description=tool.description,
parameters=tool.inputSchema,
fn=_proxy_passthrough,
fn_metadata=func_metadata(_proxy_passthrough),
is_async=True,
)
async def run(
self, arguments: dict[str, Any], context: Context | None = None
) -> Any:
# the client context manager will swallow any exceptions inside a TaskGroup
# so we return the raw result and raise an exception ourselves
async with self._client:
result = await self._client.call_tool(
self.name, arguments, _return_raw_result=True
)
if result.isError:
raise ValueError(cast(mcp.types.TextContent, result.content[0]).text)
return result.content
class ProxyResource(Resource):
def __init__(
self, client: "Client", *, _value: str | bytes | None = None, **kwargs
):
super().__init__(**kwargs)
self._client = client
self._value = _value
@classmethod
async def from_client(
cls, client: "Client", resource: mcp.types.Resource
) -> "ProxyResource":
return cls(
client=client,
uri=resource.uri,
name=resource.name,
description=resource.description,
mime_type=resource.mimeType,
)
async def read(self) -> str | bytes:
if self._value is not None:
return self._value
async with self._client:
result = await self._client.read_resource(self.uri)
if isinstance(result[0], TextResourceContents):
return result[0].text
elif isinstance(result[0], BlobResourceContents):
return result[0].blob
else:
raise ValueError(f"Unsupported content type: {type(result[0])}")
class ProxyTemplate(ResourceTemplate):
def __init__(self, client: "Client", **kwargs):
super().__init__(**kwargs)
self._client = client
@classmethod
async def from_client(
cls, client: "Client", template: mcp.types.ResourceTemplate
) -> "ProxyTemplate":
return cls(
client=client,
uri_template=template.uriTemplate,
name=template.name,
description=template.description,
fn=_proxy_passthrough,
parameters={},
)
async def create_resource(self, uri: str, params: dict[str, Any]) -> ProxyResource:
async with self._client:
result = await self._client.read_resource(uri)
if isinstance(result[0], TextResourceContents):
value = result[0].text
elif isinstance(result[0], BlobResourceContents):
value = result[0].blob
else:
raise ValueError(f"Unsupported content type: {type(result[0])}")
return ProxyResource(
client=self._client,
uri=uri,
name=self.name,
description=self.description,
mime_type=result[0].mimeType,
contents=result,
_value=value,
)
class ProxyPrompt(Prompt):
def __init__(self, client: "Client", **kwargs):
super().__init__(**kwargs)
self._client = client
@classmethod
async def from_client(
cls, client: "Client", prompt: mcp.types.Prompt
) -> "ProxyPrompt":
return cls(
client=client,
name=prompt.name,
description=prompt.description,
arguments=[a.model_dump() for a in prompt.arguments or []],
fn=_proxy_passthrough,
)
async def render(self, arguments: dict[str, Any]) -> list[Message]:
async with self._client:
result = await self._client.get_prompt(self.name, arguments)
return [Message(role=m.role, content=m.content) for m in result.messages]
class FastMCPProxy(FastMCP):
def __init__(self, _async_constructor: bool, **kwargs):
if not _async_constructor:
raise ValueError(
"FastMCPProxy() was initialied unexpectedly. Please use a constructor like `FastMCPProxy.from_client()` instead."
)
super().__init__(**kwargs)
@classmethod
async def from_client(
cls,
client: "Client",
name: str | None = None,
**settings: fastmcp.settings.ServerSettings,
) -> "FastMCPProxy":
"""Create a FastMCP proxy server from a client.
This method creates a new FastMCP server instance that proxies requests to the provided client.
It discovers the client's tools, resources, prompts, and templates, and creates corresponding
components in the server that forward requests to the client.
Args:
client: The client to proxy requests to
name: Optional name for the new FastMCP server (defaults to client name if available)
**settings: Additional settings for the FastMCP server
Returns:
A FastMCP server that proxies requests to the client
"""
server = cls(name=name, **settings, _async_constructor=True)
async with client:
# Register proxies for client tools
tools = await client.list_tools()
for tool in tools:
tool_proxy = await ProxyTool.from_client(client, tool)
server._tool_manager._tools[tool_proxy.name] = tool_proxy
logger.debug(f"Created proxy for tool: {tool_proxy.name}")
# Register proxies for client resources
resources = await client.list_resources()
for resource in resources:
resource_proxy = await ProxyResource.from_client(client, resource)
server._resource_manager._resources[str(resource_proxy.uri)] = (
resource_proxy
)
logger.debug(f"Created proxy for resource: {resource_proxy.uri}")
# Register proxies for client resource templates
templates = await client.list_resource_templates()
for template in templates:
template_proxy = await ProxyTemplate.from_client(client, template)
server._resource_manager._templates[template_proxy.uri_template] = (
template_proxy
)
logger.debug(
f"Created proxy for template: {template_proxy.uri_template}"
)
# Register proxies for client prompts
prompts = await client.list_prompts()
for prompt in prompts:
prompt_proxy = await ProxyPrompt.from_client(client, prompt)
server._prompt_manager._prompts[prompt_proxy.name] = prompt_proxy
logger.debug(f"Created proxy for prompt: {prompt_proxy.name}")
logger.info(f"Created server '{server.name}' proxying to client: {client}")
return server
@classmethod
async def from_server(cls, server: FastMCP, **settings: Any) -> "FastMCPProxy":
client = Client(transport=fastmcp.client.transports.FastMCPTransport(server))
return await cls.from_client(client, **settings)