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

  1. Fix the provider server so each tool has a unique, stable name
  2. Ensure tools/list pagination does not re-emit tools from earlier pages
  3. Namespace tools by prefix (e.g. "db.query" vs "docs.query") if multiple modules produce tools
  4. 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

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


AI-assisted analysis of different-ai/openwork@2b7df46e8a (2026-09-01). Data as JSON: /api/errors/a0954f8dbe2f3829. Report an issue: GitHub.