From b85bbb9089ab35ed56bfd629eecf49336df22a0d Mon Sep 17 00:00:00 2001 From: Matteo Date: Mon, 5 Oct 2026 11:00:34 +0200 Subject: [PATCH] MCP connectors cannot point at this AnythingMCP server itself A cloud user created an MCP connector with the URL cloud.anythingmcp.com. It imported our own kg_how_to_obtain tool, and every call to it re-entered our /mcp and called itself again: 13,709 calls in two hours on 4 Oct, each held for the 60 s timeout, and everyone's p95 nearly doubled. The MCP client now refuses, before connecting (discovery and calls), a URL whose host is this instance's own SERVER_URL / FRONTEND_URL / CLOUD_PUBLIC_URL host, with a message that says why. Host and port are compared, so another MCP server on the same machine stays allowed on self-hosted instances. Production: 3 such connectors exist; only the looping one was ever called. --- .../engines/mcp-client.engine.spec.ts | 29 +++++++++++++- .../connectors/engines/mcp-client.engine.ts | 38 +++++++++++++++++++ 2 files changed, 66 insertions(+), 1 deletion(-) diff --git a/packages/backend/src/connectors/engines/mcp-client.engine.spec.ts b/packages/backend/src/connectors/engines/mcp-client.engine.spec.ts index 297adec5..15386037 100644 --- a/packages/backend/src/connectors/engines/mcp-client.engine.spec.ts +++ b/packages/backend/src/connectors/engines/mcp-client.engine.spec.ts @@ -1,4 +1,4 @@ -import { McpClientEngine } from './mcp-client.engine'; +import { McpClientEngine, assertNotThisServer } from './mcp-client.engine'; import { OAuth2TokenService } from './oauth2-token.service'; import { Client, StreamableHTTPClientTransport } from '@modelcontextprotocol/client'; import { assertSafeOutboundUrl } from '../../common/ssrf.util'; @@ -170,3 +170,30 @@ describe('McpClientEngine endpoint resolution', () => { }); }); }); + +describe('assertNotThisServer', () => { + const env = { SERVER_URL: 'https://cloud.anythingmcp.com', FRONTEND_URL: 'https://cloud.anythingmcp.com' } as NodeJS.ProcessEnv; + + it.each(['https://cloud.anythingmcp.com/mcp', 'https://CLOUD.anythingmcp.com/mcp/abc123'])( + 'refuses an MCP connector that points at this server: %s', + (url) => { + expect(() => assertNotThisServer(new URL(url), env)).toThrow(/points at this AnythingMCP server itself/); + }, + ); + + it('allows another server, including another AnythingMCP instance', () => { + expect(() => assertNotThisServer(new URL('https://mcp.example.com/mcp'), env)).not.toThrow(); + expect(() => assertNotThisServer(new URL('https://amcp.customer.de/mcp'), env)).not.toThrow(); + }); + + it('on a self-hosted instance, another MCP server on the same machine is allowed', () => { + const local = { SERVER_URL: 'http://localhost:4000' } as NodeJS.ProcessEnv; + expect(() => assertNotThisServer(new URL('http://localhost:8080/mcp'), local)).not.toThrow(); + expect(() => assertNotThisServer(new URL('http://localhost:4000/mcp'), local)).toThrow(/itself/); + }); + + it('does nothing when the instance does not know its own URL', () => { + expect(() => assertNotThisServer(new URL('https://cloud.anythingmcp.com/mcp'), {} as NodeJS.ProcessEnv)).not.toThrow(); + }); +}); + diff --git a/packages/backend/src/connectors/engines/mcp-client.engine.ts b/packages/backend/src/connectors/engines/mcp-client.engine.ts index 88dc6244..eddcf383 100644 --- a/packages/backend/src/connectors/engines/mcp-client.engine.ts +++ b/packages/backend/src/connectors/engines/mcp-client.engine.ts @@ -38,6 +38,7 @@ export class McpClientEngine { const mcpUrl = resolveMcpEndpointUrl(config.baseUrl, endpointMapping.path); this.warnLegacyUrlChange(config.baseUrl, endpointMapping.path, mcpUrl); + assertNotThisServer(mcpUrl); await assertSafeOutboundUrl(mcpUrl.toString()); const headers: Record = { ...config.headers }; @@ -134,6 +135,7 @@ export class McpClientEngine { // Discovery reaches a user-supplied URL just like execute() does, so it // needs the same SSRF guard — it was missing here. + assertNotThisServer(mcpUrl); await assertSafeOutboundUrl(mcpUrl.toString()); const headers: Record = { ...config.headers }; @@ -238,3 +240,39 @@ export class McpClientEngine { } } } + +/** + * The public hosts of this AnythingMCP instance (SERVER_URL, FRONTEND_URL, + * CLOUD_PUBLIC_URL), lower-cased, with the port when it is not the default: + * another MCP server on the same machine (localhost:8080 next to + * localhost:4000) is a different server. + */ +export function thisServerHostnames(env: NodeJS.ProcessEnv = process.env): Set { + const out = new Set(); + for (const raw of [env.SERVER_URL, env.FRONTEND_URL, env.CLOUD_PUBLIC_URL]) { + if (!raw) continue; + try { + out.add(new URL(raw).host.toLowerCase()); + } catch { + /* not a URL: ignore */ + } + } + return out; +} + +/** + * An MCP connector pointing at this very server makes every call re-enter + * our own /mcp, which calls the connector again: an endless loop that holds + * a request for the full timeout at each hop. On 4 Oct 2026 one such + * connector ("Claude Etsy", URL cloud.anythingmcp.com) made 13,709 calls in + * two hours, each timing out after 60 s, and doubled everyone's p95. + */ +export function assertNotThisServer(mcpUrl: URL, env: NodeJS.ProcessEnv = process.env): void { + if (!thisServerHostnames(env).has(mcpUrl.host.toLowerCase())) return; + throw new Error( + `This MCP connector points at this AnythingMCP server itself (${mcpUrl.host}), so every call ` + + `would call itself again in a loop. Its tools are already here: use this server's own connectors ` + + `instead, and point an MCP connector only at another MCP server.`, + ); +} +