rohitg00/ai-engineering-from-scratch · error · ValueError

unknown tool

Error message

unknown tool

What it means

The name is a valid non-empty string but self.tools (fixed at server construction) contains no tool with that exact key. The registry is static; the server does not create tools on demand. This is the lookup counterpart of the type check in error 111.

Source

Thrown at certifications/claude/lessons/11-mcp-server-design-and-integration/code/main.py:314

            return self._complete(
                messages=[
                    {
                        "role": "user",
                        "content": {"type": "text", "text": self.prompts[name]},
                    }
                ]
            ), []
        raise LookupError(f"Method not found: {method}")

    def _call_tool(
        self, params: dict[str, Any], metadata: dict[str, Any]
    ) -> tuple[dict[str, Any], list[dict[str, Any]]]:
        name = params["name"]
        if not isinstance(name, str) or not name:
            raise ValueError("name must be a non-empty string")
        tool = self.tools.get(name)
        if tool is None:
            raise ValueError("unknown tool")
        arguments = tool.validate_arguments(params.get("arguments", {}))
        if name == "prepare_review":
            return self._prepare_review(params, metadata, arguments), []

        token = metadata.get("progressToken")
        notifications: list[dict[str, Any]] = []
        if token is not None:
            if not isinstance(token, (str, int)) or isinstance(token, bool):
                raise ValueError("progressToken must be a string or integer")
            notifications = [
                self._progress(token, 0, 1, "starting"),
                self._progress(token, 1, 1, "complete"),
            ]
        value = tool.handler(arguments)
        return self._complete(
            content=[{"type": "text", "text": json.dumps(value)}], isError=False
        ), notifications

View on GitHub (pinned to 39ea8a1c6d)

Solutions

  1. Call tools/list first and use a name from the returned list verbatim
  2. Re-fetch tools/list after any server upgrade instead of caching tool names
  3. Check exact casing, underscores, and hyphens in the name

Example fix

# before
params = {"name": "PrepareReview"}

# after
names = {t["name"] for t in server.exchange("tools/list", {})["tools"]}
assert "prepare_review" in names
params = {"name": "prepare_review"}
Defensive patterns

Strategy: validation

Validate before calling

tools = server.exchange("tools/list", {})["tools"]
known = {t["name"] for t in tools}
if name not in known:
    raise ValueError(f"tool {name!r} not advertised by this server")
server.exchange("tools/call", {"name": name, "arguments": args})

Type guard

def is_known_tool(server, name: str) -> bool:
    tools = server.exchange("tools/list", {})["tools"]
    return name in {t["name"] for t in tools}

Try / catch

try:
    result = server.exchange("tools/call", params)
except ValueError as exc:
    if "unknown tool" in str(exc):
        refresh_tool_cache()
    raise

Prevention

When it happens

Trigger: tools/call with a name that is not registered: a stale name from an older server version, wrong casing ('Prepare_Review'), or a tool belonging to a different MCP server.

Common situations: Server upgraded and a tool renamed or removed; client caches the tools/list result across restarts; copy-pasting a tool name from documentation for another server.

Related errors


AI-assisted analysis of rohitg00/ai-engineering-from-scratch@39ea8a1c6d (2026-08-26). Data as JSON: /api/errors/50331245312cdc7c. Report an issue: GitHub.