Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 71 additions & 0 deletions platform/backend/src/routes/internal-mcp-catalog.env.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
// =========================================================================
Expand Down
6 changes: 6 additions & 0 deletions platform/backend/src/routes/internal-mcp-catalog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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", () => {
Expand Down Expand Up @@ -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();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand All @@ -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
Expand Down