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
- Keep the Disposable returned by registerTool and dispose it before re-registering the same name
- Choose a unique tool name (prefix with extension id)
- Unregister the existing tool first if replacement is intended
- 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
- Store and dispose the Disposable returned by registerTool on deactivation
- Namespace tool names with your extension id to avoid collisions
- Never call registerTool twice for the same name without disposing
- Clear all registrations on config reload before re-registering
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
- Tool name is required
- ${ref.error}
- ${e instanceof Error ? e.message : String(e)}
- Resource not found: ${uri}
- Unknown tool: ${name}
AI-assisted analysis of neoclide/coc.nvim@50e974d969 (2026-08-31).
Data as JSON: /api/errors/ebd1090d4f37a0d2.
Report an issue: GitHub.