From 61163ffc50bec18754084c0cf410b51405955467 Mon Sep 17 00:00:00 2001 From: Timo Derstappen Date: Thu, 11 Jun 2026 12:42:12 +0200 Subject: [PATCH] fix(ai-chat): bound MCP server loading with timeouts and fix SSE interop hang A single unresponsive MCP server hung the whole /chat request forever: servers were loaded serially with no timeout, and @ai-sdk/mcp drops SSE events that carry no explicit `event:` field (agentgateway emits bare `data:` frames), so the muster tools/list response was silently discarded and the request promise never settled. - Load MCP servers in parallel, each bounded by a timeout (15s default, configurable via aiChat.mcp[].timeoutMs); hanging servers degrade gracefully into failedServers instead of blocking the chat. - Patch @ai-sdk/mcp to default the SSE event type to "message" when the event field is absent, per the SSE specification. Co-authored-by: Cursor --- .changeset/ai-chat-mcp-timeout-sse-interop.md | 10 + .../@ai-sdk-mcp-npm-1.0.46-b48c61b836.patch | 62 ++++++ plugins/ai-chat-backend/package.json | 2 +- .../ai-chat-backend/src/getMcpTools.test.ts | 202 ++++++++++++++++++ plugins/ai-chat-backend/src/getMcpTools.ts | 133 +++++++++--- .../ai-chat-backend/src/mcpSseInterop.test.ts | 141 ++++++++++++ plugins/gs-node/package.json | 2 +- plugins/muster-backend/package.json | 2 +- yarn.lock | 21 +- 9 files changed, 534 insertions(+), 41 deletions(-) create mode 100644 .changeset/ai-chat-mcp-timeout-sse-interop.md create mode 100644 .yarn/patches/@ai-sdk-mcp-npm-1.0.46-b48c61b836.patch create mode 100644 plugins/ai-chat-backend/src/getMcpTools.test.ts create mode 100644 plugins/ai-chat-backend/src/mcpSseInterop.test.ts diff --git a/.changeset/ai-chat-mcp-timeout-sse-interop.md b/.changeset/ai-chat-mcp-timeout-sse-interop.md new file mode 100644 index 000000000..b56acd7e1 --- /dev/null +++ b/.changeset/ai-chat-mcp-timeout-sse-interop.md @@ -0,0 +1,10 @@ +--- +'@giantswarm/backstage-plugin-ai-chat-backend': patch +'@giantswarm/backstage-plugin-muster-backend': patch +'@giantswarm/backstage-plugin-gs-node': patch +--- + +Fix AI chat hanging forever when an MCP server is slow or its responses are dropped by the transport. + +- MCP servers are now connected in parallel and each connection/tool-load is bounded by a timeout (15s default, configurable per server via `aiChat.mcp[].timeoutMs`). A hanging server is reported as failed and the chat continues with the remaining servers' tools. +- Patch `@ai-sdk/mcp` to treat SSE events without an explicit `event:` field as `message` events, per the SSE specification. MCP servers behind agentgateway emit bare `data:` frames, which the unpatched client silently dropped — leaving the request promise pending forever and hanging the whole chat request. diff --git a/.yarn/patches/@ai-sdk-mcp-npm-1.0.46-b48c61b836.patch b/.yarn/patches/@ai-sdk-mcp-npm-1.0.46-b48c61b836.patch new file mode 100644 index 000000000..b2c10b309 --- /dev/null +++ b/.yarn/patches/@ai-sdk-mcp-npm-1.0.46-b48c61b836.patch @@ -0,0 +1,62 @@ +diff --git a/dist/index.js b/dist/index.js +index 973f16ed6bbe752f46dc417dd4be08c78526fcfb..6a6ef2412c0044ffd898110254897626857c6238 100644 +--- a/dist/index.js ++++ b/dist/index.js +@@ -1222,7 +1222,7 @@ var SseMCPTransport = class { + } + this.connected = true; + resolve(); +- } else if (event === "message") { ++ } else if ((event === void 0 || event === "message")) { + try { + const message = await parseJSONRPCMessage(data); + (_a4 = this.onmessage) == null ? void 0 : _a4.call(this, message); +@@ -1497,7 +1497,7 @@ var HttpMCPTransport = class { + const { done, value } = await reader.read(); + if (done) return; + const { event, data } = value; +- if (event === "message") { ++ if ((event === void 0 || event === "message")) { + try { + const msg = await parseJSONRPCMessage(data); + (_a4 = this.onmessage) == null ? void 0 : _a4.call(this, msg); +@@ -1624,7 +1624,7 @@ var HttpMCPTransport = class { + if (id) { + this.lastInboundEventId = id; + } +- if (event === "message") { ++ if ((event === void 0 || event === "message")) { + try { + const msg = await parseJSONRPCMessage(data); + (_a4 = this.onmessage) == null ? void 0 : _a4.call(this, msg); +diff --git a/dist/index.mjs b/dist/index.mjs +index 2b80c31413f15377f1bfa76a520a914655d720d0..7298ad5e561e1847047afda222221f216144d57a 100644 +--- a/dist/index.mjs ++++ b/dist/index.mjs +@@ -1192,7 +1192,7 @@ var SseMCPTransport = class { + } + this.connected = true; + resolve(); +- } else if (event === "message") { ++ } else if ((event === void 0 || event === "message")) { + try { + const message = await parseJSONRPCMessage(data); + (_a4 = this.onmessage) == null ? void 0 : _a4.call(this, message); +@@ -1471,7 +1471,7 @@ var HttpMCPTransport = class { + const { done, value } = await reader.read(); + if (done) return; + const { event, data } = value; +- if (event === "message") { ++ if ((event === void 0 || event === "message")) { + try { + const msg = await parseJSONRPCMessage(data); + (_a4 = this.onmessage) == null ? void 0 : _a4.call(this, msg); +@@ -1598,7 +1598,7 @@ var HttpMCPTransport = class { + if (id) { + this.lastInboundEventId = id; + } +- if (event === "message") { ++ if ((event === void 0 || event === "message")) { + try { + const msg = await parseJSONRPCMessage(data); + (_a4 = this.onmessage) == null ? void 0 : _a4.call(this, msg); diff --git a/plugins/ai-chat-backend/package.json b/plugins/ai-chat-backend/package.json index 284cd757d..f93210fc4 100644 --- a/plugins/ai-chat-backend/package.json +++ b/plugins/ai-chat-backend/package.json @@ -27,7 +27,7 @@ "dependencies": { "@ai-sdk/anthropic": "^3.0.76", "@ai-sdk/azure": "^3.0.64", - "@ai-sdk/mcp": "^1.0.41", + "@ai-sdk/mcp": "patch:@ai-sdk/mcp@npm%3A1.0.46#~/.yarn/patches/@ai-sdk-mcp-npm-1.0.46-b48c61b836.patch", "@ai-sdk/openai": "^3.0.63", "@ai-sdk/openai-compatible": "^2.0.47", "@backstage/backend-defaults": "backstage:^", diff --git a/plugins/ai-chat-backend/src/getMcpTools.test.ts b/plugins/ai-chat-backend/src/getMcpTools.test.ts new file mode 100644 index 000000000..661338ac7 --- /dev/null +++ b/plugins/ai-chat-backend/src/getMcpTools.test.ts @@ -0,0 +1,202 @@ +import { ConfigReader } from '@backstage/config'; +import { LoggerService } from '@backstage/backend-plugin-api'; +import { McpClientCache } from '@giantswarm/backstage-plugin-gs-node'; +import { experimental_createMCPClient as createMCPClient } from '@ai-sdk/mcp'; +import { getMcpTools } from './getMcpTools'; + +jest.mock('@ai-sdk/mcp', () => ({ + experimental_createMCPClient: jest.fn(), +})); + +const createMCPClientMock = createMCPClient as jest.Mock; + +function mockLogger(): LoggerService { + const logger = { + debug: jest.fn(), + info: jest.fn(), + warn: jest.fn(), + error: jest.fn(), + child: jest.fn(), + }; + logger.child.mockReturnValue(logger); + return logger as unknown as LoggerService; +} + +function makeGoodClient() { + return { + listResources: jest + .fn() + .mockRejectedValue(new Error('does not support resources')), + tools: jest.fn().mockResolvedValue({ + my_tool: { + description: 'A test tool', + execute: async () => 'ok', + }, + }), + close: jest.fn().mockResolvedValue(undefined), + }; +} + +describe('getMcpTools', () => { + let clientCache: McpClientCache; + + beforeEach(() => { + jest.clearAllMocks(); + clientCache = new McpClientCache(mockLogger()); + }); + + afterEach(async () => { + // Don't await dispose: it may wait on never-resolving client promises + // that the hanging-server tests intentionally leave behind. + void clientCache.dispose().catch(() => {}); + }); + + it('loads tools from a responsive MCP server', async () => { + createMCPClientMock.mockImplementation(() => + Promise.resolve(makeGoodClient()), + ); + + const config = new ConfigReader({ + aiChat: { + mcp: [{ name: 'good', url: 'http://good.example.com/mcp' }], + }, + }); + + const result = await getMcpTools( + config, + {}, + undefined, + mockLogger(), + clientCache, + ); + + expect(result.connectedServers).toEqual(['good']); + expect(result.failedServers).toEqual([]); + expect(Object.keys(result.tools)).toEqual(['my_tool']); + }); + + it('does not hang the chat request when an MCP server never completes the connection handshake', async () => { + createMCPClientMock.mockImplementation(({ name }: { name: string }) => { + if (name === 'hanging') { + // Simulates a server whose initialize response is never delivered + // to the client (observed in production with muster behind + // agentgateway): the promise never settles. + return new Promise(() => {}); + } + return Promise.resolve(makeGoodClient()); + }); + + const config = new ConfigReader({ + aiChat: { + mcp: [ + { name: 'good', url: 'http://good.example.com/mcp' }, + { + name: 'hanging', + url: 'http://hanging.example.com/mcp', + timeoutMs: 250, + }, + ], + }, + }); + + const result = await getMcpTools( + config, + {}, + undefined, + mockLogger(), + clientCache, + ); + + expect(result.connectedServers).toEqual(['good']); + expect(result.failedServers).toHaveLength(1); + expect(result.failedServers[0].name).toBe('hanging'); + expect(result.failedServers[0].error).toMatch(/timed out/i); + // Tools from the healthy server are still available. + expect(Object.keys(result.tools)).toEqual(['my_tool']); + }, 5000); + + it('does not hang the chat request when tools/list never returns', async () => { + createMCPClientMock.mockImplementation(({ name }: { name: string }) => { + if (name === 'hanging-tools') { + return Promise.resolve({ + listResources: jest + .fn() + .mockRejectedValue(new Error('does not support resources')), + // tools/list response never arrives + tools: jest.fn().mockReturnValue(new Promise(() => {})), + close: jest.fn().mockResolvedValue(undefined), + }); + } + return Promise.resolve(makeGoodClient()); + }); + + const config = new ConfigReader({ + aiChat: { + mcp: [ + { + name: 'hanging-tools', + url: 'http://hanging.example.com/mcp', + timeoutMs: 250, + }, + { name: 'good', url: 'http://good.example.com/mcp' }, + ], + }, + }); + + const result = await getMcpTools( + config, + {}, + undefined, + mockLogger(), + clientCache, + ); + + expect(result.connectedServers).toEqual(['good']); + expect(result.failedServers).toHaveLength(1); + expect(result.failedServers[0].name).toBe('hanging-tools'); + expect(result.failedServers[0].error).toMatch(/timed out/i); + expect(Object.keys(result.tools)).toEqual(['my_tool']); + }, 5000); + + it('evicts a timed-out server from the cache so the next request retries', async () => { + let callCount = 0; + createMCPClientMock.mockImplementation(() => { + callCount += 1; + if (callCount === 1) { + return new Promise(() => {}); + } + return Promise.resolve(makeGoodClient()); + }); + + const config = new ConfigReader({ + aiChat: { + mcp: [ + { + name: 'flaky', + url: 'http://flaky.example.com/mcp', + timeoutMs: 250, + }, + ], + }, + }); + + const first = await getMcpTools( + config, + {}, + undefined, + mockLogger(), + clientCache, + ); + expect(first.failedServers).toHaveLength(1); + + const second = await getMcpTools( + config, + {}, + undefined, + mockLogger(), + clientCache, + ); + expect(second.connectedServers).toEqual(['flaky']); + expect(second.failedServers).toEqual([]); + }, 5000); +}); diff --git a/plugins/ai-chat-backend/src/getMcpTools.ts b/plugins/ai-chat-backend/src/getMcpTools.ts index 1aee0e9fe..6511e4576 100644 --- a/plugins/ai-chat-backend/src/getMcpTools.ts +++ b/plugins/ai-chat-backend/src/getMcpTools.ts @@ -16,8 +16,18 @@ interface McpServerConfig { headers?: Record; installation?: string; authToken?: string; + timeoutMs?: number; } +/** + * Upper bound for connecting to an MCP server and loading its resources + * and tools. Without it a single unresponsive server hangs the whole chat + * request forever (observed in production when an MCP server's responses + * were silently dropped by the transport). Override per server with + * `aiChat.mcp[].timeoutMs`. + */ +const DEFAULT_MCP_SERVER_TIMEOUT_MS = 15_000; + export interface BackstageUserContext { userEntityRef: string; mcpActionsToken: string; @@ -52,6 +62,7 @@ function readMcpServersFromConfig( const name = mcp.getString('name'); const url = mcp.getString('url'); const installation = mcp.getOptionalString('installation'); + const timeoutMs = mcp.getOptionalNumber('timeoutMs'); // Rule 1: Forward a Backstage token minted on behalf of the calling // user, scoped to the built-in `mcp-actions` plugin. Use this for @@ -75,6 +86,7 @@ function readMcpServersFromConfig( // Stable per-user cache key so the cache survives across // requests despite the Authorization token being minted fresh. authToken: `bs-user:${backstageUser.userEntityRef}`, + timeoutMs, }; continue; @@ -93,6 +105,7 @@ function readMcpServersFromConfig( headers: { Authorization: `Bearer ${token}` }, installation, authToken: token, + timeoutMs, }; continue; @@ -101,12 +114,12 @@ function readMcpServersFromConfig( // Rule 3: If headers configured, add server with those headers const headers = getMcpServerHeaders(mcp); if (headers) { - mcpServers[name] = { url, headers, installation }; + mcpServers[name] = { url, headers, installation, timeoutMs }; continue; } // Rule 4: No headers and no authProvider, just add server - mcpServers[name] = { url, installation }; + mcpServers[name] = { url, installation, timeoutMs }; } return mcpServers; @@ -267,28 +280,38 @@ export interface McpToolsResult { connectedServers: string[]; } -export async function getMcpTools( - config: Config, - authTokens: AuthTokens, - backstageUser: BackstageUserContext | undefined, +interface ServerLoadResult { + serverName: string; + tools?: ToolSet; + resources?: { [resourceName: string]: string }; + error?: string; +} + +async function loadServer( + serverName: string, + server: McpServerConfig, logger: LoggerService, clientCache: McpClientCache, -): Promise { - const mcpServers = readMcpServersFromConfig( - config, - authTokens, - backstageUser, - ); - - const tools: ToolSet = {}; - const resources: { [resourceName: string]: string } = {}; - const failedServers: Array<{ name: string; error: string }> = []; - const connectedServers: string[] = []; - - for (const [serverName, server] of Object.entries(mcpServers)) { - const cacheKey = McpClientCache.buildKey(serverName, server.authToken); +): Promise { + const cacheKey = McpClientCache.buildKey(serverName, server.authToken); + const timeoutMs = server.timeoutMs ?? DEFAULT_MCP_SERVER_TIMEOUT_MS; + + let timer: ReturnType | undefined; + const timeout = new Promise((_, reject) => { + timer = setTimeout( + () => + reject( + new Error( + `Timed out after ${timeoutMs}ms waiting for MCP server '${serverName}'`, + ), + ), + timeoutMs, + ); + timer.unref?.(); + }); - try { + try { + const load = (async () => { const mcpClient = await clientCache.getOrCreate(cacheKey, () => createMCPClient({ name: serverName, @@ -301,8 +324,6 @@ export async function getMcpTools( ); const serverResources = await getResources(mcpClient, serverName, logger); - Object.assign(resources, serverResources); - const serverTools = await getTools( mcpClient, serverName, @@ -310,17 +331,61 @@ export async function getMcpTools( () => clientCache.markDead(cacheKey), server.installation, ); - Object.assign(tools, serverTools); - connectedServers.push(serverName); - } catch (error) { - const errorMessage = - error instanceof Error ? error.message : String(error); - logger.error( - `Failed to connect to MCP server '${serverName}' at ${server.url}: ${errorMessage}`, - ); - failedServers.push({ name: serverName, error: errorMessage }); - // Evict broken session so the next request retries - await clientCache.invalidate(cacheKey); + + return { serverName, tools: serverTools, resources: serverResources }; + })(); + + return await Promise.race([load, timeout]); + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error); + logger.error( + `Failed to connect to MCP server '${serverName}' at ${server.url}: ${errorMessage}`, + ); + // Evict the broken session so the next request retries. Not awaited: + // invalidate awaits the cached client promise before closing it, which + // may itself never settle when the server is hanging. + void clientCache.invalidate(cacheKey).catch(() => {}); + return { serverName, error: errorMessage }; + } finally { + clearTimeout(timer); + } +} + +export async function getMcpTools( + config: Config, + authTokens: AuthTokens, + backstageUser: BackstageUserContext | undefined, + logger: LoggerService, + clientCache: McpClientCache, +): Promise { + const mcpServers = readMcpServersFromConfig( + config, + authTokens, + backstageUser, + ); + + const tools: ToolSet = {}; + const resources: { [resourceName: string]: string } = {}; + const failedServers: Array<{ name: string; error: string }> = []; + const connectedServers: string[] = []; + + // Load all servers in parallel; each is individually bounded by its + // timeout so one slow or hanging server neither delays nor blocks the + // others. Results are merged in config order to keep tool precedence + // deterministic. + const results = await Promise.all( + Object.entries(mcpServers).map(([serverName, server]) => + loadServer(serverName, server, logger, clientCache), + ), + ); + + for (const result of results) { + if (result.error !== undefined) { + failedServers.push({ name: result.serverName, error: result.error }); + } else { + Object.assign(resources, result.resources); + Object.assign(tools, result.tools); + connectedServers.push(result.serverName); } } diff --git a/plugins/ai-chat-backend/src/mcpSseInterop.test.ts b/plugins/ai-chat-backend/src/mcpSseInterop.test.ts new file mode 100644 index 000000000..311a28dcf --- /dev/null +++ b/plugins/ai-chat-backend/src/mcpSseInterop.test.ts @@ -0,0 +1,141 @@ +import http from 'http'; +import { AddressInfo } from 'net'; +import { experimental_createMCPClient as createMCPClient } from '@ai-sdk/mcp'; + +/** + * Regression test for the production AI chat hang (2026-06-11). + * + * MCP servers behind agentgateway answer streamable-HTTP POSTs with + * `Content-Type: text/event-stream` bodies whose events carry no explicit + * `event:` field — only `data:` lines. Per the SSE specification, an event + * without an `event:` field defaults to the type "message", so a compliant + * client must process it. @ai-sdk/mcp 1.0.x compared the parsed event type + * strictly against the string "message" (`event === 'message'`), dropping + * bare-data events and leaving the request promise pending forever, which + * hung the whole chat request. + * + * This test runs the real @ai-sdk/mcp client against a minimal MCP server + * that mimics agentgateway's SSE framing. It fails (times out) without the + * @ai-sdk/mcp patch and passes with it. + */ +describe('MCP client interop with bare-data SSE responses (agentgateway framing)', () => { + let server: http.Server; + let url: string; + + beforeAll(async () => { + server = http.createServer((req, res) => { + if (req.method === 'GET') { + // No standalone inbound SSE stream; clients must tolerate this. + res.writeHead(405).end(); + return; + } + if (req.method === 'DELETE') { + res.writeHead(200).end(); + return; + } + + let body = ''; + req.on('data', chunk => { + body += chunk; + }); + req.on('end', () => { + const message = JSON.parse(body); + + // Notifications get an empty 202, like agentgateway/mcp-go. + if (!('id' in message)) { + res.writeHead(202).end(); + return; + } + + let result: unknown; + switch (message.method) { + case 'initialize': + result = { + protocolVersion: message.params.protocolVersion, + capabilities: { tools: { listChanged: true } }, + serverInfo: { name: 'fake-agentgateway', version: '0.0.1' }, + }; + break; + case 'tools/list': + result = { + tools: [ + { + name: 'echo', + description: 'Echoes the input', + inputSchema: { + type: 'object', + properties: { text: { type: 'string' } }, + }, + }, + ], + }; + break; + default: + res.writeHead(200, { + 'Content-Type': 'text/event-stream', + 'mcp-session-id': 'test-session', + }); + res.end( + `data: ${JSON.stringify({ + jsonrpc: '2.0', + id: message.id, + error: { code: -32601, message: 'Method not found' }, + })}\n\n`, + ); + return; + } + + res.writeHead(200, { + 'Content-Type': 'text/event-stream', + 'mcp-session-id': 'test-session', + }); + // Crucially: no `event: message` line, only `data:` — this is the + // framing agentgateway produces. + res.end( + `data: ${JSON.stringify({ + jsonrpc: '2.0', + id: message.id, + result, + })}\n\n`, + ); + }); + }); + + await new Promise(resolve => { + server.listen(0, '127.0.0.1', () => resolve()); + }); + const { address, port } = server.address() as AddressInfo; + url = `http://${address}:${port}/mcp`; + }); + + afterAll(async () => { + await new Promise(resolve => { + server.close(() => resolve()); + }); + }); + + it('initializes and lists tools when SSE events have no explicit event field', async () => { + const guard = (label: string) => + new Promise((_, reject) => { + const timer = setTimeout( + () => reject(new Error(`${label} did not complete within 3s`)), + 3000, + ); + timer.unref(); + }); + + const client = await Promise.race([ + createMCPClient({ + name: 'bare-data-sse-server', + transport: { type: 'http', url }, + }), + guard('initialize'), + ]); + + const tools = await Promise.race([client.tools(), guard('tools/list')]); + + expect(Object.keys(tools)).toContain('echo'); + + await client.close(); + }, 10000); +}); diff --git a/plugins/gs-node/package.json b/plugins/gs-node/package.json index f85516321..618d46a08 100644 --- a/plugins/gs-node/package.json +++ b/plugins/gs-node/package.json @@ -24,7 +24,7 @@ "postpack": "backstage-cli package postpack" }, "dependencies": { - "@ai-sdk/mcp": "^1.0.41", + "@ai-sdk/mcp": "patch:@ai-sdk/mcp@npm%3A1.0.46#~/.yarn/patches/@ai-sdk-mcp-npm-1.0.46-b48c61b836.patch", "@backstage/backend-plugin-api": "backstage:^", "@backstage/errors": "backstage:^", "@backstage/types": "backstage:^", diff --git a/plugins/muster-backend/package.json b/plugins/muster-backend/package.json index b21f35927..a444cf7b4 100644 --- a/plugins/muster-backend/package.json +++ b/plugins/muster-backend/package.json @@ -25,7 +25,7 @@ "postpack": "backstage-cli package postpack" }, "dependencies": { - "@ai-sdk/mcp": "^1.0.41", + "@ai-sdk/mcp": "patch:@ai-sdk/mcp@npm%3A1.0.46#~/.yarn/patches/@ai-sdk-mcp-npm-1.0.46-b48c61b836.patch", "@backstage/backend-plugin-api": "backstage:^", "@backstage/config": "backstage:^", "@backstage/errors": "backstage:^", diff --git a/yarn.lock b/yarn.lock index 72137cd96..f7990f24d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -100,7 +100,7 @@ __metadata: languageName: node linkType: hard -"@ai-sdk/mcp@npm:^1.0.41": +"@ai-sdk/mcp@npm:1.0.46": version: 1.0.46 resolution: "@ai-sdk/mcp@npm:1.0.46" dependencies: @@ -113,6 +113,19 @@ __metadata: languageName: node linkType: hard +"@ai-sdk/mcp@patch:@ai-sdk/mcp@npm%3A1.0.46#~/.yarn/patches/@ai-sdk-mcp-npm-1.0.46-b48c61b836.patch": + version: 1.0.46 + resolution: "@ai-sdk/mcp@patch:@ai-sdk/mcp@npm%3A1.0.46#~/.yarn/patches/@ai-sdk-mcp-npm-1.0.46-b48c61b836.patch::version=1.0.46&hash=0cae29" + dependencies: + "@ai-sdk/provider": "npm:3.0.10" + "@ai-sdk/provider-utils": "npm:4.0.27" + pkce-challenge: "npm:^5.0.0" + peerDependencies: + zod: ^3.25.76 || ^4.1.8 + checksum: 10c0/d20fc5b7b9b763b8dcc95fcbb714e140ce14d1b886e725c626e8769a4f1ee18b0e95e57dcc96ce5dcc4870a6b95da7e221e27c9bfcb45e5f37cd0f27c37f7f52 + languageName: node + linkType: hard + "@ai-sdk/openai-compatible@npm:^2.0.47": version: 2.0.48 resolution: "@ai-sdk/openai-compatible@npm:2.0.48" @@ -10639,7 +10652,7 @@ __metadata: dependencies: "@ai-sdk/anthropic": "npm:^3.0.76" "@ai-sdk/azure": "npm:^3.0.64" - "@ai-sdk/mcp": "npm:^1.0.41" + "@ai-sdk/mcp": "patch:@ai-sdk/mcp@npm%3A1.0.46#~/.yarn/patches/@ai-sdk-mcp-npm-1.0.46-b48c61b836.patch" "@ai-sdk/openai": "npm:^3.0.63" "@ai-sdk/openai-compatible": "npm:^2.0.47" "@backstage/backend-defaults": "backstage:^" @@ -10880,7 +10893,7 @@ __metadata: version: 0.0.0-use.local resolution: "@giantswarm/backstage-plugin-gs-node@workspace:plugins/gs-node" dependencies: - "@ai-sdk/mcp": "npm:^1.0.41" + "@ai-sdk/mcp": "patch:@ai-sdk/mcp@npm%3A1.0.46#~/.yarn/patches/@ai-sdk-mcp-npm-1.0.46-b48c61b836.patch" "@backstage/backend-plugin-api": "backstage:^" "@backstage/backend-test-utils": "backstage:^" "@backstage/cli": "backstage:^" @@ -11007,7 +11020,7 @@ __metadata: version: 0.0.0-use.local resolution: "@giantswarm/backstage-plugin-muster-backend@workspace:plugins/muster-backend" dependencies: - "@ai-sdk/mcp": "npm:^1.0.41" + "@ai-sdk/mcp": "patch:@ai-sdk/mcp@npm%3A1.0.46#~/.yarn/patches/@ai-sdk-mcp-npm-1.0.46-b48c61b836.patch" "@backstage/backend-defaults": "backstage:^" "@backstage/backend-plugin-api": "backstage:^" "@backstage/backend-test-utils": "backstage:^"