mirror of
https://github.com/n8n-io/n8n.git
synced 2026-05-30 16:26:59 +02:00
fix(editor): Prioritize MCP tile in empty state (no-changelog) (#30418)
Some checks are pending
CI: Master (Build, Test, Lint) / Build for Github Cache (push) Waiting to run
CI: Master (Build, Test, Lint) / Unit tests (22.x) (push) Waiting to run
CI: Master (Build, Test, Lint) / Unit tests (24.14.1) (push) Waiting to run
CI: Master (Build, Test, Lint) / Unit tests (25.x) (push) Waiting to run
CI: Master (Build, Test, Lint) / Lint (push) Waiting to run
CI: Master (Build, Test, Lint) / Performance (push) Waiting to run
CI: Master (Build, Test, Lint) / Notify Slack on failure (push) Blocked by required conditions
Some checks are pending
CI: Master (Build, Test, Lint) / Build for Github Cache (push) Waiting to run
CI: Master (Build, Test, Lint) / Unit tests (22.x) (push) Waiting to run
CI: Master (Build, Test, Lint) / Unit tests (24.14.1) (push) Waiting to run
CI: Master (Build, Test, Lint) / Unit tests (25.x) (push) Waiting to run
CI: Master (Build, Test, Lint) / Lint (push) Waiting to run
CI: Master (Build, Test, Lint) / Performance (push) Waiting to run
CI: Master (Build, Test, Lint) / Notify Slack on failure (push) Blocked by required conditions
Co-authored-by: Codex <noreply@openai.com> Co-authored-by: OpenCode <noreply@opencode.ai>
This commit is contained in:
parent
c3e39f8504
commit
ceb561b87b
|
|
@ -2946,14 +2946,15 @@
|
|||
"experiments.surfaceMcpToNewCloudUsers.onboarding.intro.settingsLink": "Settings > Instance-level MCP",
|
||||
"experiments.surfaceMcpToNewCloudUsers.onboarding.intro.tryIt": "Try MCP",
|
||||
"experiments.surfaceMcpToNewCloudUsers.onboarding.intro.skip": "Skip for now",
|
||||
"experiments.surfaceMcpToNewCloudUsers.onboarding.prompt.claude": "Find the official n8n connector and show it in this chat.",
|
||||
"experiments.surfaceMcpToNewCloudUsers.onboarding.prompt.claudeCode": "Set up the n8n MCP server for Claude Code globally (available in all projects).\n\n1. Register the MCP server at user scope by running:\n\n claude mcp add --scope user --transport http n8n \"{serverUrl}\"\n\n2. When Claude Code asks you to authenticate, complete the n8n OAuth flow.\n\n3. When you finish, tell me to restart Claude Code. Don't try to verify the server in this session.",
|
||||
"experiments.surfaceMcpToNewCloudUsers.onboarding.prompt.cursor": "Set up the n8n MCP server for Cursor globally.\n\n1. Add the following to ~/.cursor/mcp.json (create the file and directory if they do not exist; if the file already exists, merge the n8n server entry without removing existing config):\n\n {'{'}\n \"mcpServers\": {'{'}\n \"n8n\": {'{'}\n \"url\": \"{serverUrl}\"\n {'}'}\n {'}'}\n {'}'}\n\n2. When Cursor asks you to authenticate, complete the n8n OAuth flow.\n\n3. When you finish, tell me to restart Cursor. Don't try to verify the server in this session.",
|
||||
"experiments.surfaceMcpToNewCloudUsers.onboarding.prompt.codex": "Set up the n8n MCP server for Codex globally.\n\n1. Add the following to ~/.codex/config.toml (create the file and directory if they do not exist; if the file already exists, append the section without removing existing config):\n\n [mcp_servers.n8n]\n url = \"{serverUrl}\"\n\n2. When Codex asks you to authenticate, complete the n8n OAuth flow.\n\n3. When you finish, tell me to restart Codex. Don't try to verify the server in this session.",
|
||||
"experiments.surfaceMcpToNewCloudUsers.onboarding.prompt.claude": "Show me the n8n MCP connector and walk me through enabling it if it isn't already on. I'll paste my server URL when you tell me where it goes.",
|
||||
"experiments.surfaceMcpToNewCloudUsers.onboarding.prompt.claudeCode": "Set up the n8n MCP server for Claude Code so it's available across all projects.\n\n1. Register the server at user scope by running:\n\n claude mcp add --scope user --transport http n8n \"{serverUrl}\"\n\n2. Once it's registered, remind me to restart Claude Code — I'll complete the n8n OAuth flow when prompted in the new session, so don't try to list or call its tools yet.",
|
||||
"experiments.surfaceMcpToNewCloudUsers.onboarding.prompt.cursor": "Set up the n8n MCP server for Cursor across all projects.\n\n1. Open ~/.cursor/mcp.json (create the file and directory if they don't exist). Add the entry below, merging with any existing config so other servers stay intact:\n\n {'{'}\n \"mcpServers\": {'{'}\n \"n8n\": {'{'}\n \"url\": \"{serverUrl}\"\n {'}'}\n {'}'}\n {'}'}\n\n2. Once it's saved, remind me to restart Cursor and complete the n8n OAuth flow when it prompts me — the server's tools won't be available until then.",
|
||||
"experiments.surfaceMcpToNewCloudUsers.onboarding.prompt.codex": "Set up the n8n MCP server for Codex across all projects.\n\n1. Open ~/.codex/config.toml (create the file and directory if they don't exist). Append the section below, leaving any existing config in place:\n\n [mcp_servers.n8n]\n url = \"{serverUrl}\"\n\n2. Once it's saved, remind me to restart Codex and complete the n8n OAuth flow in my browser when it prompts me — the server's tools won't be available until then.",
|
||||
"experiments.surfaceMcpToNewCloudUsers.onboarding.section.client.title": "Choose your assistant",
|
||||
"experiments.surfaceMcpToNewCloudUsers.onboarding.section.access.title": "Enable MCP access",
|
||||
"experiments.surfaceMcpToNewCloudUsers.onboarding.section.prompt.title": "Paste the prompt in {assistant}",
|
||||
"experiments.surfaceMcpToNewCloudUsers.onboarding.section.serverUrl.title": "Paste Server URL",
|
||||
"experiments.surfaceMcpToNewCloudUsers.onboarding.section.restart.title": "Restart {assistant} and connect to n8n",
|
||||
"experiments.surfaceMcpToNewCloudUsers.onboarding.section.chatgptDeveloperMode.title": "Enable developer mode in ChatGPT",
|
||||
"experiments.surfaceMcpToNewCloudUsers.onboarding.section.chatgptDeveloperMode.description": "In ChatGPT, go to Settings > Apps & Connectors > Advanced settings. Turn on developer mode.",
|
||||
"experiments.surfaceMcpToNewCloudUsers.onboarding.section.chatgptCustomApp.title": "Create a custom app",
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import { useUsersStore } from '@/features/settings/users/users.store';
|
|||
import { useProjectsStore } from '@/features/collaboration/projects/projects.store';
|
||||
import { useSourceControlStore } from '@/features/integrations/sourceControl.ee/sourceControl.store';
|
||||
import { useRecommendedTemplatesStore } from '@/features/workflows/templates/recommendations/recommendedTemplates.store';
|
||||
import { useReadyToRunStore } from '@/features/workflows/readyToRun/stores/readyToRun.store';
|
||||
import { useBannersStore } from '@/features/shared/banners/banners.store';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import type { IUser } from '@n8n/rest-api-client/api/users';
|
||||
|
|
@ -28,9 +29,15 @@ vi.mock('vue-router', () => ({
|
|||
},
|
||||
}));
|
||||
|
||||
vi.mock('@/experiments/surfaceMcpToNewCloudUsers/composables/useSurfaceMcpEmptyState', () => ({
|
||||
useSurfaceMcpEmptyState: vi.fn(() => surfaceMcpEmptyState),
|
||||
}));
|
||||
vi.mock('@/experiments/surfaceMcpToNewCloudUsers/composables/useSurfaceMcpEmptyState', async () => {
|
||||
const { computed } = await import('vue');
|
||||
return {
|
||||
useSurfaceMcpEmptyState: vi.fn(() => ({
|
||||
showTile: computed(() => surfaceMcpEmptyState.showTile),
|
||||
showReminder: computed(() => surfaceMcpEmptyState.showReminder),
|
||||
})),
|
||||
};
|
||||
});
|
||||
|
||||
const renderComponent = createComponentRenderer(EmptyStateLayout, {
|
||||
pinia: createTestingPinia(),
|
||||
|
|
@ -59,6 +66,7 @@ describe('EmptyStateLayout', () => {
|
|||
let recommendedTemplatesStore: ReturnType<
|
||||
typeof mockedStore<typeof useRecommendedTemplatesStore>
|
||||
>;
|
||||
let readyToRunStore: ReturnType<typeof mockedStore<typeof useReadyToRunStore>>;
|
||||
let bannersStore: ReturnType<typeof mockedStore<typeof useBannersStore>>;
|
||||
|
||||
beforeEach(() => {
|
||||
|
|
@ -66,6 +74,7 @@ describe('EmptyStateLayout', () => {
|
|||
projectsStore = mockedStore(useProjectsStore);
|
||||
sourceControlStore = mockedStore(useSourceControlStore);
|
||||
recommendedTemplatesStore = mockedStore(useRecommendedTemplatesStore);
|
||||
readyToRunStore = mockedStore(useReadyToRunStore);
|
||||
bannersStore = mockedStore(useBannersStore);
|
||||
|
||||
usersStore.currentUser = {
|
||||
|
|
@ -91,6 +100,7 @@ describe('EmptyStateLayout', () => {
|
|||
} as unknown as ReturnType<typeof useSourceControlStore>['preferences'];
|
||||
|
||||
bannersStore.bannersHeight = 0;
|
||||
readyToRunStore.userCanClaimOpenAiCredits = false;
|
||||
surfaceMcpEmptyState.showTile = false;
|
||||
surfaceMcpEmptyState.showReminder = false;
|
||||
|
||||
|
|
@ -193,6 +203,25 @@ describe('EmptyStateLayout', () => {
|
|||
expect(getByTestId('mcp-onboarding-reminder')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render ready-to-run card when user can claim OpenAI credits and MCP tile is hidden', () => {
|
||||
readyToRunStore.userCanClaimOpenAiCredits = true;
|
||||
|
||||
const { getByTestId } = renderComponent();
|
||||
|
||||
expect(getByTestId('ready-to-run-card')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should hide ready-to-run card when Surface MCP tile is shown', () => {
|
||||
readyToRunStore.userCanClaimOpenAiCredits = true;
|
||||
surfaceMcpEmptyState.showTile = true;
|
||||
|
||||
const { queryByTestId, getByTestId } = renderComponent();
|
||||
|
||||
expect(queryByTestId('ready-to-run-card')).not.toBeInTheDocument();
|
||||
expect(getByTestId('mcp-onboarding-card')).toBeInTheDocument();
|
||||
expect(getByTestId('new-workflow-card')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should emit click:add event when workflow card is clicked', async () => {
|
||||
const { getByTestId, emitted } = renderComponent();
|
||||
|
||||
|
|
|
|||
|
|
@ -54,7 +54,7 @@ const addWorkflow = () => {
|
|||
|
||||
// Check if user can claim credits for ready-to-run
|
||||
const showReadyToRunCard = computed(() => {
|
||||
return readyToRunStore.userCanClaimOpenAiCredits && canCreateWorkflow.value;
|
||||
return readyToRunStore.userCanClaimOpenAiCredits && canCreateWorkflow.value && !showMcpTile.value;
|
||||
});
|
||||
|
||||
const handleReadyToRunClick = async () => {
|
||||
|
|
@ -176,7 +176,6 @@ const handleAppSelectionContinue = () => {
|
|||
$style.actionCardsContainer,
|
||||
{
|
||||
[$style.singleCard]: !showReadyToRunCard && !showMcpTile,
|
||||
[$style.threeCards]: showReadyToRunCard && showMcpTile,
|
||||
},
|
||||
]"
|
||||
>
|
||||
|
|
@ -297,10 +296,6 @@ const handleAppSelectionContinue = () => {
|
|||
grid-template-columns: 192px;
|
||||
}
|
||||
|
||||
&.threeCards {
|
||||
grid-template-columns: repeat(3, 192px);
|
||||
}
|
||||
|
||||
@media (max-width: vars.$breakpoint-xs) {
|
||||
grid-template-columns: 1fr;
|
||||
gap: var(--spacing--md);
|
||||
|
|
|
|||
|
|
@ -40,8 +40,8 @@ describe('MCPOnboardingClientSetup', () => {
|
|||
});
|
||||
const text = container.textContent ?? '';
|
||||
|
||||
expect(text).toContain('Find the official n8n connector and show it in this chat.');
|
||||
expect(text).not.toContain("When it's ready, ask me for the server URL.");
|
||||
expect(text).toContain('Show me the n8n MCP connector');
|
||||
expect(text).toContain("I'll paste my server URL when you tell me where it goes.");
|
||||
expect(text).not.toContain('claude mcp add --scope user --transport http n8n');
|
||||
expect(queryByTestId('mcp-onboarding-claude-server-url')).not.toBeInTheDocument();
|
||||
});
|
||||
|
|
|
|||
|
|
@ -110,7 +110,7 @@ describe('MCPOnboardingModal', () => {
|
|||
|
||||
expect(mockMcpStore.setMcpAccessEnabled).toHaveBeenCalledWith(true);
|
||||
expect(mockExperimentStore.trackEnableClicked).toHaveBeenCalledWith('first_open_modal');
|
||||
expect(await findByText(/Find the official n8n connector/)).toBeInTheDocument();
|
||||
expect(await findByText(/Show me the n8n MCP connector/)).toBeInTheDocument();
|
||||
expect(mockExperimentStore.trackEnabled).toHaveBeenCalledWith('first_open_modal');
|
||||
});
|
||||
|
||||
|
|
@ -124,7 +124,7 @@ describe('MCPOnboardingModal', () => {
|
|||
|
||||
const { getByRole, findByText, queryByTestId } = renderComponent();
|
||||
|
||||
await findByText(/Find the official n8n connector/);
|
||||
await findByText(/Show me the n8n MCP connector/);
|
||||
await user.click(getByRole('switch'));
|
||||
|
||||
expect(mockMcpStore.setMcpAccessEnabled).toHaveBeenCalledWith(false);
|
||||
|
|
@ -152,7 +152,7 @@ describe('MCPOnboardingModal', () => {
|
|||
|
||||
const { findByTestId, findByText, getByTestId } = renderComponent();
|
||||
|
||||
expect(await findByText(/Find the official n8n connector/)).toBeInTheDocument();
|
||||
expect(await findByText(/Show me the n8n MCP connector/)).toBeInTheDocument();
|
||||
expect(await findByText('https://example.n8n.cloud/mcp-server/http')).toBeInTheDocument();
|
||||
expect(await findByText('Paste the prompt in Claude')).toBeInTheDocument();
|
||||
expect(await findByText('Paste Server URL')).toBeInTheDocument();
|
||||
|
|
@ -189,13 +189,13 @@ describe('MCPOnboardingModal', () => {
|
|||
|
||||
const { getByRole, findByText, getByTestId } = renderComponent();
|
||||
|
||||
await findByText(/Find the official n8n connector/);
|
||||
await findByText(/Show me the n8n MCP connector/);
|
||||
await user.click(getByRole('switch'));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockShowError).toHaveBeenCalledWith(error, 'Error updating MCP access');
|
||||
});
|
||||
expect(await findByText(/Find the official n8n connector/)).toBeInTheDocument();
|
||||
expect(await findByText(/Show me the n8n MCP connector/)).toBeInTheDocument();
|
||||
expect(getByTestId('mcp-onboarding-copy-prompt-button')).toBeEnabled();
|
||||
});
|
||||
|
||||
|
|
@ -203,7 +203,7 @@ describe('MCPOnboardingModal', () => {
|
|||
const user = userEvent.setup();
|
||||
mockMcpStore.mcpAccessEnabled = true;
|
||||
|
||||
const { getByText, container } = renderComponent();
|
||||
const { getByText, getByTestId, container } = renderComponent();
|
||||
|
||||
await user.click(getByText('Claude Code'));
|
||||
|
||||
|
|
@ -217,13 +217,16 @@ describe('MCPOnboardingModal', () => {
|
|||
'prompt',
|
||||
);
|
||||
expect(container.textContent).toContain('claude mcp add --scope user --transport http n8n');
|
||||
expect(getByTestId('mcp-onboarding-restart-step')).toHaveTextContent(
|
||||
'Restart Claude Code and connect to n8n',
|
||||
);
|
||||
});
|
||||
|
||||
it('switches between Claude Code and Codex setup instructions', async () => {
|
||||
const user = userEvent.setup();
|
||||
mockMcpStore.mcpAccessEnabled = true;
|
||||
|
||||
const { getByText, queryByTestId, container } = renderComponent();
|
||||
const { getByText, getByTestId, queryByTestId, container } = renderComponent();
|
||||
|
||||
await user.click(getByText('Codex'));
|
||||
|
||||
|
|
@ -234,21 +237,25 @@ describe('MCPOnboardingModal', () => {
|
|||
expect(container.textContent).toContain('Paste the prompt in Codex');
|
||||
expect(container.textContent).toContain('[mcp_servers.n8n]');
|
||||
expect(queryByTestId('mcp-onboarding-claude-server-url')).not.toBeInTheDocument();
|
||||
expect(getByTestId('mcp-onboarding-restart-step')).toHaveTextContent(
|
||||
'Restart Codex and connect to n8n',
|
||||
);
|
||||
});
|
||||
|
||||
it('switches to Claude setup instructions', async () => {
|
||||
const user = userEvent.setup();
|
||||
mockMcpStore.mcpAccessEnabled = true;
|
||||
|
||||
const { getByText, container } = renderComponent();
|
||||
const { getByText, queryByTestId, container } = renderComponent();
|
||||
|
||||
await user.click(getByText('Claude'));
|
||||
|
||||
expect(mockExperimentStore.trackClientSelected).not.toHaveBeenCalled();
|
||||
expect(container.textContent).toContain('Find the official n8n connector');
|
||||
expect(container.textContent).toContain('Show me the n8n MCP connector');
|
||||
expect(container.textContent).toContain('Paste the prompt in Claude');
|
||||
expect(container.textContent).toContain('Paste Server URL');
|
||||
expect(container.textContent).not.toContain('claude mcp add --scope user --transport http n8n');
|
||||
expect(queryByTestId('mcp-onboarding-restart-step')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('switches to ChatGPT setup instructions', async () => {
|
||||
|
|
@ -286,6 +293,7 @@ describe('MCPOnboardingModal', () => {
|
|||
expect(queryByTestId('mcp-onboarding-client-setup')).not.toBeInTheDocument();
|
||||
expect(queryByTestId('mcp-onboarding-claude-server-url')).not.toBeInTheDocument();
|
||||
expect(queryByTestId('mcp-onboarding-copy-prompt-button')).not.toBeInTheDocument();
|
||||
expect(queryByTestId('mcp-onboarding-restart-step')).not.toBeInTheDocument();
|
||||
|
||||
await user.click(getByTestId('mcp-onboarding-copy-chatgpt-server-url-button'));
|
||||
|
||||
|
|
@ -310,7 +318,7 @@ describe('MCPOnboardingModal', () => {
|
|||
const user = userEvent.setup();
|
||||
mockMcpStore.mcpAccessEnabled = true;
|
||||
|
||||
const { getByText, container } = renderComponent();
|
||||
const { getByText, getByTestId, container } = renderComponent();
|
||||
|
||||
await user.click(getByText('Cursor'));
|
||||
|
||||
|
|
@ -321,6 +329,9 @@ describe('MCPOnboardingModal', () => {
|
|||
expect(container.textContent).toContain('~/.cursor/mcp.json');
|
||||
expect(container.textContent).toContain('complete the n8n OAuth flow');
|
||||
expect(container.textContent).not.toContain('Authorization');
|
||||
expect(getByTestId('mcp-onboarding-restart-step')).toHaveTextContent(
|
||||
'Restart Cursor and connect to n8n',
|
||||
);
|
||||
});
|
||||
|
||||
it('forwards prompt copy telemetry with the selected client', async () => {
|
||||
|
|
|
|||
|
|
@ -83,6 +83,12 @@ const clientOptions = computed<MCPOnboardingClientOption[]>(() => [
|
|||
const serverUrl = computed(() => `${rootStore.urlBaseEditor}${MCP_ENDPOINT}`);
|
||||
const isChatGptClient = computed(() => activeClient.value === 'chatgpt');
|
||||
const showServerUrlStep = computed(() => activeClient.value === 'claude');
|
||||
const showRestartStep = computed(
|
||||
() =>
|
||||
activeClient.value === 'claude_code' ||
|
||||
activeClient.value === 'cursor' ||
|
||||
activeClient.value === 'codex',
|
||||
);
|
||||
const activePromptClient = computed<MCPOnboardingPromptClient>(() =>
|
||||
activeClient.value === 'chatgpt' ? 'claude' : activeClient.value,
|
||||
);
|
||||
|
|
@ -96,6 +102,14 @@ const promptSectionTitle = computed(() =>
|
|||
interpolate: { assistant: activeClientLabel.value },
|
||||
}),
|
||||
);
|
||||
const restartSectionTitle = computed(() =>
|
||||
i18n.baseText(
|
||||
'experiments.surfaceMcpToNewCloudUsers.onboarding.section.restart.title' as BaseTextKey,
|
||||
{
|
||||
interpolate: { assistant: activeClientLabel.value },
|
||||
},
|
||||
),
|
||||
);
|
||||
const chatGptCustomAppFields = computed(() => [
|
||||
{
|
||||
label: i18n.baseText(
|
||||
|
|
@ -400,6 +414,19 @@ onBeforeUnmount(() => {
|
|||
/>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section
|
||||
v-if="showRestartStep"
|
||||
:class="[$style.section, $style.revealSection]"
|
||||
data-test-id="mcp-onboarding-restart-step"
|
||||
>
|
||||
<header :class="$style.sectionHeader">
|
||||
<span :class="$style.sectionStep">4</span>
|
||||
<h2 :class="$style.sectionTitle">
|
||||
{{ restartSectionTitle }}
|
||||
</h2>
|
||||
</header>
|
||||
</section>
|
||||
</template>
|
||||
</template>
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user