Names at the MCP side + tools/list advanced
This commit is contained in:
@@ -40,6 +40,13 @@ from .models import (
|
|||||||
RawWorkflowPlan,
|
RawWorkflowPlan,
|
||||||
)
|
)
|
||||||
from .mcp_sdk_adapter import McpSdkAdapter
|
from .mcp_sdk_adapter import McpSdkAdapter
|
||||||
|
from .names import (
|
||||||
|
ADMIN_NAMESPACE,
|
||||||
|
ProxyToolName,
|
||||||
|
is_admin_tool_name,
|
||||||
|
namespaced_tool_name,
|
||||||
|
parse_namespaced_tool_name,
|
||||||
|
)
|
||||||
from .proxy_validation import ProxyConfigError, validate_transparent_proxy_config
|
from .proxy_validation import ProxyConfigError, validate_transparent_proxy_config
|
||||||
from .service import WfMcpService
|
from .service import WfMcpService
|
||||||
from .store import FileStore, Store
|
from .store import FileStore, Store
|
||||||
@@ -55,6 +62,7 @@ from .wrappers import wrap_discovered_tool
|
|||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"AuthRecord",
|
"AuthRecord",
|
||||||
|
"ADMIN_NAMESPACE",
|
||||||
"BackendAdapter",
|
"BackendAdapter",
|
||||||
"BrokerConfig",
|
"BrokerConfig",
|
||||||
"BrokerConfigManager",
|
"BrokerConfigManager",
|
||||||
@@ -77,6 +85,7 @@ __all__ = [
|
|||||||
"McpEvent",
|
"McpEvent",
|
||||||
"McpSdkAdapter",
|
"McpSdkAdapter",
|
||||||
"ProxyConfigError",
|
"ProxyConfigError",
|
||||||
|
"ProxyToolName",
|
||||||
"RawWorkflowPlan",
|
"RawWorkflowPlan",
|
||||||
"Store",
|
"Store",
|
||||||
"StdioConnectionMetadata",
|
"StdioConnectionMetadata",
|
||||||
@@ -91,9 +100,12 @@ __all__ = [
|
|||||||
"create_transparent_proxy_client",
|
"create_transparent_proxy_client",
|
||||||
"create_transparent_proxy_server",
|
"create_transparent_proxy_server",
|
||||||
"discover_connection_capabilities",
|
"discover_connection_capabilities",
|
||||||
|
"is_admin_tool_name",
|
||||||
"load_broker_config",
|
"load_broker_config",
|
||||||
"make_event",
|
"make_event",
|
||||||
|
"namespaced_tool_name",
|
||||||
"parse_connection_id",
|
"parse_connection_id",
|
||||||
|
"parse_namespaced_tool_name",
|
||||||
"qualify_node_name",
|
"qualify_node_name",
|
||||||
"run_broker_server",
|
"run_broker_server",
|
||||||
"run_transparent_proxy_server",
|
"run_transparent_proxy_server",
|
||||||
|
|||||||
@@ -0,0 +1,42 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
ADMIN_NAMESPACE = "wf.mcp"
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, slots=True)
|
||||||
|
class ProxyToolName:
|
||||||
|
proxy_name: str
|
||||||
|
connection_id: str
|
||||||
|
local_name: str
|
||||||
|
|
||||||
|
|
||||||
|
def namespaced_tool_name(connection_id: str, local_name: str) -> str:
|
||||||
|
return f"{connection_id}_{local_name}"
|
||||||
|
|
||||||
|
|
||||||
|
def parse_namespaced_tool_name(
|
||||||
|
proxy_name: str,
|
||||||
|
connection_ids: set[str],
|
||||||
|
) -> ProxyToolName | None:
|
||||||
|
matches = [
|
||||||
|
connection_id
|
||||||
|
for connection_id in connection_ids
|
||||||
|
if proxy_name.startswith(f"{connection_id}_")
|
||||||
|
]
|
||||||
|
if not matches:
|
||||||
|
return None
|
||||||
|
connection_id = max(matches, key=len)
|
||||||
|
local_name = proxy_name[len(connection_id) + 1 :]
|
||||||
|
if not local_name:
|
||||||
|
return None
|
||||||
|
return ProxyToolName(
|
||||||
|
proxy_name=proxy_name,
|
||||||
|
connection_id=connection_id,
|
||||||
|
local_name=local_name,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def is_admin_tool_name(proxy_name: str) -> bool:
|
||||||
|
return proxy_name.startswith(f"{ADMIN_NAMESPACE}_")
|
||||||
@@ -15,19 +15,20 @@ from fastmcp.server.transforms.search import BM25SearchTransform
|
|||||||
|
|
||||||
from .config_manager import BrokerConfigManager, ConfigMutationError
|
from .config_manager import BrokerConfigManager, ConfigMutationError
|
||||||
from .models import BrokerConfig, ConnectionConfig
|
from .models import BrokerConfig, ConnectionConfig
|
||||||
|
from .names import ADMIN_NAMESPACE, is_admin_tool_name, parse_namespaced_tool_name
|
||||||
from .proxy_validation import validate_transparent_proxy_config
|
from .proxy_validation import validate_transparent_proxy_config
|
||||||
|
|
||||||
_ADMIN_NAMESPACE = "wf.mcp"
|
|
||||||
_ADMIN_TOOL_NAMES = [
|
_ADMIN_TOOL_NAMES = [
|
||||||
f"{_ADMIN_NAMESPACE}_list_connections",
|
f"{ADMIN_NAMESPACE}_list_connections",
|
||||||
f"{_ADMIN_NAMESPACE}_get_connection_statuses",
|
f"{ADMIN_NAMESPACE}_get_connection_statuses",
|
||||||
f"{_ADMIN_NAMESPACE}_get_config",
|
f"{ADMIN_NAMESPACE}_get_config",
|
||||||
f"{_ADMIN_NAMESPACE}_reload_config",
|
f"{ADMIN_NAMESPACE}_reload_config",
|
||||||
f"{_ADMIN_NAMESPACE}_add_connection",
|
f"{ADMIN_NAMESPACE}_list_proxy_tools",
|
||||||
f"{_ADMIN_NAMESPACE}_update_connection",
|
f"{ADMIN_NAMESPACE}_add_connection",
|
||||||
f"{_ADMIN_NAMESPACE}_enable_connection",
|
f"{ADMIN_NAMESPACE}_update_connection",
|
||||||
f"{_ADMIN_NAMESPACE}_disable_connection",
|
f"{ADMIN_NAMESPACE}_enable_connection",
|
||||||
f"{_ADMIN_NAMESPACE}_remove_connection",
|
f"{ADMIN_NAMESPACE}_disable_connection",
|
||||||
|
f"{ADMIN_NAMESPACE}_remove_connection",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@@ -78,7 +79,7 @@ class TransparentProxyRuntime:
|
|||||||
self.server.providers[:] = [self.server.local_provider]
|
self.server.providers[:] = [self.server.local_provider]
|
||||||
|
|
||||||
admin = create_proxy_admin_server(self)
|
admin = create_proxy_admin_server(self)
|
||||||
admin.add_transform(Namespace(_ADMIN_NAMESPACE))
|
admin.add_transform(Namespace(ADMIN_NAMESPACE))
|
||||||
self.server.mount(admin)
|
self.server.mount(admin)
|
||||||
|
|
||||||
mounted_connections: list[str] = []
|
mounted_connections: list[str] = []
|
||||||
@@ -103,6 +104,31 @@ class TransparentProxyRuntime:
|
|||||||
"enabled_connection_count": len(mounted_connections),
|
"enabled_connection_count": len(mounted_connections),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async def list_proxy_tools(self) -> list[dict[str, Any]]:
|
||||||
|
config = self.current_config()
|
||||||
|
connection_ids = {
|
||||||
|
connection.id for connection in config.connections if connection.enabled
|
||||||
|
}
|
||||||
|
tools = await self.server.list_tools()
|
||||||
|
result: list[dict[str, Any]] = []
|
||||||
|
for tool in tools:
|
||||||
|
if is_admin_tool_name(tool.name):
|
||||||
|
continue
|
||||||
|
parsed = parse_namespaced_tool_name(tool.name, connection_ids)
|
||||||
|
if parsed is None:
|
||||||
|
continue
|
||||||
|
result.append(
|
||||||
|
{
|
||||||
|
"proxy_name": parsed.proxy_name,
|
||||||
|
"connection_id": parsed.connection_id,
|
||||||
|
"local_name": parsed.local_name,
|
||||||
|
"title": tool.title,
|
||||||
|
"description": tool.description,
|
||||||
|
"enabled": True,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return sorted(result, key=lambda item: item["proxy_name"])
|
||||||
|
|
||||||
|
|
||||||
def create_proxy_admin_server(
|
def create_proxy_admin_server(
|
||||||
runtime: TransparentProxyRuntime,
|
runtime: TransparentProxyRuntime,
|
||||||
@@ -152,6 +178,10 @@ def create_proxy_admin_server(
|
|||||||
async def reload_config() -> dict[str, Any]:
|
async def reload_config() -> dict[str, Any]:
|
||||||
return runtime.reload()
|
return runtime.reload()
|
||||||
|
|
||||||
|
@admin.tool()
|
||||||
|
async def list_proxy_tools() -> list[dict[str, Any]]:
|
||||||
|
return await runtime.list_proxy_tools()
|
||||||
|
|
||||||
@admin.tool()
|
@admin.tool()
|
||||||
async def add_connection(
|
async def add_connection(
|
||||||
connection_id: str,
|
connection_id: str,
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from wf_mcp.names import (
|
||||||
|
is_admin_tool_name,
|
||||||
|
namespaced_tool_name,
|
||||||
|
parse_namespaced_tool_name,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_namespaced_tool_names_are_reversible_with_known_connections() -> None:
|
||||||
|
proxy_name = namespaced_tool_name("everything.default", "get-sum")
|
||||||
|
|
||||||
|
parsed = parse_namespaced_tool_name(
|
||||||
|
proxy_name,
|
||||||
|
{"everything.default", "everything"},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert parsed is not None
|
||||||
|
assert parsed.proxy_name == "everything.default_get-sum"
|
||||||
|
assert parsed.connection_id == "everything.default"
|
||||||
|
assert parsed.local_name == "get-sum"
|
||||||
|
|
||||||
|
|
||||||
|
def test_namespaced_tool_parser_rejects_unknown_and_admin_names() -> None:
|
||||||
|
assert parse_namespaced_tool_name("missing_echo", {"everything.default"}) is None
|
||||||
|
assert is_admin_tool_name("wf.mcp_list_connections") is True
|
||||||
|
assert is_admin_tool_name("everything.default_echo") is False
|
||||||
@@ -3,6 +3,7 @@ from __future__ import annotations
|
|||||||
import asyncio
|
import asyncio
|
||||||
import json
|
import json
|
||||||
import sys
|
import sys
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
@@ -18,6 +19,12 @@ from wf_mcp.broker_server import load_broker_config
|
|||||||
from tests.test_wf_mcp_support import fixture_server_path, local_temp_root
|
from tests.test_wf_mcp_support import fixture_server_path, local_temp_root
|
||||||
|
|
||||||
|
|
||||||
|
def _structured(result: Any) -> dict[str, Any]:
|
||||||
|
content = result.structured_content
|
||||||
|
assert isinstance(content, dict)
|
||||||
|
return content
|
||||||
|
|
||||||
|
|
||||||
def test_transparent_proxy_lists_and_calls_upstream_tools() -> None:
|
def test_transparent_proxy_lists_and_calls_upstream_tools() -> None:
|
||||||
config = BrokerConfig(
|
config = BrokerConfig(
|
||||||
store_root=local_temp_root() / "transparent_proxy_store",
|
store_root=local_temp_root() / "transparent_proxy_store",
|
||||||
@@ -42,10 +49,11 @@ def test_transparent_proxy_lists_and_calls_upstream_tools() -> None:
|
|||||||
names = [tool.name for tool in tools]
|
names = [tool.name for tool in tools]
|
||||||
assert "wf.mcp_list_connections" in names
|
assert "wf.mcp_list_connections" in names
|
||||||
assert "wf.mcp_get_connection_statuses" in names
|
assert "wf.mcp_get_connection_statuses" in names
|
||||||
|
assert "wf.mcp_list_proxy_tools" in names
|
||||||
assert "fixture.personal_echo_tool" in names
|
assert "fixture.personal_echo_tool" in names
|
||||||
|
|
||||||
connections_result = await client.call_tool("wf.mcp_list_connections")
|
connections_result = await client.call_tool("wf.mcp_list_connections")
|
||||||
assert connections_result.structured_content == {
|
assert _structured(connections_result) == {
|
||||||
"result": [
|
"result": [
|
||||||
{
|
{
|
||||||
"id": "fixture.personal",
|
"id": "fixture.personal",
|
||||||
@@ -65,7 +73,15 @@ def test_transparent_proxy_lists_and_calls_upstream_tools() -> None:
|
|||||||
"fixture.personal_echo_tool",
|
"fixture.personal_echo_tool",
|
||||||
{"text": "hello"},
|
{"text": "hello"},
|
||||||
)
|
)
|
||||||
assert result.structured_content == {"echoed": "hello"}
|
assert _structured(result) == {"echoed": "hello"}
|
||||||
|
|
||||||
|
proxy_tools_result = await client.call_tool("wf.mcp_list_proxy_tools")
|
||||||
|
proxy_tools = _structured(proxy_tools_result)["result"]
|
||||||
|
assert len(proxy_tools) == 1
|
||||||
|
assert proxy_tools[0]["proxy_name"] == "fixture.personal_echo_tool"
|
||||||
|
assert proxy_tools[0]["connection_id"] == "fixture.personal"
|
||||||
|
assert proxy_tools[0]["local_name"] == "echo_tool"
|
||||||
|
assert proxy_tools[0]["enabled"] is True
|
||||||
|
|
||||||
asyncio.run(run_proxy())
|
asyncio.run(run_proxy())
|
||||||
|
|
||||||
@@ -178,6 +194,7 @@ def test_transparent_proxy_can_collapse_upstream_tools_behind_search() -> None:
|
|||||||
assert "search_tools" in names
|
assert "search_tools" in names
|
||||||
assert "wf.mcp_list_connections" in names
|
assert "wf.mcp_list_connections" in names
|
||||||
assert "wf.mcp_get_connection_statuses" in names
|
assert "wf.mcp_get_connection_statuses" in names
|
||||||
|
assert "wf.mcp_list_proxy_tools" in names
|
||||||
assert "fixture.personal_echo_tool" not in names
|
assert "fixture.personal_echo_tool" not in names
|
||||||
|
|
||||||
search_result = await client.call_tool(
|
search_result = await client.call_tool(
|
||||||
@@ -228,7 +245,7 @@ def test_transparent_proxy_admin_tools_mutate_config_file() -> None:
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
assert add_result.structured_content == {
|
assert _structured(add_result) == {
|
||||||
"action": "add_connection",
|
"action": "add_connection",
|
||||||
"connection_id": "fixture.work",
|
"connection_id": "fixture.work",
|
||||||
"ok": True,
|
"ok": True,
|
||||||
@@ -239,7 +256,7 @@ def test_transparent_proxy_admin_tools_mutate_config_file() -> None:
|
|||||||
"wf.mcp_disable_connection",
|
"wf.mcp_disable_connection",
|
||||||
{"connection_id": "fixture.work"},
|
{"connection_id": "fixture.work"},
|
||||||
)
|
)
|
||||||
assert disable_result.structured_content == {
|
assert _structured(disable_result) == {
|
||||||
"action": "update_connection",
|
"action": "update_connection",
|
||||||
"connection_id": "fixture.work",
|
"connection_id": "fixture.work",
|
||||||
"ok": True,
|
"ok": True,
|
||||||
@@ -257,7 +274,7 @@ def test_transparent_proxy_admin_tools_mutate_config_file() -> None:
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
assert update_result.structured_content == {
|
assert _structured(update_result) == {
|
||||||
"action": "update_connection",
|
"action": "update_connection",
|
||||||
"connection_id": "fixture.work",
|
"connection_id": "fixture.work",
|
||||||
"ok": True,
|
"ok": True,
|
||||||
@@ -265,13 +282,13 @@ def test_transparent_proxy_admin_tools_mutate_config_file() -> None:
|
|||||||
}
|
}
|
||||||
|
|
||||||
config_result = await client.call_tool("wf.mcp_get_config")
|
config_result = await client.call_tool("wf.mcp_get_config")
|
||||||
assert "fixture.work" in str(config_result.structured_content)
|
assert "fixture.work" in str(_structured(config_result))
|
||||||
|
|
||||||
remove_result = await client.call_tool(
|
remove_result = await client.call_tool(
|
||||||
"wf.mcp_remove_connection",
|
"wf.mcp_remove_connection",
|
||||||
{"connection_id": "fixture.work"},
|
{"connection_id": "fixture.work"},
|
||||||
)
|
)
|
||||||
assert remove_result.structured_content == {
|
assert _structured(remove_result) == {
|
||||||
"action": "remove_connection",
|
"action": "remove_connection",
|
||||||
"connection_id": "fixture.work",
|
"connection_id": "fixture.work",
|
||||||
"ok": True,
|
"ok": True,
|
||||||
@@ -327,7 +344,7 @@ def test_transparent_proxy_admin_reload_remounts_connections() -> None:
|
|||||||
assert "fixture.personal_echo_tool" not in before_reload_names
|
assert "fixture.personal_echo_tool" not in before_reload_names
|
||||||
|
|
||||||
reload_result = await client.call_tool("wf.mcp_reload_config")
|
reload_result = await client.call_tool("wf.mcp_reload_config")
|
||||||
assert reload_result.structured_content == {
|
assert _structured(reload_result) == {
|
||||||
"ok": True,
|
"ok": True,
|
||||||
"reloaded": True,
|
"reloaded": True,
|
||||||
"mounted_connections": ["fixture.personal"],
|
"mounted_connections": ["fixture.personal"],
|
||||||
@@ -343,6 +360,6 @@ def test_transparent_proxy_admin_reload_remounts_connections() -> None:
|
|||||||
"fixture.personal_echo_tool",
|
"fixture.personal_echo_tool",
|
||||||
{"text": "reloaded"},
|
{"text": "reloaded"},
|
||||||
)
|
)
|
||||||
assert result.structured_content == {"echoed": "reloaded"}
|
assert _structured(result) == {"echoed": "reloaded"}
|
||||||
|
|
||||||
asyncio.run(run_proxy())
|
asyncio.run(run_proxy())
|
||||||
|
|||||||
Reference in New Issue
Block a user