neoclide/coc.nvim · error

Tool ${tool.name} already registered

Error message

Tool ${tool.name} already registered

What it means

`ToolRegistry.register` keeps a map keyed by tool name and refuses to overwrite an existing entry. Registering a tool whose name is already taken throws, forcing callers to unregister first or pick a unique name — this keeps `tools/list` unambiguous.

Source

Thrown at src/mcp/tools/index.ts:79

  public readonly onDidChange: Event<void> = this._onDidChange.event

  /**
   * Restrict exposed tools to a whitelist of names. `null` allows every
   * registered tool; an empty set exposes none. Tools outside the whitelist
   * are hidden from `tools/list` and rejected by `has`/`get`/`call`.
   */
  public setAllowedTools(names: string[] | null): void {
    this.allowed = names ? new Set(names) : null
    this._onDidChange.fire()
  }

  public isAllowed(name: string): boolean {
    return this.allowed === null || this.allowed.has(name)
  }

  public register(tool: McpTool): Disposable {
    if (this.tools.has(tool.name)) {
      throw new Error(`Tool ${tool.name} already registered`)
    }
    this.tools.set(tool.name, tool)
    this._onDidChange.fire()
    return Disposable.create(() => {
      this.unregister(tool.name)
    })
  }

  public unregister(name: string): void {
    if (this.tools.delete(name)) {
      this._onDidChange.fire()
    }
  }

  public get(name: string): McpTool | undefined {
    let tool = this.tools.get(name)
    return tool && this.isAllowed(name) ? tool : undefined
  }

View on GitHub (pinned to 50e974d969)

Solutions

  1. Keep the Disposable returned by registerTool and dispose it before re-registering the same name
  2. Choose a unique tool name (prefix with extension id)
  3. Unregister the existing tool first if replacement is intended
  4. Guard with registry `get(name)` / existence check before registering

Example fix

// before
server.registerTool({ name: 'search', ... })
server.registerTool({ name: 'search', ... }) // throws
// after
const d = server.registerTool({ name: 'search', ... })
d.dispose() // before re-registering
server.registerTool({ name: 'search', ... })
Defensive patterns

Strategy: try-catch

Validate before calling

function registerUnique(server: McpServer, tool: McpTool): Disposable {
  const disposables: Disposable[] = []
  if (registeredNames.has(tool.name)) registeredNames.get(tool.name)!.dispose()
  const d = server.registerTool(tool)
  registeredNames.set(tool.name, d)
  return d
}

Try / catch

let d: Disposable
try {
  d = server.registerTool(tool)
} catch (e) {
  if (/already registered/.test(e.message)) {
    logger.warn(`Tool '${tool.name}' already registered — skipping duplicate`)
    return
  }
  throw e
}

Prevention

When it happens

Trigger: Calling `registerTool` twice with tools sharing the same `name`; re-initializing/reloading the MCP server (initialize path) without disposing previously registered tools; two extensions registering tools with colliding names.

Common situations: Hot-reload or re-registration on config change where the old Disposable wasn't kept/disposed; duplicate tool definitions in configuration; name collision between built-in tools (workspace search etc.) and extension tools.

Related errors


AI-assisted analysis of neoclide/coc.nvim@50e974d969 (2026-08-31). Data as JSON: /api/errors/ebd1090d4f37a0d2. Report an issue: GitHub.