From 07404c0cf936bae8175e03d52bdf37e2fad63224 Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Mon, 27 Jul 2026 08:49:24 -0400 Subject: [PATCH] fix: keep duplicate indexes on registry fallback and harden the oauth split Duplication blanked mcpServerNames when the registry was unavailable, because filterAuthorizedTools grandfathers the source's tools without resolving them - the copy kept tools it could no longer resolve. Source names now carry forward for the tools that still point at them. splitToolCallName also treated any oauth_mcp_ prefix as a synthetic OAuth call, so a genuine upstream tool by that name resolved to the wrong server. A configured server name now decides when one matches, since a real key always ends in its server, and the prefix only breaks ties for unconfigured servers. --- api/server/controllers/agents/v1.js | 16 ++++++++++++++ packages/data-provider/src/config.ts | 15 +++++++++---- .../data-provider/src/splitMCPToolKey.spec.ts | 22 +++++++++++++++++++ 3 files changed, 49 insertions(+), 4 deletions(-) diff --git a/api/server/controllers/agents/v1.js b/api/server/controllers/agents/v1.js index b464f12f92..52496e3242 100644 --- a/api/server/controllers/agents/v1.js +++ b/api/server/controllers/agents/v1.js @@ -948,6 +948,22 @@ const duplicateAgentHandler = async (req, res) => { configServers, resolvedServerNames, }); + /** When the registry is unavailable, `filterAuthorizedTools` grandfathers the + * source's tools without resolving them, so carry forward the source names those + * retained tools still point at rather than blanking the index. */ + const sourceNames = agent.mcpServerNames ?? []; + if (sourceNames.length > 0) { + const sourceNameSet = new Set(sourceNames); + for (const tool of newAgentData.tools ?? []) { + if (typeof tool !== 'string' || !tool.includes(Constants.mcp_delimiter)) { + continue; + } + const [, retainedName] = splitMCPToolKey(tool, sourceNames); + if (retainedName && sourceNameSet.has(retainedName)) { + resolvedServerNames.add(retainedName); + } + } + } newAgentData.mcpServerNames = Array.from(resolvedServerNames); } diff --git a/packages/data-provider/src/config.ts b/packages/data-provider/src/config.ts index 356c8b217e..6e54a6d15f 100644 --- a/packages/data-provider/src/config.ts +++ b/packages/data-provider/src/config.ts @@ -2809,15 +2809,22 @@ export function splitMCPToolKey( * Splits a tool-call name for display, where the key may be a synthetic MCP OAuth * call (`oauth${mcp_delimiter}${serverName}`) rather than a real tool key. * - * The OAuth form is unambiguous because its tool half is always exactly `oauth`, - * so everything after the first delimiter is the server even when the server name - * itself contains one. Real tool keys go through `splitMCPToolKey`, whose tool half - * is untrusted and may carry the delimiter. + * A configured server name is authoritative when one matches, because a real tool key + * always ends in its server. Only when none matches does the `oauth` prefix decide, + * which keeps a genuine upstream tool named `oauth${mcp_delimiter}...` from being read + * as a synthetic call while still resolving OAuth prompts for unconfigured servers. */ export function splitToolCallName( toolCallName: string, knownServerNames?: readonly string[], ): [string, string | undefined] { + if (knownServerNames?.length) { + const [toolName, serverName] = splitMCPToolKey(toolCallName, knownServerNames); + if (serverName != null && knownServerNames.includes(serverName)) { + return [toolName, serverName]; + } + } + const oauthPrefix = `oauth${Constants.mcp_delimiter}`; if (toolCallName.startsWith(oauthPrefix)) { return ['oauth', toolCallName.slice(oauthPrefix.length)]; diff --git a/packages/data-provider/src/splitMCPToolKey.spec.ts b/packages/data-provider/src/splitMCPToolKey.spec.ts index 1d1bfb20bc..0ac4410f96 100644 --- a/packages/data-provider/src/splitMCPToolKey.spec.ts +++ b/packages/data-provider/src/splitMCPToolKey.spec.ts @@ -69,3 +69,25 @@ describe('splitToolCallName', () => { ]); }); }); + +describe('splitToolCallName with configured server names', () => { + const d = Constants.mcp_delimiter; + + it('reads a real tool whose own name starts with the oauth prefix', () => { + expect(splitToolCallName(`oauth${d}reset${d}github`, ['github'])).toEqual([ + `oauth${d}reset`, + 'github', + ]); + }); + + it('still resolves a synthetic OAuth call for a configured server', () => { + expect(splitToolCallName(`oauth${d}github`, ['github'])).toEqual(['oauth', 'github']); + }); + + it('resolves a synthetic OAuth call for a delimiter-bearing configured server', () => { + expect(splitToolCallName(`oauth${d}foo${d}bar`, [`foo${d}bar`])).toEqual([ + 'oauth', + `foo${d}bar`, + ]); + }); +});