different-ai/openwork · error · ExternalMcpDiagnosticError
MCP_CATALOG_DUPLICATE_TOOL
MCP_CATALOG_DUPLICATE_TOOL
Error message
MCP_CATALOG_DUPLICATE_TOOL
What it means
Thrown when two tools on the same external MCP server advertise the same `name` across tools/list pages. The gateway requires unique, stable tool names to address capabilities unambiguously (search_capabilities/execute_capability key on the name).
Source
Thrown at ee/apps/den-api/src/capability-sources/external-mcp-client.ts:701
operation: (options) => input.listPage(cursor, options),
})
if (tools.length + result.tools.length > itemLimit) {
throw catalogDiagnosticError({
tracker: input.diagnostic,
code: "MCP_CATALOG_ITEM_LIMIT",
operatorAction: `Reduce the provider catalog below ${itemLimit} tools or use a scoped MCP server.`,
})
}
for (const tool of result.tools) {
catalogBytes += measureCatalogTool({
diagnostic: input.diagnostic,
tool,
remainingBytes: EXTERNAL_MCP_CATALOG_LIMIT_BYTES - catalogBytes,
})
if (seenToolNames.has(tool.name)) {
throw catalogDiagnosticError({
tracker: input.diagnostic,
code: "MCP_CATALOG_DUPLICATE_TOOL",
operatorAction: "Ensure every tools/list page uses a unique, stable tool name.",
})
}
seenToolNames.add(tool.name)
tools.push(tool)
}
if (!result.nextCursor) {
input.diagnostic.passed("MCP_TOOL_DISCOVERY", "catalog_ready")
return tools
}
if (serializedStringBytes(result.nextCursor) > EXTERNAL_MCP_CURSOR_LIMIT_BYTES) {
throw catalogDiagnosticError({
tracker: input.diagnostic,
code: "MCP_CATALOG_CURSOR_SIZE_LIMIT",
operatorAction: `Reduce each serialized tools/list cursor below ${EXTERNAL_MCP_CURSOR_LIMIT_BYTES} UTF-8 bytes.`,
})
}
const cursorMeasurement = measureSerializedJson(View on GitHub (pinned to 2b7df46e8a)
Solutions
- Fix the provider server so each tool has a unique, stable name
- Ensure tools/list pagination does not re-emit tools from earlier pages
- Namespace tools by prefix (e.g. "db.query" vs "docs.query") if multiple modules produce tools
- Use a scoped server exposing a deduplicated subset
Example fix
// before: two tools both named "search" // after: tools named "web_search" and "docs_search"
Defensive patterns
Strategy: validation
Validate before calling
function assertUniqueToolNames(tools: { name: string }[]): void {
const seen = new Set<string>()
for (const t of tools) {
if (seen.has(t.name)) throw new Error(`duplicate tool name: ${t.name}`)
seen.add(t.name)
}
} Type guard
function allToolNamesUnique(tools: { name: string }[]): boolean {
return new Set(tools.map((t) => t.name)).size === tools.length
} Try / catch
try {
await connectExternalMcp(...)
} catch (err) {
if (err instanceof Error && err.message.includes("MCP_CATALOG_DUPLICATE_TOOL")) {
console.error("Provider returned duplicate tool names across pages; dedupe at the server")
} else throw err
} Prevention
- Guarantee stable, unique tool names in your server implementation
- Verify pagination does not re-emit earlier tools
- Namespace tools when generating them from multiple modules
- Add a dedupe assertion test to your MCP server
When it happens
Trigger: During page collection, seenToolNames already contains tool.name when a new page (or a repeated page) yields a tool with an identical name.
Common situations: Provider bugs where pagination repeats already-returned tools; servers namespacing tools per-user but emitting the same bare name; template-driven servers instantiating the same tool name multiple times.
Related errors
- MCP_CATALOG_TOOL_TITLE_LIMIT
- MCP_CATALOG_TOOL_DESCRIPTION_LIMIT
- MCP_CATALOG_BYTE_LIMIT
- MCP_CATALOG_ITEM_LIMIT
- invalid_mcp_token_payload
AI-assisted analysis of different-ai/openwork@2b7df46e8a (2026-09-01).
Data as JSON: /api/errors/a0954f8dbe2f3829.
Report an issue: GitHub.