tis is the greatest thing ever

This commit is contained in:
lda
2026-05-15 23:48:02 +07:00 Verified
parent 9a035e9557
commit db2e4235d1
9 changed files with 186 additions and 48 deletions
@@ -1,6 +1,5 @@
from __future__ import annotations
import re
from collections.abc import Callable, Sequence
from typing import Any
@@ -11,8 +10,7 @@ from pydantic import ConfigDict
from pydantic.json_schema import SkipJsonSchema
from .resource_links import rewrite_resource_link_content
_URI_PATTERN = re.compile(r"^([^:]+://)(.*?)$")
from ..shared.names import connection_id_to_resource_path
class ResourceLinkRewritingTool(Tool):
@@ -55,7 +53,7 @@ class ResourceLinkNamespace(Transform):
"""Rewrite resource links returned by tools into one namespace."""
def __init__(self, prefix: str) -> None:
self._prefix = prefix
self._prefix = connection_id_to_resource_path(prefix)
def __repr__(self) -> str:
return f"ResourceLinkNamespace({self._prefix!r})"
@@ -80,8 +78,7 @@ class ResourceLinkNamespace(Transform):
def _transform_uri(self, uri: str) -> str:
"""Match FastMCP Namespace URI projection for tool-returned links."""
match = _URI_PATTERN.match(uri)
if match is None:
protocol, separator, path = uri.partition("://")
if not separator:
return uri
protocol, path = match.groups()
return f"{protocol}{self._prefix}/{path}"
return f"{protocol}://{self._prefix}/{path}"
-5
View File
@@ -53,11 +53,6 @@ def _validate_connection_ids(
continue
if connection_id in RESERVED_CONNECTION_IDS:
errors.append(f"connection id {connection_id!r} is reserved by wf-mcp")
if "_" in connection_id:
errors.append(
f"connection id {connection_id!r} must not contain '_' because "
"FastMCP Namespace uses '_' as the tool-name separator"
)
if not _NAMESPACE_ID_RE.fullmatch(connection_id):
errors.append(
f"connection id {connection_id!r} must contain only letters, "
+4
View File
@@ -2,7 +2,9 @@ from .errors import error_payload, root_exception
from .names import (
ADMIN_NAMESPACE,
LdaNamespace,
ProxyNamespace,
ProxyToolName,
connection_id_to_resource_path,
is_admin_tool_name,
namespaced_tool_name,
parse_namespaced_tool_name,
@@ -12,7 +14,9 @@ from .pagination import clamp_limit, make_cursor, paginate_items, parse_cursor
__all__ = [
"ADMIN_NAMESPACE",
"LdaNamespace",
"ProxyNamespace",
"ProxyToolName",
"connection_id_to_resource_path",
"clamp_limit",
"error_payload",
"is_admin_tool_name",
+147 -3
View File
@@ -2,7 +2,25 @@ from __future__ import annotations
from dataclasses import dataclass
from fastmcp.server.transforms import Namespace
import re
from collections.abc import Sequence
from typing import TYPE_CHECKING
from fastmcp.server.transforms import (
GetPromptNext,
GetResourceNext,
GetResourceTemplateNext,
GetToolNext,
Namespace,
Transform,
)
from fastmcp.utilities.versions import VersionSpec
if TYPE_CHECKING:
from fastmcp.prompts.base import Prompt
from fastmcp.resources.base import Resource
from fastmcp.resources.template import ResourceTemplate
from fastmcp.tools.base import Tool
ADMIN_NAMESPACE = "wf.admin"
RESERVED_CONNECTION_IDS = frozenset({ADMIN_NAMESPACE, "wf.mcp"})
@@ -17,7 +35,7 @@ class ProxyToolName:
def namespaced_tool_name(connection_id: str, local_name: str) -> str:
return f"{connection_id}_{local_name}"
return f"{connection_id}.{local_name}"
def parse_namespaced_tool_name(
@@ -27,7 +45,7 @@ def parse_namespaced_tool_name(
matches = [
connection_id
for connection_id in connection_ids
if proxy_name.startswith(f"{connection_id}_")
if proxy_name.startswith(f"{connection_id}.")
]
if not matches:
return None
@@ -54,3 +72,129 @@ class LdaNamespace(Namespace):
# FastMCP's public Namespace transform uses underscores; override its
# private prefix so admin tools keep their dotted wf.admin.* names.
self._name_prefix = f"{prefix}."
_URI_PATTERN = re.compile(r"^([^:]+://)(.*?)$")
def connection_id_to_resource_path(connection_id: str) -> str:
"""Project a dotted connection id into URI path segments."""
return connection_id.replace(".", "/")
class ProxyNamespace(Transform):
"""Project MCP proxy names with dots for callables and slashes for URIs."""
def __init__(self, connection_id: str) -> None:
self._connection_id = connection_id
self._name_prefix = f"{connection_id}."
self._resource_prefix = f"{connection_id_to_resource_path(connection_id)}/"
def __repr__(self) -> str:
return f"ProxyNamespace({self._connection_id!r})"
async def list_tools(self, tools: Sequence[Tool]) -> Sequence[Tool]:
return [tool.model_copy(update={"name": self._name(tool.name)}) for tool in tools]
async def get_tool(
self,
name: str,
call_next: GetToolNext,
*,
version: VersionSpec | None = None,
) -> Tool | None:
local_name = self._local_name(name)
if local_name is None:
return None
tool = await call_next(local_name, version=version)
return None if tool is None else tool.model_copy(update={"name": name})
async def list_prompts(self, prompts: Sequence[Prompt]) -> Sequence[Prompt]:
return [
prompt.model_copy(update={"name": self._name(prompt.name)})
for prompt in prompts
]
async def get_prompt(
self,
name: str,
call_next: GetPromptNext,
*,
version: VersionSpec | None = None,
) -> Prompt | None:
local_name = self._local_name(name)
if local_name is None:
return None
prompt = await call_next(local_name, version=version)
return None if prompt is None else prompt.model_copy(update={"name": name})
async def list_resources(self, resources: Sequence[Resource]) -> Sequence[Resource]:
return [
resource.model_copy(update={"uri": self._uri(str(resource.uri))})
for resource in resources
]
async def get_resource(
self,
uri: str,
call_next: GetResourceNext,
*,
version: VersionSpec | None = None,
) -> Resource | None:
local_uri = self._local_uri(uri)
if local_uri is None:
return None
resource = await call_next(local_uri, version=version)
return None if resource is None else resource.model_copy(update={"uri": uri})
async def list_resource_templates(
self,
templates: Sequence[ResourceTemplate],
) -> Sequence[ResourceTemplate]:
return [
template.model_copy(
update={"uri_template": self._uri(template.uri_template)}
)
for template in templates
]
async def get_resource_template(
self,
uri: str,
call_next: GetResourceTemplateNext,
*,
version: VersionSpec | None = None,
) -> ResourceTemplate | None:
local_uri = self._local_uri(uri)
if local_uri is None:
return None
template = await call_next(local_uri, version=version)
return (
None
if template is None
else template.model_copy(update={"uri_template": self._uri(template.uri_template)})
)
def _name(self, name: str) -> str:
return f"{self._name_prefix}{name}"
def _local_name(self, name: str) -> str | None:
if not name.startswith(self._name_prefix):
return None
return name[len(self._name_prefix) :]
def _uri(self, uri: str) -> str:
match = _URI_PATTERN.match(uri)
if match is None:
return uri
protocol, path = match.groups()
return f"{protocol}{self._resource_prefix}{path}"
def _local_uri(self, uri: str) -> str | None:
match = _URI_PATTERN.match(uri)
if match is None:
return None
protocol, path = match.groups()
if not path.startswith(self._resource_prefix):
return None
return f"{protocol}{path[len(self._resource_prefix):]}"
+2 -3
View File
@@ -10,11 +10,10 @@ from fastmcp import FastMCP
from fastmcp.client import Client
from fastmcp.client.transports.config import MCPConfigTransport
from fastmcp.server import create_proxy
from fastmcp.server.transforms import Namespace
from ..models import BrokerConfig, ConnectionConfig
from ..proxy_results import ResourceLinkNamespace
from ..proxy_config import broker_config_to_fastmcp_config
from ..shared.names import ProxyNamespace
ProxyT = TypeVar("ProxyT")
ProxyMountFactory = Callable[[ConnectionConfig, Path], "ProxyMount[ProxyT]"]
@@ -98,7 +97,7 @@ def create_proxy_mount(
transport = MCPConfigTransport(server_config, name_as_prefix=False)
client = Client(transport=transport, name=f"wf-mcp:{connection.id}")
proxy: FastMCP[Any] = create_proxy(client, name=f"Proxy-{connection.id}")
proxy.add_transform(Namespace(connection.id))
proxy.add_transform(ProxyNamespace(connection.id))
proxy.add_transform(ResourceLinkNamespace(connection.id))
return ProxyMount(
connection_id=connection.id,