fix(e2e): grant the toolset by the tool's own name, not the wire name

tools/list serves a tool as <prefix><tool_name>, but a toolset grants by the
tool's own name: resolve_toolset_permissions reads toolset.tools[].tool_name
straight through, and the prefix is added on the way out. The test built the
toolset from the names tools/list reported, so the grant matched nothing, the
scoped key listed no tools, and await_tools ran out its whole poll_timeout
before failing

Measure the prefix off search_datadog_logs, whose own name is known, rather than
guessing it from the alias, since the proxy can be configured to prefix with a
short server id instead. The expectation compared against tools/list stays in
wire names; only what the toolset stores crosses back
This commit is contained in:
Yuneng Jiang 2026-09-06 14:04:27 +00:00
parent a90989d43e
commit db5d6e0d37
No known key found for this signature in database

View file

@ -42,6 +42,22 @@ def _key(
return key
def _wire_prefix(wire_name: str, tool_name: str, catalog: frozenset[str]) -> str:
"""The prefix tools/list puts in front of one server's tool names, measured off a
tool whose own name is known rather than guessed from the alias. A toolset grants
by the tool's own name, never the wire name, and the prefix is whatever the proxy
is configured to build (the alias, or a short server id), so measuring it is the
only way to cross between the two."""
assert wire_name.endswith(tool_name), f"tools/list served {wire_name!r}, expected it to end with {tool_name!r}"
prefix: Final = wire_name[: len(wire_name) - len(tool_name)]
unprefixed: Final = frozenset(name for name in catalog if not name.startswith(prefix))
assert not unprefixed, (
f"every tool of one server shares the wire prefix {prefix!r}, so {sorted(unprefixed)} "
f"cannot be reduced to the names a toolset grants by"
)
return prefix
class TestMcpToolsetEnforcement:
@pytest.mark.covers("mcp.list_tools.api_key.toolset_scoped")
def test_key_granted_a_toolset_lists_exactly_its_tools(self, client: McpClient, resources: ResourceManager) -> None:
@ -49,13 +65,15 @@ class TestMcpToolsetEnforcement:
client.await_registered(server_id)
catalog_key: Final = _key(client, resources, "catalog", server_id=server_id)
_ = client.await_tool(catalog_key, server_id, SEARCH_LOGS_TOOL)
known_wire: Final = client.await_tool(catalog_key, server_id, SEARCH_LOGS_TOOL)
catalog: Final = unwrap(client.list_tools(catalog_key)).tool_names_for_server(server_id)
assert len(catalog) > 2, (
f"the Datadog core toolset must serve more tools than the toolset names, or the "
f"restriction has nothing to hide; got {sorted(catalog)}"
)
chosen: Final = frozenset(sorted(catalog)[:2])
prefix: Final = _wire_prefix(known_wire, SEARCH_LOGS_TOOL, catalog)
chosen_wire: Final = frozenset(sorted(catalog)[:2])
chosen: Final = frozenset(name.removeprefix(prefix) for name in chosen_wire)
toolset: Final = client.proxy.create_toolset(
ToolsetCreateBody(
@ -70,8 +88,8 @@ class TestMcpToolsetEnforcement:
)
scoped_key: Final = _key(client, resources, "toolset", server_id=server_id, toolset_id=toolset.toolset_id)
listed: Final = client.await_tools(scoped_key, server_id, expected=chosen)
assert listed == chosen, (
listed: Final = client.await_tools(scoped_key, server_id, expected=chosen_wire)
assert listed == chosen_wire, (
f"a key granted the toolset must list exactly its two tools; "
f"got {sorted(listed)}, expected {sorted(chosen)}"
f"got {sorted(listed)}, expected {sorted(chosen_wire)}"
)