windmill-labs/windmill · error · ValueError
duplicate MCP tool name(s): {', '.join(duplicates)}
Error message
duplicate MCP tool name(s): {', '.join(duplicates)} What it means
backend/generate_mcp_endpoints_tools/generate_mcp_tools.py's find_mcp_tools collects MCP tool names derived from spec endpoints via x-mcp-tool-name. Tool names must be unique because a token's mcp:endpoints:<name> resolves by first match — a duplicate would silently route to whichever tool is generated first, so the generator raises ValueError listing the duplicated names.
Source
Thrown at backend/generate_mcp_endpoints_tools/generate_mcp_tools.py:470
'path': path,
'method': method.upper(),
'parameters': operation.get('parameters', []),
'requestBody': operation.get('requestBody'),
'required_fields': operation.get('x-mcp-required-fields', []),
'include_fields': operation.get('x-mcp-tool-include-fields'),
'opaque_fields': operation.get('x-mcp-tool-opaque-fields'),
'include_query_params': operation.get('x-mcp-tool-include-query-params'),
}
tools.append(tool)
# A tool name used to be an operationId, which OpenAPI already keeps unique.
# `x-mcp-tool-name` gives that up, and a duplicate would be silent: a token's
# `mcp:endpoints:<name>` resolves by first match, so which endpoint it reaches
# would depend on the order of this file.
names = [t['name'] for t in tools]
duplicates = sorted({n for n in names if names.count(n) > 1})
if duplicates:
raise ValueError(f"duplicate MCP tool name(s): {', '.join(duplicates)}")
return tools
def generate_typescript_code(tools: List[Dict[str, Any]], spec: Dict[str, Any], base_path: str = "") -> str:
"""Generate TypeScript code with MCP endpoint tools.
This catalogue only feeds the frontend's MCP scope picker, which reads names and
methods.
"""
if not tools:
return """// Auto-generated MCP tools from OpenAPI specification
// This file is generated by generate_mcp_tools.py - DO NOT EDIT MANUALLY
export interface EndpointTool {
name: string;
description: string;
instructions: string;
path: string;View on GitHub (pinned to e474e8803c)
Solutions
- Identify the duplicated names from the error message and grep the spec for that x-mcp-tool-name
- Rename one endpoint's x-mcp-tool-name so every tool name is unique
- If the name is derived rather than explicit, adjust the path/operation so generated names differ
- Regenerate the MCP tools (run the generator's main) and confirm no ValueError
Example fix
# before (spec) x-mcp-tool-name: get-job # on two different endpoints # after x-mcp-tool-name: get-job # first endpoint x-mcp-tool-name: get-job-result # second endpoint
Defensive patterns
Strategy: validation
Validate before calling
# validate tool-name uniqueness in the spec before running the generator
import yaml, collections
spec = yaml.safe_load(open('spec.yaml'))
names = [op.get('x-mcp-tool-name') for op in spec['paths'].values() if op.get('x-mcp-tool-name')]
dups = [n for n, c in collections.Counter(names).items() if c > 1]
assert not dups, f"duplicate x-mcp-tool-name: {dups}" Type guard
def has_unique_tool_names(tools: list[dict]) -> bool:
names = [t['name'] for t in tools]
return len(names) == len(set(names)) Try / catch
try:
tools = find_mcp_tools(spec)
except ValueError as e:
if str(e).startswith('duplicate MCP tool name'):
sys.exit(f"Fix the spec: {e}")
raise Prevention
- Grep the spec for x-mcp-tool-name before adding new endpoints
- Adopt a naming convention encoding the operation/resource in the tool name
- Run the generator in CI so collisions fail fast
- Review spec diff for copy-pasted endpoint blocks
When it happens
Trigger: Two or more endpoints in the OpenAPI spec declare the same x-mcp-tool-name (or derive the same name), so the tools list contains repeated 'name' entries and the sorted-duplicates check trips.
Common situations: Copy-pasting an endpoint block in the spec and forgetting to change x-mcp-tool-name; adding a v2 endpoint alongside v1 with the same tool name; renaming an operation but leaving a stale x-mcp-tool-name collision with another route.
Related errors
- could not read the run of job ${id}: ${res.status} ${await r
- could not record run ${experiment_id}: ${res.status} ${await
- file path escapes the build directory: ${rel}
- ${what} failed:\n${output}
- The tool list for ${path} changed while it was loading. Try
AI-assisted analysis of windmill-labs/windmill@e474e8803c (2026-09-03).
Data as JSON: /api/errors/4fb012c2255c71e6.
Report an issue: GitHub.