transparent mode uses new, builtin technology

This commit is contained in:
lda
2026-04-30 05:09:30 +07:00 Verified
parent dedb1e2519
commit 9d67bd09fc
12 changed files with 278 additions and 6 deletions
+14
View File
@@ -6,6 +6,8 @@ from .broker_server import (
build_service_from_config, build_service_from_config,
create_broker_server, create_broker_server,
load_broker_config, load_broker_config,
run_broker_server,
run_transparent_proxy_server,
) )
from .capabilities import ( from .capabilities import (
CatalogNodeEntry, CatalogNodeEntry,
@@ -33,6 +35,12 @@ from .models import (
from .mcp_sdk_adapter import McpSdkAdapter from .mcp_sdk_adapter import McpSdkAdapter
from .service import WfMcpService from .service import WfMcpService
from .store import FileStore, Store from .store import FileStore, Store
from .transparent_proxy import (
broker_config_to_fastmcp_config,
connection_to_fastmcp_server_config,
create_transparent_proxy_client,
create_transparent_proxy_server,
)
from .wrappers import wrap_discovered_tool from .wrappers import wrap_discovered_tool
__all__ = [ __all__ = [
@@ -58,12 +66,18 @@ __all__ = [
"ToolCallResult", "ToolCallResult",
"WfMcpService", "WfMcpService",
"build_service_from_config", "build_service_from_config",
"broker_config_to_fastmcp_config",
"connection_to_fastmcp_server_config",
"create_broker_server", "create_broker_server",
"create_transparent_proxy_client",
"create_transparent_proxy_server",
"discover_connection_capabilities", "discover_connection_capabilities",
"load_broker_config", "load_broker_config",
"make_event", "make_event",
"parse_connection_id", "parse_connection_id",
"qualify_node_name", "qualify_node_name",
"run_broker_server",
"run_transparent_proxy_server",
"specs_from_discovered_tools", "specs_from_discovered_tools",
"wrap_discovered_tool", "wrap_discovered_tool",
] ]
+17
View File
@@ -13,6 +13,7 @@ from .mcp_sdk_adapter import McpSdkAdapter
from .models import BrokerConfig, ConnectionConfig from .models import BrokerConfig, ConnectionConfig
from .service import WfMcpService from .service import WfMcpService
from .store import FileStore from .store import FileStore
from .transparent_proxy import create_transparent_proxy_server
def load_broker_config(path: str | Path) -> BrokerConfig: def load_broker_config(path: str | Path) -> BrokerConfig:
@@ -221,5 +222,21 @@ def run_broker_server(config_path: str | Path, transport: str = "stdio") -> None
server.run(transport=normalize_transport(transport)) server.run(transport=normalize_transport(transport))
def run_transparent_proxy_server(
config_path: str | Path,
transport: str = "stdio",
*,
resources_as_tools: bool = False,
prompts_as_tools: bool = False,
) -> None:
config = load_broker_config(config_path)
server = create_transparent_proxy_server(
config,
resources_as_tools=resources_as_tools,
prompts_as_tools=prompts_as_tools,
)
server.run(transport=normalize_transport(transport), show_banner=False)
if __name__ == "__main__": if __name__ == "__main__":
main() main()
+26 -1
View File
@@ -10,6 +10,7 @@ from .broker_server import (
build_service_from_config, build_service_from_config,
load_broker_config, load_broker_config,
run_broker_server, run_broker_server,
run_transparent_proxy_server,
) )
@@ -30,6 +31,22 @@ def build_parser() -> argparse.ArgumentParser:
choices=["stdio", "sse", "streamable-http", "streamable_http"], choices=["stdio", "sse", "streamable-http", "streamable_http"],
help="Transport to run the broker server with.", help="Transport to run the broker server with.",
) )
serve.add_argument(
"--mode",
default="proxy",
choices=["broker", "proxy"],
help="Run admin/workflow broker mode or transparent proxy mode.",
)
serve.add_argument(
"--resources-as-tools",
action="store_true",
help="Expose proxied resources through list_resources/read_resource tools.",
)
serve.add_argument(
"--prompts-as-tools",
action="store_true",
help="Expose proxied prompts through list_prompts/get_prompt tools.",
)
subparsers.add_parser("connections", help="List configured connections.") subparsers.add_parser("connections", help="List configured connections.")
subparsers.add_parser("status", help="Show connection status and snapshot counts.") subparsers.add_parser("status", help="Show connection status and snapshot counts.")
@@ -92,7 +109,15 @@ def main(argv: list[str] | None = None) -> int:
args = parser.parse_args(argv) args = parser.parse_args(argv)
if args.command == "serve": if args.command == "serve":
run_broker_server(args.config, args.transport) if args.mode == "proxy":
run_transparent_proxy_server(
args.config,
args.transport,
resources_as_tools=args.resources_as_tools,
prompts_as_tools=args.prompts_as_tools,
)
else:
run_broker_server(args.config, args.transport)
return 0 return 0
service = _service_from_config(args.config) service = _service_from_config(args.config)
+103
View File
@@ -0,0 +1,103 @@
from __future__ import annotations
from typing import Any
from fastmcp import FastMCP
from fastmcp.client import Client
from fastmcp.client.transports.config import MCPConfigTransport
from fastmcp.client.transports.memory import FastMCPTransport
from fastmcp.mcp_config import MCPConfig
from fastmcp.server import create_proxy
from fastmcp.server.transforms import Namespace, PromptsAsTools, ResourcesAsTools
from .models import BrokerConfig, ConnectionConfig
def connection_to_fastmcp_server_config(
connection: ConnectionConfig,
) -> dict[str, Any]:
metadata = dict(connection.metadata)
transport = metadata.get("transport", "stdio")
if transport == "streamable_http":
metadata["transport"] = "http"
if transport == "stdio":
return {
"command": metadata["command"],
"args": list(metadata.get("args", [])),
"env": dict(metadata.get("env", {})),
"cwd": metadata.get("cwd"),
"transport": "stdio",
"description": metadata.get("description"),
}
if transport in {"http", "streamable-http", "sse"}:
return {
"url": metadata["url"],
"transport": transport,
"headers": dict(metadata.get("headers", {})),
"description": metadata.get("description"),
}
raise ValueError(f"unsupported MCP transport {transport!r}")
def broker_config_to_fastmcp_config(config: BrokerConfig) -> MCPConfig:
return MCPConfig.from_dict(
{
"mcpServers": {
connection.id: connection_to_fastmcp_server_config(connection)
for connection in config.connections
if connection.enabled
}
}
)
def create_transparent_proxy_server(
config: BrokerConfig,
*,
resources_as_tools: bool = False,
prompts_as_tools: bool = False,
) -> FastMCP[Any]:
root = FastMCP(
"wf-mcp-transparent-proxy",
instructions=(
"Transparent MCP proxy over configured upstream MCP connections. "
"Upstream tools, resources, and prompts are exposed as first-class "
"broker capabilities with connection-qualified names."
),
)
for connection in config.connections:
if not connection.enabled:
continue
server_config = broker_config_to_fastmcp_config(
BrokerConfig(store_root=config.store_root, connections=[connection])
)
transport = MCPConfigTransport(server_config, name_as_prefix=False)
client = Client(transport=transport, name=f"wf-mcp:{connection.id}")
proxy = create_proxy(client, name=f"Proxy-{connection.id}")
proxy.add_transform(Namespace(connection.id))
root.mount(proxy)
if resources_as_tools:
root.add_transform(ResourcesAsTools(root))
if prompts_as_tools:
root.add_transform(PromptsAsTools(root))
return root
def create_transparent_proxy_client(
config: BrokerConfig,
*,
resources_as_tools: bool = False,
prompts_as_tools: bool = False,
) -> Client[FastMCPTransport]:
return Client(
FastMCPTransport(
create_transparent_proxy_server(
config,
resources_as_tools=resources_as_tools,
prompts_as_tools=prompts_as_tools,
)
)
)
+1
View File
@@ -0,0 +1 @@
"""Test package for local helper imports."""
+5 -1
View File
@@ -13,7 +13,11 @@ from wf_mcp import (
load_broker_config, load_broker_config,
) )
from test_wf_mcp_support import FailingDiscoveryAdapter, FakeAdapter, local_temp_root from tests.test_wf_mcp_support import (
FailingDiscoveryAdapter,
FakeAdapter,
local_temp_root,
)
def test_load_broker_config_resolves_relative_store_root() -> None: def test_load_broker_config_resolves_relative_store_root() -> None:
+22 -1
View File
@@ -5,7 +5,7 @@ from pathlib import Path
from wf_mcp.cli import build_parser, main from wf_mcp.cli import build_parser, main
from test_wf_mcp_support import local_temp_root from tests.test_wf_mcp_support import local_temp_root
def _write_config(path: Path) -> None: def _write_config(path: Path) -> None:
@@ -34,6 +34,27 @@ def test_build_parser_accepts_serve_transport() -> None:
assert args.command == "serve" assert args.command == "serve"
assert args.transport == "streamable_http" assert args.transport == "streamable_http"
assert args.mode == "proxy"
assert args.resources_as_tools is False
assert args.prompts_as_tools is False
def test_build_parser_accepts_proxy_compatibility_flags() -> None:
parser = build_parser()
args = parser.parse_args(
[
"--config",
"wf_mcp.config.json",
"serve",
"--resources-as-tools",
"--prompts-as-tools",
]
)
assert args.command == "serve"
assert args.mode == "proxy"
assert args.resources_as_tools is True
assert args.prompts_as_tools is True
def test_cli_connections_prints_configured_connections(capsys) -> None: def test_cli_connections_prints_configured_connections(capsys) -> None:
+1 -1
View File
@@ -6,7 +6,7 @@ import pytest
from wf_mcp import ConnectionConfig, FileStore, McpSdkAdapter, WfMcpService from wf_mcp import ConnectionConfig, FileStore, McpSdkAdapter, WfMcpService
from test_wf_mcp_support import ( from tests.test_wf_mcp_support import (
everything_server_connection, everything_server_connection,
fixture_server_path, fixture_server_path,
local_temp_root, local_temp_root,
+1 -1
View File
@@ -13,7 +13,7 @@ from wf_mcp import (
) )
from wf_mcp.error_info import error_payload from wf_mcp.error_info import error_payload
from test_wf_mcp_support import ( from tests.test_wf_mcp_support import (
FailingDiscoveryAdapter, FailingDiscoveryAdapter,
FakeAdapter, FakeAdapter,
echo_tool, echo_tool,
+1 -1
View File
@@ -2,7 +2,7 @@ from __future__ import annotations
from wf_mcp import AuthRecord, FileStore from wf_mcp import AuthRecord, FileStore
from test_wf_mcp_support import local_temp_root from tests.test_wf_mcp_support import local_temp_root
def test_file_store_round_trips_auth() -> None: def test_file_store_round_trips_auth() -> None:
+75
View File
@@ -0,0 +1,75 @@
from __future__ import annotations
import asyncio
import sys
from wf_mcp import BrokerConfig, ConnectionConfig, create_transparent_proxy_client
from tests.test_wf_mcp_support import fixture_server_path, local_temp_root
def test_transparent_proxy_lists_and_calls_upstream_tools() -> None:
config = BrokerConfig(
store_root=local_temp_root() / "transparent_proxy_store",
connections=[
ConnectionConfig(
id="fixture.personal",
server="fixture",
account="personal",
metadata={
"transport": "stdio",
"command": sys.executable,
"args": [fixture_server_path()],
},
)
],
)
async def run_proxy() -> None:
client = create_transparent_proxy_client(config)
async with client:
tools = await client.list_tools()
names = [tool.name for tool in tools]
assert "fixture.personal_echo_tool" in names
result = await client.call_tool(
"fixture.personal_echo_tool",
{"text": "hello"},
)
assert result.structured_content == {"echoed": "hello"}
asyncio.run(run_proxy())
def test_transparent_proxy_can_expose_resources_and_prompts_as_tools() -> None:
config = BrokerConfig(
store_root=local_temp_root() / "transparent_proxy_helper_store",
connections=[
ConnectionConfig(
id="fixture.personal",
server="fixture",
account="personal",
metadata={
"transport": "stdio",
"command": sys.executable,
"args": [fixture_server_path()],
},
)
],
)
async def run_proxy() -> None:
client = create_transparent_proxy_client(
config,
resources_as_tools=True,
prompts_as_tools=True,
)
async with client:
tools = await client.list_tools()
names = [tool.name for tool in tools]
assert "list_resources" in names
assert "read_resource" in names
assert "list_prompts" in names
assert "get_prompt" in names
asyncio.run(run_proxy())
+12
View File
@@ -13,6 +13,18 @@
"@modelcontextprotocol/server-everything" "@modelcontextprotocol/server-everything"
] ]
} }
}, {
"id": "context7.default",
"server": "context7",
"account": "default",
"enabled": true,
"metadata": {
"transport": "stdio",
"command": "pnpx",
"args": [
"@upstash/context7-mcp"
]
}
} }
] ]
} }