transparent mode uses new, builtin technology
This commit is contained in:
@@ -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",
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -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
@@ -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)
|
||||||
|
|||||||
@@ -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,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
"""Test package for local helper imports."""
|
||||||
@@ -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:
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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())
|
||||||
@@ -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"
|
||||||
|
]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user