From 0751e7cd80567cb19e9c00ae2c02364f72352cfc Mon Sep 17 00:00:00 2001 From: Matteo Date: Mon, 15 Jun 2026 17:40:16 +0200 Subject: [PATCH] fix(connectors): apply db-rest host swap to Test connection too MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Deutsche Bahn connector stores the public base URL (v6.db.transport.rest) and the tool-execution path swaps it to the internal db-rest in cloud. But the 'Test connection' / health-check path used the raw stored base URL, so it hit the public (503-prone) endpoint instead of the real internal one — showing a failure for a connector that actually works. Extract the swap into a shared resolveInternalDbRestUrl util and use it in both DynamicMcpTools and ConnectorsService.testConnection. Move the unit test to the util (drop the now-obsolete dynamic-mcp-tools.spec). --- .../backend/src/common/db-rest.util.spec.ts | 35 +++++++++++ packages/backend/src/common/db-rest.util.ts | 24 ++++++++ .../src/connectors/connectors.service.ts | 6 +- .../src/mcp-server/dynamic-mcp-tools.spec.ts | 59 ------------------- .../src/mcp-server/dynamic-mcp-tools.ts | 17 ++---- 5 files changed, 69 insertions(+), 72 deletions(-) create mode 100644 packages/backend/src/common/db-rest.util.spec.ts create mode 100644 packages/backend/src/common/db-rest.util.ts delete mode 100644 packages/backend/src/mcp-server/dynamic-mcp-tools.spec.ts diff --git a/packages/backend/src/common/db-rest.util.spec.ts b/packages/backend/src/common/db-rest.util.spec.ts new file mode 100644 index 0000000..122d371 --- /dev/null +++ b/packages/backend/src/common/db-rest.util.spec.ts @@ -0,0 +1,35 @@ +import { resolveInternalDbRestUrl } from './db-rest.util'; + +describe('resolveInternalDbRestUrl', () => { + const PUBLIC = 'https://v6.db.transport.rest'; + const cloud = { DEPLOYMENT_MODE: 'cloud', DB_REST_INTERNAL_URL: 'http://db-rest:3000' }; + + it('swaps the public db-rest host for the internal one in cloud', () => { + expect(resolveInternalDbRestUrl(PUBLIC, cloud as any)).toBe('http://db-rest:3000'); + }); + + it('preserves any path and strips a trailing slash on the internal URL', () => { + expect( + resolveInternalDbRestUrl(`${PUBLIC}/locations`, { + DEPLOYMENT_MODE: 'cloud', + DB_REST_INTERNAL_URL: 'http://db-rest:3000/', + } as any), + ).toBe('http://db-rest:3000/locations'); + }); + + it('leaves the URL untouched when not in cloud (self-host)', () => { + expect( + resolveInternalDbRestUrl(PUBLIC, { DB_REST_INTERNAL_URL: 'http://db-rest:3000' } as any), + ).toBe(PUBLIC); + }); + + it('leaves the URL untouched when the internal URL is not configured', () => { + expect(resolveInternalDbRestUrl(PUBLIC, { DEPLOYMENT_MODE: 'cloud' } as any)).toBe(PUBLIC); + }); + + it('does not touch non-db-rest base URLs', () => { + expect(resolveInternalDbRestUrl('https://api.example.com', cloud as any)).toBe( + 'https://api.example.com', + ); + }); +}); diff --git a/packages/backend/src/common/db-rest.util.ts b/packages/backend/src/common/db-rest.util.ts new file mode 100644 index 0000000..811e4bb --- /dev/null +++ b/packages/backend/src/common/db-rest.util.ts @@ -0,0 +1,24 @@ +/** + * Cloud-only: route the public db-rest base URL to our internal self-hosted + * instance. The shipped Deutsche Bahn connector stores the public base URL + * (`v6.db.transport.rest`) so self-hosters use it as-is; in cloud we swap the + * host to the internal db-rest (`DB_REST_INTERNAL_URL`) at request time. Pure + * host swap — same db-rest schema both sides, so paths/params/responses are + * unchanged. Returns the URL untouched on self-host (env unset / not cloud). + * + * Used by both the tool-execution path (DynamicMcpTools) and the connector + * "Test connection" / health-check path so they exercise the SAME endpoint. + */ +const PUBLIC_DB_REST = 'https://v6.db.transport.rest'; + +export function resolveInternalDbRestUrl( + baseUrl: string, + env: NodeJS.ProcessEnv = process.env, +): string { + const internal = env.DB_REST_INTERNAL_URL; + const isCloud = (env.DEPLOYMENT_MODE || '') === 'cloud'; + if (internal && isCloud && baseUrl.startsWith(PUBLIC_DB_REST)) { + return internal.replace(/\/$/, '') + baseUrl.slice(PUBLIC_DB_REST.length); + } + return baseUrl; +} diff --git a/packages/backend/src/connectors/connectors.service.ts b/packages/backend/src/connectors/connectors.service.ts index 8168d9c..20abf3c 100644 --- a/packages/backend/src/connectors/connectors.service.ts +++ b/packages/backend/src/connectors/connectors.service.ts @@ -9,6 +9,7 @@ import { DatabaseEngine } from './engines/database.engine'; import { McpClientEngine } from './engines/mcp-client.engine'; import { encrypt, decrypt } from '../common/crypto/encryption.util'; import { getRequiredSecret } from '../common/secrets.util'; +import { resolveInternalDbRestUrl } from '../common/db-rest.util'; import { resolveAdapterIcon } from './connector-icon.util'; @Injectable() @@ -198,7 +199,10 @@ export class ConnectorsService { const path = connector.healthcheckPath || '/'; await this.restEngine.execute( { - baseUrl: connector.baseUrl, + // Apply the same cloud db-rest host swap as tool execution, so + // "Test connection" exercises the real (internal) endpoint + // instead of the public base URL stored on the connector. + baseUrl: resolveInternalDbRestUrl(connector.baseUrl), authType: connector.authType, authConfig, headers: connector.headers as Record, diff --git a/packages/backend/src/mcp-server/dynamic-mcp-tools.spec.ts b/packages/backend/src/mcp-server/dynamic-mcp-tools.spec.ts deleted file mode 100644 index da49620..0000000 --- a/packages/backend/src/mcp-server/dynamic-mcp-tools.spec.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { DynamicMcpTools } from './dynamic-mcp-tools'; -import { DeploymentService } from '../common/deployment.service'; - -/** - * Focused unit test for the cloud-only db-rest host swap. Only `deployment` - * is exercised, so the other constructor deps are passed as null. - */ -describe('DynamicMcpTools.resolveInternalBaseUrl', () => { - const make = (isCloud: boolean) => { - const deployment = { isCloud: () => isCloud } as unknown as DeploymentService; - const instance = new DynamicMcpTools( - null as any, - null as any, - null as any, - null as any, - deployment, - null as any, - null as any, - null as any, - null as any, - null as any, - null as any, - ); - return (url: string): string => - (instance as any).resolveInternalBaseUrl(url); - }; - - const PUBLIC = 'https://v6.db.transport.rest'; - const prev = process.env.DB_REST_INTERNAL_URL; - afterEach(() => { - if (prev === undefined) delete process.env.DB_REST_INTERNAL_URL; - else process.env.DB_REST_INTERNAL_URL = prev; - }); - - it('swaps the public db-rest host for the internal one in cloud', () => { - process.env.DB_REST_INTERNAL_URL = 'http://db-rest:3000'; - expect(make(true)(PUBLIC)).toBe('http://db-rest:3000'); - }); - - it('strips a trailing slash on the internal URL and preserves any path', () => { - process.env.DB_REST_INTERNAL_URL = 'http://db-rest:3000/'; - expect(make(true)(`${PUBLIC}/locations`)).toBe('http://db-rest:3000/locations'); - }); - - it('leaves the URL untouched when not in cloud (self-host)', () => { - process.env.DB_REST_INTERNAL_URL = 'http://db-rest:3000'; - expect(make(false)(PUBLIC)).toBe(PUBLIC); - }); - - it('leaves the URL untouched when the internal URL is not configured', () => { - delete process.env.DB_REST_INTERNAL_URL; - expect(make(true)(PUBLIC)).toBe(PUBLIC); - }); - - it('does not touch non-db-rest base URLs', () => { - process.env.DB_REST_INTERNAL_URL = 'http://db-rest:3000'; - expect(make(true)('https://api.example.com')).toBe('https://api.example.com'); - }); -}); diff --git a/packages/backend/src/mcp-server/dynamic-mcp-tools.ts b/packages/backend/src/mcp-server/dynamic-mcp-tools.ts index c8c506c..10a2997 100644 --- a/packages/backend/src/mcp-server/dynamic-mcp-tools.ts +++ b/packages/backend/src/mcp-server/dynamic-mcp-tools.ts @@ -13,6 +13,7 @@ import { LicenseGuardService } from '../license/license-guard.service'; import { DeploymentService } from '../common/deployment.service'; import { PrismaService } from '../common/prisma.service'; import { interpolateConnectorConfig } from '../common/env-interpolation.util'; +import { resolveInternalDbRestUrl } from '../common/db-rest.util'; import type { RegisteredTool } from './tool-registry'; /** @@ -85,20 +86,12 @@ export class DynamicMcpTools { } /** - * Cloud-only: route the public db-rest base URL to our internal self-hosted - * instance. The shipped connector stays identical for everyone — self-hosted - * installs (env unset, or not cloud) keep talking to the public API, while - * Cloud transparently swaps the host to the internal db-rest for reliability - * and to avoid the public instance's rate limits / 503s. Pure host swap: - * same db-rest schema on both sides, so paths/params/responses are unchanged. + * Cloud-only db-rest host swap — see resolveInternalDbRestUrl. Kept as a thin + * method so the host swap stays consistent with the connector "Test + * connection" path, which uses the same shared util. */ private resolveInternalBaseUrl(baseUrl: string): string { - const PUBLIC_DB_REST = 'https://v6.db.transport.rest'; - const internal = process.env.DB_REST_INTERNAL_URL; - if (internal && this.deployment.isCloud() && baseUrl.startsWith(PUBLIC_DB_REST)) { - return internal.replace(/\/$/, '') + baseUrl.slice(PUBLIC_DB_REST.length); - } - return baseUrl; + return resolveInternalDbRestUrl(baseUrl); } /**