diff --git a/apps/desktop/src/features/clients/ClientsPage.tsx b/apps/desktop/src/features/clients/ClientsPage.tsx index 2d5138fb..b976f8ac 100644 --- a/apps/desktop/src/features/clients/ClientsPage.tsx +++ b/apps/desktop/src/features/clients/ClientsPage.tsx @@ -271,17 +271,17 @@ export default function ClientsPage() {
- The AI apps connected through your gateway. Which tools each one gets (which Space, - which FeatureSet) is configured in{' '} + The AI clients connected through your gateway. Which tools each one gets (which + Space, which FeatureSet) is configured in{' '} {' '} per folder, not per app. diff --git a/apps/desktop/src/features/home/HomePage.tsx b/apps/desktop/src/features/home/HomePage.tsx index e73c159f..f918633e 100644 --- a/apps/desktop/src/features/home/HomePage.tsx +++ b/apps/desktop/src/features/home/HomePage.tsx @@ -121,7 +121,7 @@ function GetStartedStrip() { icon: Monitor, title: 'Connect an AI app', desc: 'Point Cursor, Claude, or VS Code at your gateway below.', - cta: 'See Apps', + cta: 'See Clients', nav: 'clients' as NavItem, }, ]; @@ -310,8 +310,8 @@ export function HomePage() { testId="stat-clients" valueTestId="stat-clients-value" icon={Monitor} - label="Apps" - sub="Connected AI apps" + label="Clients" + sub="Connected AI clients" value={String(stats.clients)} accent="hsl(152 55% 45%)" navTarget="clients" diff --git a/apps/desktop/src/lib/api/workspaceBindings.ts b/apps/desktop/src/lib/api/workspaceBindings.ts index 8c463096..bdd0f177 100644 --- a/apps/desktop/src/lib/api/workspaceBindings.ts +++ b/apps/desktop/src/lib/api/workspaceBindings.ts @@ -106,6 +106,8 @@ export function toInput(b: WorkspaceBinding): WorkspaceBindingInput { workspace_root: b.workspace_root, space_id: b.space_id, feature_set_ids: b.feature_set_ids, + // Preserve the type so editing an id mapping doesn't re-validate as a path. + binding_type: b.binding_type, }; } diff --git a/apps/desktop/src/lib/navigation.ts b/apps/desktop/src/lib/navigation.ts index 82c44184..802213bb 100644 --- a/apps/desktop/src/lib/navigation.ts +++ b/apps/desktop/src/lib/navigation.ts @@ -86,17 +86,17 @@ export const NAV_ZONES: NavZone[] = [ entries: [ { key: 'clients', - label: 'Apps', + label: 'Clients', icon: Monitor, testId: 'nav-clients', - hint: 'AI apps connected through your gateway', + hint: 'AI clients connected through your gateway', }, { key: 'workspaces', - label: 'Workspaces', + label: 'Mapping', icon: FolderOpen, testId: 'nav-workspaces', - hint: 'Folder → tools mappings', + hint: 'Route apps to tools — by folder or id', }, { key: 'featuresets', diff --git a/crates/mcpmux-gateway/src/server/handlers.rs b/crates/mcpmux-gateway/src/server/handlers.rs index bc86bc4d..3eb2123a 100644 --- a/crates/mcpmux-gateway/src/server/handlers.rs +++ b/crates/mcpmux-gateway/src/server/handlers.rs @@ -478,6 +478,17 @@ pub async fn oauth_authorize( // authorization for the desktop UI, which renders it as text via React. let display_name_html = html_escape_text(&display_name); + // When the gateway is exposed beyond loopback, a client that reached this + // page from another machine can't complete the desktop consent (the + // mcpmux:// deep link fires only on the host). Surface the API-key path so a + // remote user isn't left at a dead end. + let network_bind = state.read().await.network_bind; + let network_note = if network_bind { + r#"
Connecting from another machine? This approval only completes on the computer running McpMux. For a remote or headless client, register an API-key client in McpMux (Clients tab) and connect with that key — no browser approval needed.
"# + } else { + "" + }; + // HTML page that triggers the deep link // The page shows a brief message while the app opens // Industry standard: Don't auto-close, let user close after approval @@ -589,6 +600,8 @@ pub async fn oauth_authorize( Complete authorization in {app_name}

+ {network_note} +
{display_name_html}
wants to connect
diff --git a/tests/e2e/specs/app.wdio.ts b/tests/e2e/specs/app.wdio.ts index 81c1d0b8..062c8d58 100644 --- a/tests/e2e/specs/app.wdio.ts +++ b/tests/e2e/specs/app.wdio.ts @@ -56,7 +56,7 @@ describe('McpMux Application', () => { await safeClick(clientsButton); await browser.pause(1500); const pageSource = await browser.getPageSource(); - expect(pageSource.includes('Apps') || pageSource.includes('clients-page')).toBe(true); + expect(pageSource.includes('Clients') || pageSource.includes('clients-page')).toBe(true); }); it('should navigate to FeatureSets page', async () => { diff --git a/tests/e2e/specs/clients.spec.ts b/tests/e2e/specs/clients.spec.ts index 55768f15..3087a7e6 100644 --- a/tests/e2e/specs/clients.spec.ts +++ b/tests/e2e/specs/clients.spec.ts @@ -8,7 +8,7 @@ test.describe('Connections Page', () => { await dashboard.navigate(); // Click Clients in sidebar - await page.locator('nav button:has-text("Apps")').click(); + await page.locator('nav button:has-text("Clients")').click(); await expect(clients.heading).toBeVisible(); await expect(clients.heading).toHaveText('Connections'); @@ -17,7 +17,7 @@ test.describe('Connections Page', () => { test('should describe that routing lives in Workspaces', async ({ page }) => { const dashboard = new DashboardPage(page); await dashboard.navigate(); - await page.locator('nav button:has-text("Apps")').click(); + await page.locator('nav button:has-text("Clients")').click(); // Routing is configured in Workspaces, not per-client. await expect( @@ -28,7 +28,7 @@ test.describe('Connections Page', () => { test('should show description text', async ({ page }) => { const dashboard = new DashboardPage(page); await dashboard.navigate(); - await page.locator('nav button:has-text("Apps")').click(); + await page.locator('nav button:has-text("Clients")').click(); const description = page.locator('text=/connected|AI|client/i'); // Description about clients should be visible @@ -37,7 +37,7 @@ test.describe('Connections Page', () => { test('should show empty state or client list', async ({ page }) => { const dashboard = new DashboardPage(page); await dashboard.navigate(); - await page.locator('nav button:has-text("Apps")').click(); + await page.locator('nav button:has-text("Clients")').click(); const emptyState = page.locator('text=/No clients|no.*connected/i'); const clientItems = page.locator('[class*="rounded"][class*="border"]'); @@ -52,7 +52,7 @@ test.describe('Connections Page', () => { test('should display client cards if clients exist', async ({ page }) => { const dashboard = new DashboardPage(page); await dashboard.navigate(); - await page.locator('nav button:has-text("Apps")').click(); + await page.locator('nav button:has-text("Clients")').click(); const clientCards = page.locator('[class*="rounded"][class*="border"]'); const count = await clientCards.count(); @@ -69,7 +69,7 @@ test.describe('Connection Details', () => { test('should show last-seen indicator on connection cards', async ({ page }) => { const dashboard = new DashboardPage(page); await dashboard.navigate(); - await page.locator('nav button:has-text("Apps")').click(); + await page.locator('nav button:has-text("Clients")').click(); const clientCards = page.locator('[data-testid^="client-card-"]'); const count = await clientCards.count(); @@ -87,7 +87,7 @@ test.describe('Connection Details', () => { }) => { const dashboard = new DashboardPage(page); await dashboard.navigate(); - await page.locator('nav button:has-text("Apps")').click(); + await page.locator('nav button:has-text("Clients")').click(); const clientCards = page.locator('[data-testid^="client-card-"]'); const count = await clientCards.count(); @@ -112,7 +112,7 @@ test.describe('Connection lifecycle', () => { test('should have refresh button if available', async ({ page }) => { const dashboard = new DashboardPage(page); await dashboard.navigate(); - await page.locator('nav button:has-text("Apps")').click(); + await page.locator('nav button:has-text("Clients")').click(); const refreshButton = page.getByRole('button', { name: /Refresh/ }); // Always rendered on the Connections header. @@ -126,7 +126,7 @@ test.describe('Connections toast container', () => { const clients = new ClientsPage(page); await dashboard.navigate(); - await page.locator('nav button:has-text("Apps")').click(); + await page.locator('nav button:has-text("Clients")').click(); await expect(clients.heading).toBeVisible(); await expect(clients.toastContainer).toBeAttached(); @@ -138,7 +138,7 @@ test.describe('Connections toast container', () => { const clients = new ClientsPage(page); await dashboard.navigate(); - await page.locator('nav button:has-text("Apps")').click(); + await page.locator('nav button:has-text("Clients")').click(); const clientCards = page.locator('[data-testid^="client-card-"]'); const count = await clientCards.count(); @@ -162,7 +162,7 @@ test.describe('Connections toast container', () => { const clients = new ClientsPage(page); await dashboard.navigate(); - await page.locator('nav button:has-text("Apps")').click(); + await page.locator('nav button:has-text("Clients")').click(); const clientCards = page.locator('[data-testid^="client-card-"]'); const count = await clientCards.count(); diff --git a/tests/e2e/specs/clients.wdio.ts b/tests/e2e/specs/clients.wdio.ts index 0d7a0987..3585fa6b 100644 --- a/tests/e2e/specs/clients.wdio.ts +++ b/tests/e2e/specs/clients.wdio.ts @@ -22,10 +22,10 @@ describe('Connections - Page shell', () => { const pageSource = await browser.getPageSource(); // Heading has been renamed. - expect(pageSource.includes('Apps')).toBe(true); + expect(pageSource.includes('Clients')).toBe(true); - // The page routes users to Workspaces for any routing questions. - expect(pageSource.includes('Workspaces')).toBe(true); + // The page routes users to the Mapping tab for any routing questions. + expect(pageSource.includes('Mapping')).toBe(true); }); it('TC-CL-002: Open side panel and verify legacy routing controls are gone', async () => { @@ -57,4 +57,32 @@ describe('Connections - Page shell', () => { expect(pageSource.includes("Let's hook up your first IDE")).toBe(true); } }); + + it('TC-CL-003: Register an API-key client and reveal the key once', async () => { + // The desktop app auto-starts the gateway, so register_api_key_client can + // mint a key. Open the Apps tab, register a client, and confirm the + // generated mcpk_ key is shown exactly once. + const connectionsBtn = await byTestId('nav-clients'); + await connectionsBtn.click(); + await browser.pause(1000); + + const registerBtn = await byTestId('register-api-key-client-btn'); + await registerBtn.click(); + await browser.pause(800); + + const nameInput = await byTestId('register-api-key-name'); + await nameInput.setValue('e2e-headless-bot'); + + const generateBtn = await byTestId('register-api-key-generate'); + await generateBtn.click(); + await browser.pause(1500); + + await browser.saveScreenshot('./tests/e2e/screenshots/cl-03-api-key-created.png'); + + const keyEl = await byTestId('register-api-key-value'); + await expect(keyEl).toBeDisplayed(); + const keyText = await keyEl.getText(); + // Shown once, prefixed mcpk_ (never the stored hash). + expect(keyText.startsWith('mcpk_')).toBe(true); + }); }); diff --git a/tests/e2e/specs/navigation.spec.ts b/tests/e2e/specs/navigation.spec.ts index 34778502..601efc92 100644 --- a/tests/e2e/specs/navigation.spec.ts +++ b/tests/e2e/specs/navigation.spec.ts @@ -30,8 +30,9 @@ test.describe('Navigation', () => { await page.locator('nav button:has-text("Discover")').click({ force: true }); await expect(page.locator('h1:has-text("Discover")')).toBeVisible(); - // Spaces (use last() to avoid space switcher) - await page.locator('nav button:has-text("Spaces")').last().click({ force: true }); + // Mapping (the workspace→tools mapping tab; nav label was renamed from + // "Workspaces", but the page heading is still "Workspaces"). + await page.locator('nav button:has-text("Mapping")').click({ force: true }); await expect(page.locator('h1:has-text("Workspaces")')).toBeVisible(); // FeatureSets diff --git a/tests/e2e/specs/user-flows.spec.ts b/tests/e2e/specs/user-flows.spec.ts index 2754e465..012b9397 100644 --- a/tests/e2e/specs/user-flows.spec.ts +++ b/tests/e2e/specs/user-flows.spec.ts @@ -44,8 +44,9 @@ test.describe('Complete User Flows', () => { await page.locator('nav button:has-text("Discover")').click(); await expect(page.locator('h1:has-text("Discover")')).toBeVisible(); - // Spaces (use last() to avoid space switcher) - await page.locator('nav button:has-text("Spaces")').last().click(); + // Mapping (the workspace→tools mapping tab; nav label was renamed from + // "Workspaces", but the page heading is still "Workspaces"). + await page.locator('nav button:has-text("Mapping")').click(); await expect(page.locator('h1:has-text("Workspaces")')).toBeVisible(); // FeatureSets