diff --git a/platform/backend/src/routes/internal-mcp-catalog.env.test.ts b/platform/backend/src/routes/internal-mcp-catalog.env.test.ts index b2f680bbf0..d2fe85def6 100644 --- a/platform/backend/src/routes/internal-mcp-catalog.env.test.ts +++ b/platform/backend/src/routes/internal-mcp-catalog.env.test.ts @@ -701,6 +701,77 @@ spec: }); }); + // ========================================================================= + // 4. AUTH METHOD SWITCHING + // ========================================================================= + describe("AUTH METHOD SWITCHING", () => { + test("4.1 switches from OAuth to Bearer — clears oauthConfig", async () => { + // Create catalog with OAuth config + const catalog = await InternalMcpCatalogModel.create({ + name: "test-oauth-to-bearer", + serverType: "remote", + serverUrl: "https://example.com/mcp", + oauthConfig: { + name: "test-oauth-to-bearer", + server_url: "https://example.com/mcp", + client_id: "test-client-id", + redirect_uris: ["https://localhost/callback"], + scopes: ["read", "write"], + default_scopes: ["read", "write"], + supports_resource_metadata: true, + }, + }); + + expect(catalog.oauthConfig).not.toBeNull(); + + // Switch to Bearer by setting oauthConfig to null and providing userConfig + const updated = await InternalMcpCatalogModel.update(catalog.id, { + oauthConfig: null, + userConfig: { + access_token: { + type: "string", + title: "Access Token", + description: "Bearer token for authentication", + required: true, + sensitive: true, + }, + }, + }); + + expect(updated?.oauthConfig).toBeNull(); + expect(updated?.userConfig).toHaveProperty("access_token"); + }); + + test("4.2 switches from OAuth to no auth — clears oauthConfig and userConfig", async () => { + // Create catalog with OAuth config + const catalog = await InternalMcpCatalogModel.create({ + name: "test-oauth-to-none", + serverType: "remote", + serverUrl: "https://example.com/mcp", + oauthConfig: { + name: "test-oauth-to-none", + server_url: "https://example.com/mcp", + client_id: "test-client-id", + redirect_uris: ["https://localhost/callback"], + scopes: ["read", "write"], + default_scopes: ["read", "write"], + supports_resource_metadata: true, + }, + }); + + expect(catalog.oauthConfig).not.toBeNull(); + + // Switch to no auth + const updated = await InternalMcpCatalogModel.update(catalog.id, { + oauthConfig: null, + userConfig: {}, + }); + + expect(updated?.oauthConfig).toBeNull(); + expect(updated?.userConfig).toEqual({}); + }); + }); + // ========================================================================= // Edge Cases // ========================================================================= diff --git a/platform/backend/src/routes/internal-mcp-catalog.ts b/platform/backend/src/routes/internal-mcp-catalog.ts index 49cbbc7af6..5b4363ca58 100644 --- a/platform/backend/src/routes/internal-mcp-catalog.ts +++ b/platform/backend/src/routes/internal-mcp-catalog.ts @@ -395,6 +395,12 @@ const internalMcpCatalogRoutes: FastifyPluginAsyncZod = async (fastify) => { delete restBody.oauthConfig.client_secret; } + // Handle switching away from OAuth: clean up old client secret + if (restBody.oauthConfig === null && originalCatalogItem.clientSecretId) { + await secretManager().deleteSecret(originalCatalogItem.clientSecretId); + restBody.clientSecretId = null; + } + // Handle local config secrets - either via Readonly Vault or direct values if (localConfigVaultPath && localConfigVaultKey) { // Readonly Vault flow for local config secrets diff --git a/platform/frontend/src/app/mcp-catalog/_parts/mcp-catalog-form.test.ts b/platform/frontend/src/app/mcp-catalog/_parts/mcp-catalog-form.test.ts index 8e465ac19e..882f657b97 100644 --- a/platform/frontend/src/app/mcp-catalog/_parts/mcp-catalog-form.test.ts +++ b/platform/frontend/src/app/mcp-catalog/_parts/mcp-catalog-form.test.ts @@ -1,5 +1,9 @@ +import type { McpCatalogFormValues } from "./mcp-catalog-form.types"; import { formSchema } from "./mcp-catalog-form.types"; -import { stripEnvVarQuotes } from "./mcp-catalog-form.utils"; +import { + stripEnvVarQuotes, + transformFormToApiData, +} from "./mcp-catalog-form.utils"; describe("stripEnvVarQuotes", () => { describe("real-world environment variable examples", () => { @@ -290,3 +294,62 @@ describe("formSchema", () => { }); }); }); + +describe("transformFormToApiData", () => { + const baseFormValues: McpCatalogFormValues = { + name: "test-server", + serverType: "remote", + serverUrl: "https://example.com/mcp", + authMethod: "none", + oauthConfig: undefined, + }; + + it("oauthConfig is null when authMethod is bearer", () => { + const result = transformFormToApiData({ + ...baseFormValues, + authMethod: "bearer", + }); + expect(result.oauthConfig).toBeNull(); + }); + + it("oauthConfig is null when authMethod is raw_token", () => { + const result = transformFormToApiData({ + ...baseFormValues, + authMethod: "raw_token", + }); + expect(result.oauthConfig).toBeNull(); + }); + + it("oauthConfig is null when authMethod is none", () => { + const result = transformFormToApiData({ + ...baseFormValues, + authMethod: "none", + }); + expect(result.oauthConfig).toBeNull(); + }); + + it("oauthConfig is set when authMethod is oauth", () => { + const result = transformFormToApiData({ + ...baseFormValues, + authMethod: "oauth", + oauthConfig: { + client_id: "test-id", + client_secret: "test-secret", + redirect_uris: "https://localhost/callback", + scopes: "read,write", + supports_resource_metadata: true, + }, + }); + expect(result.oauthConfig).not.toBeNull(); + expect(result.oauthConfig?.client_id).toBe("test-id"); + }); + + it("null oauthConfig survives JSON serialization", () => { + const result = transformFormToApiData({ + ...baseFormValues, + authMethod: "bearer", + }); + const roundTripped = JSON.parse(JSON.stringify(result)); + expect(roundTripped.oauthConfig).toBeNull(); + }); +}); diff --git a/platform/frontend/src/app/mcp-catalog/_parts/mcp-catalog-form.utils.ts b/platform/frontend/src/app/mcp-catalog/_parts/mcp-catalog-form.utils.ts index ba45b00a65..49f2b32636 100644 --- a/platform/frontend/src/app/mcp-catalog/_parts/mcp-catalog-form.utils.ts +++ b/platform/frontend/src/app/mcp-catalog/_parts/mcp-catalog-form.utils.ts @@ -116,7 +116,7 @@ export function transformFormToApiData( }, }; // Clear oauthConfig when using Bearer Token - data.oauthConfig = undefined; + data.oauthConfig = null; } else if (values.authMethod === "raw_token") { // Handle Token (no prefix) configuration data.userConfig = { @@ -129,11 +129,11 @@ export function transformFormToApiData( }, }; // Clear oauthConfig when using Token - data.oauthConfig = undefined; + data.oauthConfig = null; } else { // No authentication - clear both configs data.userConfig = {}; - data.oauthConfig = undefined; + data.oauthConfig = null; } // Handle labels