Skip to content

fix(mcp): normalize tools for Moonshot API - #2539

Open
lihailong00 wants to merge 2 commits into
MoonshotAI:mainfrom
lihailong00:codex/issue-2531-mcp-schema
Open

fix(mcp): normalize tools for Moonshot API#2539
lihailong00 wants to merge 2 commits into
MoonshotAI:mainfrom
lihailong00:codex/issue-2531-mcp-schema

Conversation

@lihailong00

@lihailong00 lihailong00 commented Jul 23, 2026

Copy link
Copy Markdown

Summary

  • generate stable Moonshot-compatible aliases for MCP tool names while retaining original names for upstream call routing
  • add a missing root object type when an MCP schema defines object properties
  • distribute the exact object anyOf/required schema shape reported in the issue into equivalent Moonshot-compatible branches
  • leave unsupported complex combinator schemas unchanged

Validation

  • uv run pytest packages/kosong/tests/test_kimi_tool_conversion.py tests/core/test_mcp_tool_names.py -q (9 passed)
  • targeted Ruff check and format check passed
  • targeted Pyright check passed with 0 errors
  • remote compare verified: 1 commit, 4 intended files, 154 additions / 2 deletions

Closes #2531


Open in Devin Review

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Devin Review: No Issues Found

Devin Review analyzed this PR and found no potential bugs to report.

View in Devin Review to see 1 additional finding.

Open in Devin Review

@zycxfyh

zycxfyh commented Jul 27, 2026

Copy link
Copy Markdown

Thanks for addressing #2531. I checked out 0861fd3 and independently validated the patch.

Validation that passed

  • uv run pytest packages/kosong/tests/test_kimi_tool_conversion.py tests/core/test_mcp_tool_names.py -q9 passed
  • targeted Ruff → passed
  • targeted Pyright → 0 errors
  • 98,980 generated invalid (server_name, tool_name) pairs → 0 collisions among generated aliases
  • the anyOf transformation preserved shared required, title, description, properties, and additionalProperties in supplemental checks

The reported 21st_* and simple object-anyOf cases look correctly handled.

One routing collision remains

_safe_mcp_tool_name() cannot guarantee uniqueness against an already-valid upstream name, because compatible names are returned unchanged. A server (or another loaded server) can expose a valid tool whose original name is exactly the alias generated for an invalid tool:

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) == alias

Constructing both MCPTools gives the same runtime name:

runtime_a m_21st_magic_component_builder_25ae5ad3
runtime_b m_21st_magic_component_builder_25ae5ad3
same_runtime_name True

KimiToolset.add() currently performs:

self._tool_dict[tool.name] = tool

so registration silently overwrites the first tool:

registered_count 1
winner_upstream m_21st_magic_component_builder_25ae5ad3

The model-visible call can therefore be routed to a different original MCP tool depending on load order.

Suggested fix

Resolve 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.

@zycxfyh

zycxfyh commented Jul 27, 2026

Copy link
Copy Markdown

Here is the compact regression test corresponding to the collision report. It can be copied into tests/core/test_mcp_alias_collision_regression.py:

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 -q

Against commit 0861fd3, it currently fails at the intended single point:

Failed: DID NOT RAISE <class 'ValueError'>

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.

NLPark-Cran added a commit to NLPark-Cran/cran-code that referenced this pull request Aug 18, 2026
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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

MCP tool names & schemas rejected by Moonshot API (HTTP 400) — sanitize client-side before sending

2 participants