diff --git a/packages/@n8n/api-types/src/frontend-settings.ts b/packages/@n8n/api-types/src/frontend-settings.ts index 1d78980b335..0c11a626b69 100644 --- a/packages/@n8n/api-types/src/frontend-settings.ts +++ b/packages/@n8n/api-types/src/frontend-settings.ts @@ -288,6 +288,8 @@ export type FrontendModuleSettings = { mcpAccessEnabled: boolean; /** Whether MCP settings are managed via environment variables. */ mcpManagedByEnv: boolean; + /** Public URL of the instance MCP server endpoint. */ + serverUrl?: string; }; /** diff --git a/packages/cli/src/modules/mcp/__tests__/mcp-protected-resource.test.ts b/packages/cli/src/modules/mcp/__tests__/mcp-protected-resource.test.ts index 23f87fd6ced..8abc29842d2 100644 --- a/packages/cli/src/modules/mcp/__tests__/mcp-protected-resource.test.ts +++ b/packages/cli/src/modules/mcp/__tests__/mcp-protected-resource.test.ts @@ -1,5 +1,6 @@ import { mock } from 'vitest-mock-extended'; +import type { McpConfig } from '../mcp.config'; import type { McpSettingsService } from '../mcp.settings.service'; import type { UrlService } from '@/services/url.service'; @@ -8,10 +9,12 @@ import { McpProtectedResource } from '../mcp-protected-resource'; describe('McpProtectedResource', () => { const urlService = mock(); const mcpSettingsService = mock(); - const resource = new McpProtectedResource(urlService, mcpSettingsService); + const mcpConfig = mock(); + const resource = new McpProtectedResource(urlService, mcpSettingsService, mcpConfig); beforeEach(() => { vi.clearAllMocks(); + mcpConfig.baseUrl = ''; }); describe('getResourceUrl', () => { @@ -53,4 +56,35 @@ describe('McpProtectedResource', () => { it('should be the default audience for resource-less token requests', () => { expect(resource.isDefault).toBe(true); }); + + describe('with a dedicated MCP base URL (split-hostname deployments)', () => { + beforeEach(() => { + urlService.getInstanceBaseUrl.mockReturnValue('https://n8n.example.com'); + mcpConfig.baseUrl = 'https://n8n-mcp.example.com'; + }); + + it('should use the configured base URL as the canonical resource', () => { + expect(resource.getResourceUrl()).toBe('https://n8n-mcp.example.com/mcp-server/http'); + }); + + it('should keep serving the instance-base-URL-derived resource', () => { + expect(resource.getResourceUrls()).toEqual([ + 'https://n8n-mcp.example.com/mcp-server/http', + 'https://n8n.example.com/mcp-server/http', + ]); + }); + + it('should accept audiences for both resource URLs plus the legacy audience', () => { + expect(resource.getAudiences()).toEqual([ + 'https://n8n-mcp.example.com/mcp-server/http', + 'https://n8n.example.com/mcp-server/http', + 'mcp-server-api', + ]); + }); + + it('should collapse to a single resource URL when unset', () => { + mcpConfig.baseUrl = ''; + expect(resource.getResourceUrls()).toEqual(['https://n8n.example.com/mcp-server/http']); + }); + }); }); diff --git a/packages/cli/src/modules/mcp/__tests__/mcp.config.test.ts b/packages/cli/src/modules/mcp/__tests__/mcp.config.test.ts index c982a44f24e..c6cb6adb228 100644 --- a/packages/cli/src/modules/mcp/__tests__/mcp.config.test.ts +++ b/packages/cli/src/modules/mcp/__tests__/mcp.config.test.ts @@ -9,6 +9,7 @@ describe('McpConfig', () => { afterEach(() => { delete process.env.N8N_MCP_SERVER_RATE_LIMIT; + delete process.env.N8N_MCP_BASE_URL; }); it('applies the documented default limit', () => { @@ -43,4 +44,31 @@ describe('McpConfig', () => { expect(config.rateLimitServer).toBe(100); }, ); + + describe('baseUrl', () => { + it('defaults to unset', () => { + expect(Container.get(McpConfig).baseUrl).toBe(''); + }); + + it('reads and normalizes the URL from its environment variable', () => { + process.env.N8N_MCP_BASE_URL = 'https://n8n-mcp.example.com/'; + + expect(Container.get(McpConfig).baseUrl).toBe('https://n8n-mcp.example.com'); + }); + + it('preserves a subpath while stripping query and fragment', () => { + process.env.N8N_MCP_BASE_URL = 'https://example.com/n8n/?foo=1#bar'; + + expect(Container.get(McpConfig).baseUrl).toBe('https://example.com/n8n'); + }); + + it.each(['not-a-url', 'ftp://example.com', '//half-a-url'])( + 'falls back to unset when given an invalid value (%s)', + (value) => { + process.env.N8N_MCP_BASE_URL = value; + + expect(Container.get(McpConfig).baseUrl).toBe(''); + }, + ); + }); }); diff --git a/packages/cli/src/modules/mcp/mcp-protected-resource.ts b/packages/cli/src/modules/mcp/mcp-protected-resource.ts index 0cfef583c53..2088eabb2ad 100644 --- a/packages/cli/src/modules/mcp/mcp-protected-resource.ts +++ b/packages/cli/src/modules/mcp/mcp-protected-resource.ts @@ -1,5 +1,6 @@ import { Service } from '@n8n/di'; +import { McpConfig } from './mcp.config'; import { McpSettingsService } from './mcp.settings.service'; import type { ProtectedResource } from '@/services/protected-resource.registry'; import { UrlService } from '@/services/url.service'; @@ -47,17 +48,34 @@ export class McpProtectedResource implements ProtectedResource { constructor( private readonly urlService: UrlService, private readonly mcpSettingsService: McpSettingsService, + private readonly mcpConfig: McpConfig, ) {} getResourceUrl(): string { + // A dedicated MCP base URL (split-hostname deployments) takes precedence + // as the canonical resource: it is what clients are told to use. + if (this.mcpConfig.baseUrl) { + return `${this.mcpConfig.baseUrl}${MCP_RESOURCE_PATH}`; + } const baseUrl = this.urlService.getInstanceBaseUrl().replace(/\/$/, ''); return `${baseUrl}${MCP_RESOURCE_PATH}`; } + /** + * Canonical resource URL first, then the instance-base-URL-derived one when + * a dedicated MCP base URL is configured — clients connecting through the + * main hostname (and tokens minted before the config change) must keep + * working. + */ + getResourceUrls(): string[] { + const instanceUrl = `${this.urlService.getInstanceBaseUrl().replace(/\/$/, '')}${MCP_RESOURCE_PATH}`; + return [...new Set([this.getResourceUrl(), instanceUrl])]; + } + getAudiences(): string[] { // The legacy audience stays scoped to this resource only — it must never // be accepted at another protected resource's gate. - return [this.getResourceUrl(), LEGACY_MCP_AUDIENCE]; + return [...this.getResourceUrls(), LEGACY_MCP_AUDIENCE]; } async getAllowedRedirectUris(): Promise { diff --git a/packages/cli/src/modules/mcp/mcp.config.ts b/packages/cli/src/modules/mcp/mcp.config.ts index f1ab1d8f2fb..e1f23fdd6d6 100644 --- a/packages/cli/src/modules/mcp/mcp.config.ts +++ b/packages/cli/src/modules/mcp/mcp.config.ts @@ -1,6 +1,25 @@ import { Config, Env } from '@n8n/config'; import { z } from 'zod'; +/** Normalized to origin + path without a trailing slash; query and fragment are dropped. */ +const baseUrlSchema = z + .string() + .refine( + (value) => { + try { + const url = new URL(value); + return url.protocol === 'http:' || url.protocol === 'https:'; + } catch { + return false; + } + }, + { message: 'Must be a valid http(s) URL' }, + ) + .transform((value) => { + const url = new URL(value); + return `${url.origin}${url.pathname}`.replace(/\/$/, ''); + }); + /** Configuration for the instance MCP server module. */ @Config export class McpConfig { @@ -10,4 +29,19 @@ export class McpConfig { */ @Env('N8N_MCP_SERVER_RATE_LIMIT', z.number({ coerce: true }).int().nonnegative()) rateLimitServer: number = 100; + + /** + * Public base URL at which MCP clients reach this instance's MCP server, + * when it differs from the instance base URL — for split-hostname + * deployments that front the same backend with a dedicated MCP hostname + * (e.g. `https://n8n-mcp.example.com` while `N8N_EDITOR_BASE_URL` stays on + * the main UI hostname). When set, it becomes the canonical MCP resource + * URL: advertised in discovery, accepted as an RFC 8707 resource indicator, + * and used as the access-token audience. The instance-base-URL-derived + * resource remains accepted so existing clients keep working. + * + * @example N8N_MCP_BASE_URL=https://n8n-mcp.example.com + */ + @Env('N8N_MCP_BASE_URL', baseUrlSchema) + baseUrl: string = ''; } diff --git a/packages/cli/src/modules/mcp/mcp.module.ts b/packages/cli/src/modules/mcp/mcp.module.ts index 90c9e4612f6..b030fcac0d8 100644 --- a/packages/cli/src/modules/mcp/mcp.module.ts +++ b/packages/cli/src/modules/mcp/mcp.module.ts @@ -26,13 +26,18 @@ export class McpModule implements ModuleInterface { /** * Settings exposed to the frontend under `/rest/module-settings`. * - * The response shape will be `{ mcp: { mcpAccessEnabled: boolean, mcpManagedByEnv: boolean } }`. + * The response shape will be + * `{ mcp: { mcpAccessEnabled: boolean, mcpManagedByEnv: boolean, serverUrl: string } }`. */ async settings() { const { McpSettingsService } = await import('./mcp.settings.service'); + const { McpProtectedResource } = await import('./mcp-protected-resource'); const mcpAccessEnabled = await Container.get(McpSettingsService).getEnabled(); const { mcpManagedByEnv } = Container.get(InstanceSettingsLoaderConfig); - return { mcpAccessEnabled, mcpManagedByEnv }; + // Canonical resource URL, so the UI shows the URL clients must actually + // use when a dedicated MCP base URL is configured. + const serverUrl = Container.get(McpProtectedResource).getResourceUrl(); + return { mcpAccessEnabled, mcpManagedByEnv, serverUrl }; } @OnShutdown() diff --git a/packages/cli/src/modules/oauth-server/__tests__/oauth-server.service.test.ts b/packages/cli/src/modules/oauth-server/__tests__/oauth-server.service.test.ts index 0768622c029..2ca68f6e224 100644 --- a/packages/cli/src/modules/oauth-server/__tests__/oauth-server.service.test.ts +++ b/packages/cli/src/modules/oauth-server/__tests__/oauth-server.service.test.ts @@ -17,7 +17,11 @@ import { OAuthAuthorizationCodeService } from '../oauth-authorization-code.servi import { OAuthServerService } from '../oauth-server.service'; import { OAuthSessionService } from '../oauth-session.service'; import { OAuthTokenService } from '../oauth-token.service'; +import { McpProtectedResource } from '@/modules/mcp/mcp-protected-resource'; +import type { McpConfig } from '@/modules/mcp/mcp.config'; +import type { McpSettingsService } from '@/modules/mcp/mcp.settings.service'; import { ProtectedResourceRegistry } from '@/services/protected-resource.registry'; +import type { UrlService } from '@/services/url.service'; const SUPPORTED_SCOPES = ['tool:listWorkflows', 'tool:getWorkflowDetails']; const TEST_RESOURCE_URL = 'https://n8n.example.com/mcp-server/http'; @@ -1085,4 +1089,62 @@ describe('OAuthServerService', () => { ).rejects.toThrow(InvalidTargetError); }); }); + + // Chain test with the real MCP protected resource: the resource generated + // from the configured MCP base URL must pass indicator validation. + describe('resource indicator validation with a configured MCP base URL', () => { + const makeConfiguredService = () => { + const urlService = mock(); + urlService.getInstanceBaseUrl.mockReturnValue('https://n8n.example.com'); + const mcpConfig = mock(); + mcpConfig.baseUrl = 'https://n8n-mcp.example.com'; + const mcpResource = new McpProtectedResource( + urlService, + mock(), + mcpConfig, + ); + expect(mcpResource.getResourceUrl()).toBe('https://n8n-mcp.example.com/mcp-server/http'); + + const configuredRegistry = new ProtectedResourceRegistry(mock()); + configuredRegistry.register(mcpResource); + + return new OAuthServerService( + logger, + mockInstance(GlobalConfig), + oauthSessionService, + oauthClientRepository, + tokenService, + authorizationCodeService, + userConsentRepository, + configuredRegistry, + ); + }; + + it('should accept the resource generated from the configured base URL', async () => { + const configuredService = makeConfiguredService(); + expect( + await (configuredService as any).resolveAndValidateResourceIndicator( + 'https://n8n-mcp.example.com/mcp-server/http', + ), + ).toBe('https://n8n-mcp.example.com/mcp-server/http'); + }); + + it('should keep accepting the instance-base-URL-derived resource', async () => { + const configuredService = makeConfiguredService(); + expect( + await (configuredService as any).resolveAndValidateResourceIndicator( + 'https://n8n.example.com/mcp-server/http', + ), + ).toBe('https://n8n.example.com/mcp-server/http'); + }); + + it('should reject resources on hosts that are not configured', async () => { + const configuredService = makeConfiguredService(); + await expect( + (configuredService as any).resolveAndValidateResourceIndicator( + 'https://other.example.com/mcp-server/http', + ), + ).rejects.toThrow(InvalidTargetError); + }); + }); }); diff --git a/packages/cli/src/modules/oauth-server/__tests__/oauth-token.service.test.ts b/packages/cli/src/modules/oauth-server/__tests__/oauth-token.service.test.ts index ca2c9f958fe..ca0ba341508 100644 --- a/packages/cli/src/modules/oauth-server/__tests__/oauth-token.service.test.ts +++ b/packages/cli/src/modules/oauth-server/__tests__/oauth-token.service.test.ts @@ -13,7 +13,11 @@ import type { RefreshToken } from '../database/entities/oauth-refresh-token.enti import { AccessTokenRepository } from '../database/repositories/oauth-access-token.repository'; import { RefreshTokenRepository } from '../database/repositories/oauth-refresh-token.repository'; import { OAuthTokenService } from '../oauth-token.service'; +import { McpProtectedResource } from '@/modules/mcp/mcp-protected-resource'; +import type { McpConfig } from '@/modules/mcp/mcp.config'; +import type { McpSettingsService } from '@/modules/mcp/mcp.settings.service'; import { ProtectedResourceRegistry } from '@/services/protected-resource.registry'; +import type { UrlService } from '@/services/url.service'; const instanceSettings = mock({ encryptionKey: 'test-key' }); const jwtService = new JwtService(instanceSettings, mock()); @@ -593,4 +597,68 @@ describe('OAuthTokenService', () => { ).resolves.toMatchObject({ clientId: 'client-456' }); }); }); + + // Chain test with the real MCP protected resource, matching the middleware's + // gate (expectedAudience = getResourceUrl(), which is derived from the + // configured MCP base URL when set). + describe('audience gate with a configured MCP base URL', () => { + const CONFIGURED_RESOURCE_URL = 'https://n8n-mcp.example.com/mcp-server/http'; + + let configuredService: OAuthTokenService; + + beforeAll(() => { + const urlService = mock(); + urlService.getInstanceBaseUrl.mockReturnValue(TEST_BASE_URL); + const mcpConfig = mock(); + mcpConfig.baseUrl = 'https://n8n-mcp.example.com'; + const mcpResource = new McpProtectedResource( + urlService, + mock(), + mcpConfig, + ); + + const configuredRegistry = new ProtectedResourceRegistry(mock()); + configuredRegistry.register(mcpResource); + + configuredService = new OAuthTokenService( + logger, + jwtService, + userRepository, + accessTokenRepository, + refreshTokenRepository, + configuredRegistry, + ); + }); + + it.each([ + ['the configured resource URL', CONFIGURED_RESOURCE_URL], + ['the instance-base-URL-derived resource URL', TEST_RESOURCE_URL], + ['the legacy audience', LEGACY_AUDIENCE], + ])('should accept a token whose aud is %s', async (_, audience) => { + const token = jwtService.sign({ + sub: 'user-123', + aud: audience, + client_id: 'client-456', + }); + accessTokenRepository.findOne.mockResolvedValue( + mock({ token, clientId: 'client-456', userId: 'user-123' }), + ); + + await expect( + configuredService.verifyAccessToken(token, CONFIGURED_RESOURCE_URL), + ).resolves.toMatchObject({ clientId: 'client-456' }); + }); + + it('should reject a token whose aud is an unconfigured host', async () => { + const token = jwtService.sign({ + sub: 'user-123', + aud: 'https://other.example.com/mcp-server/http', + client_id: 'client-456', + }); + + await expect( + configuredService.verifyAccessToken(token, CONFIGURED_RESOURCE_URL), + ).rejects.toThrow('JWT Verification Failed'); + }); + }); }); diff --git a/packages/cli/src/services/__tests__/protected-resource.registry.test.ts b/packages/cli/src/services/__tests__/protected-resource.registry.test.ts index b59c10ccbbc..680de6c227c 100644 --- a/packages/cli/src/services/__tests__/protected-resource.registry.test.ts +++ b/packages/cli/src/services/__tests__/protected-resource.registry.test.ts @@ -37,6 +37,36 @@ describe('ProtectedResourceRegistry', () => { expect(registry.getById('unknown')).toBeUndefined(); }); + it('should resolve a resource by any of its declared resource URLs', async () => { + const multiUrlResource: ProtectedResource = { + id: 'instance-mcp-multi', + getResourceUrl: () => 'https://n8n-mcp.example.com/mcp-server/http', + getResourceUrls: () => [ + 'https://n8n-mcp.example.com/mcp-server/http', + 'https://n8n.example.com/mcp-server/http', + ], + getAudiences: () => [ + 'https://n8n-mcp.example.com/mcp-server/http', + 'https://n8n.example.com/mcp-server/http', + ], + authorize: async () => true, + scopes: [], + }; + const multiRegistry = new ProtectedResourceRegistry(mock()); + multiRegistry.register(multiUrlResource); + + expect( + await multiRegistry.getByResourceUrl('https://n8n-mcp.example.com/mcp-server/http'), + ).toBe(multiUrlResource); + expect(await multiRegistry.getByResourceUrl('https://n8n.example.com/mcp-server/http/')).toBe( + multiUrlResource, + ); + expect( + await multiRegistry.getByResourceUrl('https://other.example.com/mcp-server/http'), + ).toBeUndefined(); + expect(await multiRegistry.getByResourcePath('/mcp-server/http')).toBe(multiUrlResource); + }); + it('should resolve resources by resource URL, ignoring trailing slashes', async () => { expect(await registry.getByResourceUrl('https://n8n.example.com/mcp-server/http')).toBe( resourceA, diff --git a/packages/cli/src/services/protected-resource.registry.ts b/packages/cli/src/services/protected-resource.registry.ts index 3628b6fc393..8334a55b0db 100644 --- a/packages/cli/src/services/protected-resource.registry.ts +++ b/packages/cli/src/services/protected-resource.registry.ts @@ -26,6 +26,15 @@ export interface ProtectedResource { */ getResourceUrl(): string; + /** + * Every resource URL this resource is served at, canonical URL + * (`getResourceUrl()`) first. A configured MCP base URL becomes the + * canonical entry, and the instance-base-URL-derived resource follows so + * clients using the instance hostname keep working. Treated as + * `[getResourceUrl()]` when not implemented. + */ + getResourceUrls?(): string[]; + /** * All `aud` values accepted at this resource's gate. Must include the * canonical resource URL; may include resource-specific legacy audiences. @@ -96,6 +105,9 @@ export interface ProtectedResourceResolver { const trimTrailingSlash = (url: string): string => url.replace(/\/$/, ''); +const getResourceUrls = (resource: ProtectedResource): string[] => + resource.getResourceUrls?.() ?? [resource.getResourceUrl()]; + /** * Registry of the protected resources served by this instance's shared OAuth * server. Modules register their resources on init (e.g. instance MCP @@ -121,11 +133,13 @@ export class ProtectedResourceRegistry { return this.resources.get(id); } - /** Look up a resource by its canonical URL (trailing slashes ignored). */ + /** Look up a resource by any of its resource URLs (trailing slashes ignored). */ async getByResourceUrl(resourceUrl: string): Promise { const normalized = trimTrailingSlash(resourceUrl); for (const resource of this.resources.values()) { - if (trimTrailingSlash(resource.getResourceUrl()) === normalized) return resource; + if (getResourceUrls(resource).some((url) => trimTrailingSlash(url) === normalized)) { + return resource; + } } for (const resolver of this.resolvers) { try { @@ -142,12 +156,14 @@ export class ProtectedResourceRegistry { async getByResourcePath(pathname: string): Promise { const normalized = trimTrailingSlash(pathname); for (const resource of this.resources.values()) { - try { - if (trimTrailingSlash(new URL(resource.getResourceUrl()).pathname) === normalized) { - return resource; + for (const url of getResourceUrls(resource)) { + try { + if (trimTrailingSlash(new URL(url).pathname) === normalized) { + return resource; + } + } catch { + continue; } - } catch { - continue; } } diff --git a/packages/frontend/editor-ui/src/experiments/surfaceMcpToNewCloudUsers/components/onboarding/MCPOnboardingModal.test.ts b/packages/frontend/editor-ui/src/experiments/surfaceMcpToNewCloudUsers/components/onboarding/MCPOnboardingModal.test.ts index 52b115290c9..0e2a9c78f11 100644 --- a/packages/frontend/editor-ui/src/experiments/surfaceMcpToNewCloudUsers/components/onboarding/MCPOnboardingModal.test.ts +++ b/packages/frontend/editor-ui/src/experiments/surfaceMcpToNewCloudUsers/components/onboarding/MCPOnboardingModal.test.ts @@ -46,6 +46,7 @@ vi.mock('@/experiments/surfaceMcpToNewCloudUsers/stores/surfaceMcpToNewCloudUser type MockMcpStore = { mcpAccessEnabled: boolean; mcpManagedByEnv: boolean; + serverUrl: string; setMcpAccessEnabled: ReturnType; }; @@ -94,6 +95,7 @@ describe('MCPOnboardingModal', () => { mockMcpStore = reactive({ mcpAccessEnabled: false, mcpManagedByEnv: false, + serverUrl: 'https://example.n8n.cloud/mcp-server/http', setMcpAccessEnabled: vi.fn().mockImplementation(async () => { mockMcpStore.mcpAccessEnabled = true; return true; diff --git a/packages/frontend/editor-ui/src/experiments/surfaceMcpToNewCloudUsers/components/onboarding/MCPOnboardingModal.vue b/packages/frontend/editor-ui/src/experiments/surfaceMcpToNewCloudUsers/components/onboarding/MCPOnboardingModal.vue index 33860d30474..6d9b0cbf9fe 100644 --- a/packages/frontend/editor-ui/src/experiments/surfaceMcpToNewCloudUsers/components/onboarding/MCPOnboardingModal.vue +++ b/packages/frontend/editor-ui/src/experiments/surfaceMcpToNewCloudUsers/components/onboarding/MCPOnboardingModal.vue @@ -5,12 +5,11 @@ import SurfaceMcpBridgeGraphic from '@/experiments/surfaceMcpToNewCloudUsers/com import { SURFACE_MCP_ONBOARDING_MODAL_KEY } from '@/experiments/surfaceMcpToNewCloudUsers/constants'; import { useSurfaceMcpToNewCloudUsersStore } from '@/experiments/surfaceMcpToNewCloudUsers/stores/surfaceMcpToNewCloudUsers.store'; import MCPAccessToggle from '@/features/ai/mcpAccess/components/header/McpAccessToggle.vue'; -import { MCP_ENDPOINT, MCP_SETTINGS_VIEW } from '@/features/ai/mcpAccess/mcp.constants'; +import { MCP_SETTINGS_VIEW } from '@/features/ai/mcpAccess/mcp.constants'; import { useMCPStore } from '@/features/ai/mcpAccess/mcp.store'; import { N8nIcon, N8nLink, N8nText } from '@n8n/design-system'; import { useI18n } from '@n8n/i18n'; import type { BaseTextKey } from '@n8n/i18n'; -import { useRootStore } from '@n8n/stores/useRootStore'; import { createEventBus } from '@n8n/utils/event-bus'; import { computed, onBeforeUnmount, onMounted, ref } from 'vue'; import { I18nT } from 'vue-i18n'; @@ -35,7 +34,6 @@ const props = defineProps<{ const i18n = useI18n(); const toast = useToast(); -const rootStore = useRootStore(); const mcpStore = useMCPStore(); const experimentStore = useSurfaceMcpToNewCloudUsersStore(); const modalBus = createEventBus(); @@ -81,7 +79,7 @@ const clientOptions = computed(() => [ }, ]); -const serverUrl = computed(() => `${rootStore.urlBaseEditor}${MCP_ENDPOINT}`); +const serverUrl = computed(() => mcpStore.serverUrl); const isChatGptClient = computed(() => activeClient.value === 'chatgpt'); const showServerUrlStep = computed(() => activeClient.value === 'claude'); const showRestartStep = computed( diff --git a/packages/frontend/editor-ui/src/features/ai/mcpAccess/components/header/connectPopover/McpConnectPopover.vue b/packages/frontend/editor-ui/src/features/ai/mcpAccess/components/header/connectPopover/McpConnectPopover.vue index bbfe2c8bcaf..3eb3d8c1fd9 100644 --- a/packages/frontend/editor-ui/src/features/ai/mcpAccess/components/header/connectPopover/McpConnectPopover.vue +++ b/packages/frontend/editor-ui/src/features/ai/mcpAccess/components/header/connectPopover/McpConnectPopover.vue @@ -1,17 +1,15 @@