fix(mcp): normalize tools for Moonshot API - #2539
Conversation
|
Thanks for addressing #2531. I checked out Validation that passed
The reported One routing collision remains
server = "magic"
invalid = "21st.magic component builder"
alias = _safe_mcp_tool_name(server, invalid)
# alias == "m_21st_magic_component_builder_25ae5ad3"
_safe_mcp_tool_name(server, alias) == aliasConstructing both
self._tool_dict[tool.name] = toolso registration silently overwrites the first tool: The model-visible call can therefore be routed to a different original MCP tool depending on load order. Suggested fixResolve aliases with visibility over the complete tool set rather than independently, or at minimum detect duplicate runtime names during MCP registration and fail with an explicit diagnostic instead of silently replacing an existing tool. A regression test pairing an invalid name with a valid name equal to its generated alias should cover the case. This does not invalidate the schema normalization or the ordinary #2531 examples, but I think the silent routing ambiguity is worth closing before relying on the alias map as bidirectional. |
|
Here is the compact regression test corresponding to the collision report. It can be copied into from types import SimpleNamespace
from typing import Any, cast
import pytest
from mcp.types import Tool
from kimi_cli.soul.toolset import KimiToolset, MCPTool, _safe_mcp_tool_name
def test_mcp_alias_collision_is_rejected_instead_of_silent_overwrite() -> None:
server_name = "magic"
invalid_name = "21st.magic component builder"
generated_alias = _safe_mcp_tool_name(server_name, invalid_name)
# A different MCP tool is allowed to use that generated alias as its
# original, already-compatible name.
assert _safe_mcp_tool_name(server_name, generated_alias) == generated_alias
runtime = SimpleNamespace(
config=SimpleNamespace(
mcp=SimpleNamespace(client=SimpleNamespace(tool_call_timeout_ms=60_000))
)
)
client = cast(Any, object())
invalid_tool = MCPTool(
server_name,
Tool(name=invalid_name, description="invalid original", inputSchema={"type": "object"}),
client,
runtime=cast(Any, runtime),
)
valid_tool = MCPTool(
server_name,
Tool(
name=generated_alias,
description="valid original",
inputSchema={"type": "object"},
),
client,
runtime=cast(Any, runtime),
)
assert invalid_tool.name == valid_tool.name
toolset = KimiToolset()
toolset.add(invalid_tool)
# Minimal safe policy: reject the duplicate explicitly. An implementation
# that allocates a second unique runtime alias can use an equivalent
# assertion that both original tools remain registered and routable.
with pytest.raises(ValueError, match="duplicate|conflict|already registered"):
toolset.add(valid_tool)Run with: uv run pytest tests/core/test_mcp_alias_collision_regression.py -qAgainst commit This version encodes the minimum fail-safe policy: duplicate runtime names must be rejected rather than silently overwrite an existing MCP route. If the implementation instead allocates a second unique alias, the final assertion can be changed to require both original tools to remain registered and independently routable. |
Community PRs (ported with kimi_cli->cran_code adaptation): - MoonshotAI#2507 ACP AskUserQuestion signals QuestionNotSupported (no phantom dismissal) - MoonshotAI#2520 fork/undo context truncation aligns to wire turns by text matching - MoonshotAI#2572 double-encoded tool args; MoonshotAI#2530 shell pipe hang - MoonshotAI#2592 media results degrade gracefully when model lacks image_in - MoonshotAI#2539 MCP tool-name normalization with collision guard; MoonshotAI#2535 cache-key scoping kimi-code Tier-1 (compaction quality): - compact.md rewritten as first-person handoff note - summary prefix teaches 'notes, not proof — verify before relying' - post-compaction token estimate adds system-prompt + tool-schema overhead
Summary
objecttype when an MCP schema defines object propertiesanyOf/required schema shape reported in the issue into equivalent Moonshot-compatible branchesValidation
uv run pytest packages/kosong/tests/test_kimi_tool_conversion.py tests/core/test_mcp_tool_names.py -q(9 passed)Closes #2531