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
+3 -3
View File
@@ -29,7 +29,7 @@ wf-mcp serve --mode unified
### Working ### Working
- `tools/list` shows Everything tools through `wf-mcp`. - `tools/list` shows Everything tools through `wf-mcp`.
- `tools/call` works for normal tools such as `everything.default_echo`. - `tools/call` works for normal tools such as `everything.default.echo`.
- Tool metadata is preserved for proxy inventory, including title, description, - Tool metadata is preserved for proxy inventory, including title, description,
and JSON input schema. and JSON input schema.
- Annotated text content is preserved. - Annotated text content is preserved.
@@ -47,7 +47,7 @@ Example listed resource mapping:
```text ```text
upstream: demo://resource/static/document/instructions.md upstream: demo://resource/static/document/instructions.md
proxied: demo://everything.default/resource/static/document/instructions.md proxied: demo://everything/default/resource/static/document/instructions.md
``` ```
### Gaps ### Gaps
@@ -63,7 +63,7 @@ demo://resource/dynamic/text/2
normal dynamic resources: normal dynamic resources:
```text ```text
demo://everything.default/resource/dynamic/text/2 demo://everything/default/resource/dynamic/text/2
``` ```
- Session resource links from `gzip-file-as-resource` are not currently usable - Session resource links from `gzip-file-as-resource` are not currently usable
@@ -1,6 +1,5 @@
from __future__ import annotations from __future__ import annotations
import re
from collections.abc import Callable, Sequence from collections.abc import Callable, Sequence
from typing import Any from typing import Any
@@ -11,8 +10,7 @@ from pydantic import ConfigDict
from pydantic.json_schema import SkipJsonSchema from pydantic.json_schema import SkipJsonSchema
from .resource_links import rewrite_resource_link_content from .resource_links import rewrite_resource_link_content
from ..shared.names import connection_id_to_resource_path
_URI_PATTERN = re.compile(r"^([^:]+://)(.*?)$")
class ResourceLinkRewritingTool(Tool): class ResourceLinkRewritingTool(Tool):
@@ -55,7 +53,7 @@ class ResourceLinkNamespace(Transform):
"""Rewrite resource links returned by tools into one namespace.""" """Rewrite resource links returned by tools into one namespace."""
def __init__(self, prefix: str) -> None: def __init__(self, prefix: str) -> None:
self._prefix = prefix self._prefix = connection_id_to_resource_path(prefix)
def __repr__(self) -> str: def __repr__(self) -> str:
return f"ResourceLinkNamespace({self._prefix!r})" return f"ResourceLinkNamespace({self._prefix!r})"
@@ -80,8 +78,7 @@ class ResourceLinkNamespace(Transform):
def _transform_uri(self, uri: str) -> str: def _transform_uri(self, uri: str) -> str:
"""Match FastMCP Namespace URI projection for tool-returned links.""" """Match FastMCP Namespace URI projection for tool-returned links."""
match = _URI_PATTERN.match(uri) protocol, separator, path = uri.partition("://")
if match is None: if not separator:
return uri 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 continue
if connection_id in RESERVED_CONNECTION_IDS: if connection_id in RESERVED_CONNECTION_IDS:
errors.append(f"connection id {connection_id!r} is reserved by wf-mcp") 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): if not _NAMESPACE_ID_RE.fullmatch(connection_id):
errors.append( errors.append(
f"connection id {connection_id!r} must contain only letters, " 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 ( from .names import (
ADMIN_NAMESPACE, ADMIN_NAMESPACE,
LdaNamespace, LdaNamespace,
ProxyNamespace,
ProxyToolName, ProxyToolName,
connection_id_to_resource_path,
is_admin_tool_name, is_admin_tool_name,
namespaced_tool_name, namespaced_tool_name,
parse_namespaced_tool_name, parse_namespaced_tool_name,
@@ -12,7 +14,9 @@ from .pagination import clamp_limit, make_cursor, paginate_items, parse_cursor
__all__ = [ __all__ = [
"ADMIN_NAMESPACE", "ADMIN_NAMESPACE",
"LdaNamespace", "LdaNamespace",
"ProxyNamespace",
"ProxyToolName", "ProxyToolName",
"connection_id_to_resource_path",
"clamp_limit", "clamp_limit",
"error_payload", "error_payload",
"is_admin_tool_name", "is_admin_tool_name",
+147 -3
View File
@@ -2,7 +2,25 @@ from __future__ import annotations
from dataclasses import dataclass 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" ADMIN_NAMESPACE = "wf.admin"
RESERVED_CONNECTION_IDS = frozenset({ADMIN_NAMESPACE, "wf.mcp"}) 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: 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( def parse_namespaced_tool_name(
@@ -27,7 +45,7 @@ def parse_namespaced_tool_name(
matches = [ matches = [
connection_id connection_id
for connection_id in connection_ids for connection_id in connection_ids
if proxy_name.startswith(f"{connection_id}_") if proxy_name.startswith(f"{connection_id}.")
] ]
if not matches: if not matches:
return None return None
@@ -54,3 +72,129 @@ class LdaNamespace(Namespace):
# FastMCP's public Namespace transform uses underscores; override its # FastMCP's public Namespace transform uses underscores; override its
# private prefix so admin tools keep their dotted wf.admin.* names. # private prefix so admin tools keep their dotted wf.admin.* names.
self._name_prefix = f"{prefix}." 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 import Client
from fastmcp.client.transports.config import MCPConfigTransport from fastmcp.client.transports.config import MCPConfigTransport
from fastmcp.server import create_proxy from fastmcp.server import create_proxy
from fastmcp.server.transforms import Namespace
from ..models import BrokerConfig, ConnectionConfig from ..models import BrokerConfig, ConnectionConfig
from ..proxy_results import ResourceLinkNamespace from ..proxy_results import ResourceLinkNamespace
from ..proxy_config import broker_config_to_fastmcp_config from ..proxy_config import broker_config_to_fastmcp_config
from ..shared.names import ProxyNamespace
ProxyT = TypeVar("ProxyT") ProxyT = TypeVar("ProxyT")
ProxyMountFactory = Callable[[ConnectionConfig, Path], "ProxyMount[ProxyT]"] ProxyMountFactory = Callable[[ConnectionConfig, Path], "ProxyMount[ProxyT]"]
@@ -98,7 +97,7 @@ def create_proxy_mount(
transport = MCPConfigTransport(server_config, name_as_prefix=False) transport = MCPConfigTransport(server_config, name_as_prefix=False)
client = Client(transport=transport, name=f"wf-mcp:{connection.id}") client = Client(transport=transport, name=f"wf-mcp:{connection.id}")
proxy: FastMCP[Any] = create_proxy(client, name=f"Proxy-{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)) proxy.add_transform(ResourceLinkNamespace(connection.id))
return ProxyMount( return ProxyMount(
connection_id=connection.id, connection_id=connection.id,
+3 -3
View File
@@ -17,15 +17,15 @@ def test_namespaced_tool_names_are_reversible_with_known_connections() -> None:
) )
assert parsed is not None assert parsed is not None
assert parsed.proxy_name == "everything.default_get-sum" assert parsed.proxy_name == "everything.default.get-sum"
assert parsed.connection_id == "everything.default" assert parsed.connection_id == "everything.default"
assert parsed.local_name == "get-sum" assert parsed.local_name == "get-sum"
def test_namespaced_tool_parser_rejects_unknown_and_admin_names() -> None: def test_namespaced_tool_parser_rejects_unknown_and_admin_names() -> None:
assert parse_namespaced_tool_name("missing_echo", {"everything.default"}) is None assert parse_namespaced_tool_name("missing.echo", {"everything.default"}) is None
assert is_admin_tool_name("wf.admin.list_connections") is True assert is_admin_tool_name("wf.admin.list_connections") is True
assert is_admin_tool_name("everything.default_echo") is False assert is_admin_tool_name("everything.default.echo") is False
def test_admin_namespace_is_distinct_from_wf_mcp_runtime_source() -> None: def test_admin_namespace_is_distinct_from_wf_mcp_runtime_source() -> None:
+19 -20
View File
@@ -54,7 +54,7 @@ def test_transparent_proxy_lists_and_calls_upstream_tools() -> None:
assert "wf.admin.get_connection_statuses" in names assert "wf.admin.get_connection_statuses" in names
assert "wf.admin.list_proxy_tools" in names assert "wf.admin.list_proxy_tools" in names
assert "wf.admin.get_proxy_tool" in names assert "wf.admin.get_proxy_tool" in names
assert "fixture.personal_echo_tool" in names assert "fixture.personal.echo_tool" in names
connections_result = await client.call_tool("wf.admin.list_connections") connections_result = await client.call_tool("wf.admin.list_connections")
assert _structured(connections_result) == { assert _structured(connections_result) == {
@@ -74,7 +74,7 @@ def test_transparent_proxy_lists_and_calls_upstream_tools() -> None:
} }
result = await client.call_tool( result = await client.call_tool(
"fixture.personal_echo_tool", "fixture.personal.echo_tool",
{"text": "hello"}, {"text": "hello"},
) )
assert _structured(result) == {"echoed": "hello"} assert _structured(result) == {"echoed": "hello"}
@@ -85,21 +85,21 @@ def test_transparent_proxy_lists_and_calls_upstream_tools() -> None:
assert proxy_tools_payload["nextCursor"] is None assert proxy_tools_payload["nextCursor"] is None
assert proxy_tools_payload["total"] == 2 assert proxy_tools_payload["total"] == 2
assert len(proxy_tools) == 2 assert len(proxy_tools) == 2
assert proxy_tools[0]["proxy_name"] == "fixture.personal_echo_tool" assert proxy_tools[0]["proxy_name"] == "fixture.personal.echo_tool"
assert proxy_tools[0]["connection_id"] == "fixture.personal" assert proxy_tools[0]["connection_id"] == "fixture.personal"
assert proxy_tools[0]["local_name"] == "echo_tool" assert proxy_tools[0]["local_name"] == "echo_tool"
assert proxy_tools[0]["enabled"] is True assert proxy_tools[0]["enabled"] is True
assert ( assert (
proxy_tools[1]["proxy_name"] proxy_tools[1]["proxy_name"]
== "fixture.personal_resource_link_tool" == "fixture.personal.resource_link_tool"
) )
proxy_tool_result = await client.call_tool( proxy_tool_result = await client.call_tool(
"wf.admin.get_proxy_tool", "wf.admin.get_proxy_tool",
{"proxy_name": "fixture.personal_echo_tool"}, {"proxy_name": "fixture.personal.echo_tool"},
) )
proxy_tool = _structured(proxy_tool_result) proxy_tool = _structured(proxy_tool_result)
assert proxy_tool["proxy_name"] == "fixture.personal_echo_tool" assert proxy_tool["proxy_name"] == "fixture.personal.echo_tool"
assert proxy_tool["connection_id"] == "fixture.personal" assert proxy_tool["connection_id"] == "fixture.personal"
assert proxy_tool["local_name"] == "echo_tool" assert proxy_tool["local_name"] == "echo_tool"
assert proxy_tool["input_schema"]["properties"]["text"]["type"] == "string" assert proxy_tool["input_schema"]["properties"]["text"]["type"] == "string"
@@ -127,10 +127,10 @@ def test_transparent_proxy_rewrites_resource_links_returned_by_tools() -> None:
async def run_proxy() -> None: async def run_proxy() -> None:
client = create_transparent_proxy_client(config) client = create_transparent_proxy_client(config)
async with client: async with client:
result = await client.call_tool("fixture.personal_resource_link_tool") result = await client.call_tool("fixture.personal.resource_link_tool")
link = result.content[0] link = result.content[0]
assert isinstance(link, mcp_types.ResourceLink) assert isinstance(link, mcp_types.ResourceLink)
assert str(link.uri) == "fixture://fixture.personal/docs/welcome" assert str(link.uri) == "fixture://fixture/personal/docs/welcome"
contents = await client.read_resource(str(link.uri)) contents = await client.read_resource(str(link.uri))
assert isinstance(contents[0], mcp_types.TextResourceContents) assert isinstance(contents[0], mcp_types.TextResourceContents)
@@ -189,7 +189,6 @@ def test_transparent_proxy_rejects_invalid_connection_config() -> None:
assert "duplicate connection id 'fixture.personal'" in message assert "duplicate connection id 'fixture.personal'" in message
assert "fixture.personal: stdio transport requires metadata.command" in message assert "fixture.personal: stdio transport requires metadata.command" in message
assert "fixture.personal: unsupported MCP transport 'websocket'" in message assert "fixture.personal: unsupported MCP transport 'websocket'" in message
assert "connection id 'bad_scope.personal' must not contain '_'" in message
assert "fixture.http: http transport requires metadata.url" in message assert "fixture.http: http transport requires metadata.url" in message
assert "connection id 'wf.mcp' is reserved by wf-mcp" in message assert "connection id 'wf.mcp' is reserved by wf-mcp" in message
assert "connection id 'wf.admin' is reserved by wf-mcp" in message assert "connection id 'wf.admin' is reserved by wf-mcp" in message
@@ -255,13 +254,13 @@ def test_transparent_proxy_can_collapse_upstream_tools_behind_search() -> None:
assert "wf.admin.list_connections" in names assert "wf.admin.list_connections" in names
assert "wf.admin.get_connection_statuses" in names assert "wf.admin.get_connection_statuses" in names
assert "wf.admin.list_proxy_tools" in names assert "wf.admin.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(
"search_tools", "search_tools",
{"query": "echo text back"}, {"query": "echo text back"},
) )
assert "fixture.personal_echo_tool" in str(search_result) assert "fixture.personal.echo_tool" in str(search_result)
asyncio.run(run_proxy()) asyncio.run(run_proxy())
@@ -327,7 +326,7 @@ def test_transparent_proxy_proxy_tool_listing_supports_filters_and_cursor() -> N
filtered = _structured(filtered_result) filtered = _structured(filtered_result)
assert filtered["nextCursor"] is None assert filtered["nextCursor"] is None
assert filtered["total"] == 1 assert filtered["total"] == 1
assert filtered["tools"][0]["proxy_name"] == "fixture.personal_echo_tool" assert filtered["tools"][0]["proxy_name"] == "fixture.personal.echo_tool"
asyncio.run(run_proxy()) asyncio.run(run_proxy())
@@ -449,7 +448,7 @@ def test_transparent_proxy_admin_reload_remounts_connections() -> None:
async with client: async with client:
initial_tools = await client.list_tools() initial_tools = await client.list_tools()
initial_names = [tool.name for tool in initial_tools] initial_names = [tool.name for tool in initial_tools]
assert "fixture.personal_echo_tool" not in initial_names assert "fixture.personal.echo_tool" not in initial_names
await client.call_tool( await client.call_tool(
"wf.admin.add_connection", "wf.admin.add_connection",
@@ -467,7 +466,7 @@ def test_transparent_proxy_admin_reload_remounts_connections() -> None:
before_reload_tools = await client.list_tools() before_reload_tools = await client.list_tools()
before_reload_names = [tool.name for tool in before_reload_tools] before_reload_names = [tool.name for tool in before_reload_tools]
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.admin.reload_config") reload_result = await client.call_tool("wf.admin.reload_config")
assert _structured(reload_result) == { assert _structured(reload_result) == {
@@ -480,10 +479,10 @@ def test_transparent_proxy_admin_reload_remounts_connections() -> None:
after_reload_tools = await client.list_tools() after_reload_tools = await client.list_tools()
after_reload_names = [tool.name for tool in after_reload_tools] after_reload_names = [tool.name for tool in after_reload_tools]
assert "fixture.personal_echo_tool" in after_reload_names assert "fixture.personal.echo_tool" in after_reload_names
result = await client.call_tool( result = await client.call_tool(
"fixture.personal_echo_tool", "fixture.personal.echo_tool",
{"text": "reloaded"}, {"text": "reloaded"},
) )
assert _structured(result) == {"echoed": "reloaded"} assert _structured(result) == {"echoed": "reloaded"}
@@ -648,7 +647,7 @@ def test_proxy_reload_result_serializes_and_drives_reload_events() -> None:
def test_proxy_tool_payload_serializes_admin_tool_metadata() -> None: def test_proxy_tool_payload_serializes_admin_tool_metadata() -> None:
payload = ProxyToolPayload( payload = ProxyToolPayload(
proxy_name="fixture.personal_echo_tool", proxy_name="fixture.personal.echo_tool",
connection_id="fixture.personal", connection_id="fixture.personal",
local_name="echo_tool", local_name="echo_tool",
title="Echo Tool", title="Echo Tool",
@@ -660,7 +659,7 @@ def test_proxy_tool_payload_serializes_admin_tool_metadata() -> None:
minimal = payload.to_payload(include_schema=False) minimal = payload.to_payload(include_schema=False)
with_schema = payload.to_payload(include_schema=True) with_schema = payload.to_payload(include_schema=True)
assert minimal["proxy_name"] == "fixture.personal_echo_tool" assert minimal["proxy_name"] == "fixture.personal.echo_tool"
assert minimal["connection_id"] == "fixture.personal" assert minimal["connection_id"] == "fixture.personal"
assert minimal["local_name"] == "echo_tool" assert minimal["local_name"] == "echo_tool"
assert minimal["enabled"] is True assert minimal["enabled"] is True
@@ -671,7 +670,7 @@ def test_proxy_tool_payload_serializes_admin_tool_metadata() -> None:
def test_proxy_tools_page_serializes_paginated_payload() -> None: def test_proxy_tools_page_serializes_paginated_payload() -> None:
tool = ProxyToolPayload( tool = ProxyToolPayload(
proxy_name="fixture.personal_echo_tool", proxy_name="fixture.personal.echo_tool",
connection_id="fixture.personal", connection_id="fixture.personal",
local_name="echo_tool", local_name="echo_tool",
) )
@@ -685,4 +684,4 @@ def test_proxy_tools_page_serializes_paginated_payload() -> None:
assert payload["nextCursor"] == "cursor-1" assert payload["nextCursor"] == "cursor-1"
assert payload["total"] == 3 assert payload["total"] == 3
assert payload["tools"][0]["proxy_name"] == "fixture.personal_echo_tool" assert payload["tools"][0]["proxy_name"] == "fixture.personal.echo_tool"
+3 -3
View File
@@ -38,13 +38,13 @@ def test_unified_server_exposes_upstream_admin_and_workflow_tools() -> None:
async with client: async with client:
tools = await client.list_tools() tools = await client.list_tools()
names = [tool.name for tool in tools] names = [tool.name for tool in tools]
assert "fixture.personal_echo_tool" in names assert "fixture.personal.echo_tool" in names
assert "wf.admin.list_connections" in names assert "wf.admin.list_connections" in names
assert "wf.workflow.list_artifacts" in names assert "wf.workflow.list_artifacts" in names
assert "wf.workflow.run_deployment" in names assert "wf.workflow.run_deployment" in names
echo_result = await client.call_tool( echo_result = await client.call_tool(
"fixture.personal_echo_tool", "fixture.personal.echo_tool",
{"text": "hello"}, {"text": "hello"},
) )
artifacts_result = await client.call_tool("wf.workflow.list_artifacts") artifacts_result = await client.call_tool("wf.workflow.list_artifacts")
@@ -77,7 +77,7 @@ def test_unified_server_can_hide_admin_tools() -> None:
async with client: async with client:
tools = await client.list_tools() tools = await client.list_tools()
names = [tool.name for tool in tools] names = [tool.name for tool in tools]
assert "fixture.personal_echo_tool" in names assert "fixture.personal.echo_tool" in names
assert "wf.workflow.list_artifacts" in names assert "wf.workflow.list_artifacts" in names
assert "wf.admin.list_connections" not in names assert "wf.admin.list_connections" not in names