Skip to content

Commit 87df4a2

Browse files
authored
feat: Mapping/Clients rename + non-localhost consent note (P3/3) (#203)
Signed-off-by: Mohammod Al Amin Ashik <maa.ashik00@gmail.com>
1 parent 2913ecb commit 87df4a2

10 files changed

Lines changed: 75 additions & 30 deletions

File tree

apps/desktop/src/features/clients/ClientsPage.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -271,17 +271,17 @@ export default function ClientsPage() {
271271
<header className="flex-shrink-0 border-b border-[rgb(var(--border-subtle))] p-8">
272272
<div className="mx-auto max-w-[2000px]">
273273
<PageHeader
274-
title="Apps"
274+
title="Clients"
275275
titleTestId="clients-title"
276276
subtitle={
277277
<>
278-
The AI apps connected through your gateway. Which tools each one gets (which Space,
279-
which FeatureSet) is configured in{' '}
278+
The AI clients connected through your gateway. Which tools each one gets (which
279+
Space, which FeatureSet) is configured in{' '}
280280
<button
281281
onClick={() => navigateTo('workspaces')}
282282
className="font-medium text-[rgb(var(--accent))] hover:underline"
283283
>
284-
Workspaces
284+
Mapping
285285
</button>{' '}
286286
per folder, not per app.
287287
</>

apps/desktop/src/features/home/HomePage.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,7 @@ function GetStartedStrip() {
121121
icon: Monitor,
122122
title: 'Connect an AI app',
123123
desc: 'Point Cursor, Claude, or VS Code at your gateway below.',
124-
cta: 'See Apps',
124+
cta: 'See Clients',
125125
nav: 'clients' as NavItem,
126126
},
127127
];
@@ -310,8 +310,8 @@ export function HomePage() {
310310
testId="stat-clients"
311311
valueTestId="stat-clients-value"
312312
icon={Monitor}
313-
label="Apps"
314-
sub="Connected AI apps"
313+
label="Clients"
314+
sub="Connected AI clients"
315315
value={String(stats.clients)}
316316
accent="hsl(152 55% 45%)"
317317
navTarget="clients"

apps/desktop/src/lib/api/workspaceBindings.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,8 @@ export function toInput(b: WorkspaceBinding): WorkspaceBindingInput {
106106
workspace_root: b.workspace_root,
107107
space_id: b.space_id,
108108
feature_set_ids: b.feature_set_ids,
109+
// Preserve the type so editing an id mapping doesn't re-validate as a path.
110+
binding_type: b.binding_type,
109111
};
110112
}
111113

apps/desktop/src/lib/navigation.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -86,17 +86,17 @@ export const NAV_ZONES: NavZone[] = [
8686
entries: [
8787
{
8888
key: 'clients',
89-
label: 'Apps',
89+
label: 'Clients',
9090
icon: Monitor,
9191
testId: 'nav-clients',
92-
hint: 'AI apps connected through your gateway',
92+
hint: 'AI clients connected through your gateway',
9393
},
9494
{
9595
key: 'workspaces',
96-
label: 'Workspaces',
96+
label: 'Mapping',
9797
icon: FolderOpen,
9898
testId: 'nav-workspaces',
99-
hint: 'Folder → tools mappings',
99+
hint: 'Route apps to tools — by folder or id',
100100
},
101101
{
102102
key: 'featuresets',

crates/mcpmux-gateway/src/server/handlers.rs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -478,6 +478,17 @@ pub async fn oauth_authorize(
478478
// authorization for the desktop UI, which renders it as text via React.
479479
let display_name_html = html_escape_text(&display_name);
480480

481+
// When the gateway is exposed beyond loopback, a client that reached this
482+
// page from another machine can't complete the desktop consent (the
483+
// mcpmux:// deep link fires only on the host). Surface the API-key path so a
484+
// remote user isn't left at a dead end.
485+
let network_bind = state.read().await.network_bind;
486+
let network_note = if network_bind {
487+
r#"<div style="margin-bottom:1.5rem;padding:0.85rem 1rem;border-radius:10px;background:rgba(218,119,86,0.08);border:1px solid rgba(218,119,86,0.25);color:#d8b08c;font-size:0.8rem;line-height:1.45;text-align:left;"><strong style="color:#DA7756;">Connecting from another machine?</strong> This approval only completes on the computer running McpMux. For a remote or headless client, register an <strong>API-key client</strong> in McpMux (Clients tab) and connect with that key &mdash; no browser approval needed.</div>"#
488+
} else {
489+
""
490+
};
491+
481492
// HTML page that triggers the deep link
482493
// The page shows a brief message while the app opens
483494
// Industry standard: Don't auto-close, let user close after approval
@@ -589,6 +600,8 @@ pub async fn oauth_authorize(
589600
Complete authorization in {app_name}
590601
</p>
591602
603+
{network_note}
604+
592605
<div class="client-info">
593606
<div class="client-name">{display_name_html}</div>
594607
<div class="client-id">wants to connect</div>

tests/e2e/specs/app.wdio.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ describe('McpMux Application', () => {
5656
await safeClick(clientsButton);
5757
await browser.pause(1500);
5858
const pageSource = await browser.getPageSource();
59-
expect(pageSource.includes('Apps') || pageSource.includes('clients-page')).toBe(true);
59+
expect(pageSource.includes('Clients') || pageSource.includes('clients-page')).toBe(true);
6060
});
6161

6262
it('should navigate to FeatureSets page', async () => {

tests/e2e/specs/clients.spec.ts

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ test.describe('Connections Page', () => {
88
await dashboard.navigate();
99

1010
// Click Clients in sidebar
11-
await page.locator('nav button:has-text("Apps")').click();
11+
await page.locator('nav button:has-text("Clients")').click();
1212

1313
await expect(clients.heading).toBeVisible();
1414
await expect(clients.heading).toHaveText('Connections');
@@ -17,7 +17,7 @@ test.describe('Connections Page', () => {
1717
test('should describe that routing lives in Workspaces', async ({ page }) => {
1818
const dashboard = new DashboardPage(page);
1919
await dashboard.navigate();
20-
await page.locator('nav button:has-text("Apps")').click();
20+
await page.locator('nav button:has-text("Clients")').click();
2121

2222
// Routing is configured in Workspaces, not per-client.
2323
await expect(
@@ -28,7 +28,7 @@ test.describe('Connections Page', () => {
2828
test('should show description text', async ({ page }) => {
2929
const dashboard = new DashboardPage(page);
3030
await dashboard.navigate();
31-
await page.locator('nav button:has-text("Apps")').click();
31+
await page.locator('nav button:has-text("Clients")').click();
3232

3333
const description = page.locator('text=/connected|AI|client/i');
3434
// Description about clients should be visible
@@ -37,7 +37,7 @@ test.describe('Connections Page', () => {
3737
test('should show empty state or client list', async ({ page }) => {
3838
const dashboard = new DashboardPage(page);
3939
await dashboard.navigate();
40-
await page.locator('nav button:has-text("Apps")').click();
40+
await page.locator('nav button:has-text("Clients")').click();
4141

4242
const emptyState = page.locator('text=/No clients|no.*connected/i');
4343
const clientItems = page.locator('[class*="rounded"][class*="border"]');
@@ -52,7 +52,7 @@ test.describe('Connections Page', () => {
5252
test('should display client cards if clients exist', async ({ page }) => {
5353
const dashboard = new DashboardPage(page);
5454
await dashboard.navigate();
55-
await page.locator('nav button:has-text("Apps")').click();
55+
await page.locator('nav button:has-text("Clients")').click();
5656

5757
const clientCards = page.locator('[class*="rounded"][class*="border"]');
5858
const count = await clientCards.count();
@@ -69,7 +69,7 @@ test.describe('Connection Details', () => {
6969
test('should show last-seen indicator on connection cards', async ({ page }) => {
7070
const dashboard = new DashboardPage(page);
7171
await dashboard.navigate();
72-
await page.locator('nav button:has-text("Apps")').click();
72+
await page.locator('nav button:has-text("Clients")').click();
7373

7474
const clientCards = page.locator('[data-testid^="client-card-"]');
7575
const count = await clientCards.count();
@@ -87,7 +87,7 @@ test.describe('Connection Details', () => {
8787
}) => {
8888
const dashboard = new DashboardPage(page);
8989
await dashboard.navigate();
90-
await page.locator('nav button:has-text("Apps")').click();
90+
await page.locator('nav button:has-text("Clients")').click();
9191

9292
const clientCards = page.locator('[data-testid^="client-card-"]');
9393
const count = await clientCards.count();
@@ -112,7 +112,7 @@ test.describe('Connection lifecycle', () => {
112112
test('should have refresh button if available', async ({ page }) => {
113113
const dashboard = new DashboardPage(page);
114114
await dashboard.navigate();
115-
await page.locator('nav button:has-text("Apps")').click();
115+
await page.locator('nav button:has-text("Clients")').click();
116116

117117
const refreshButton = page.getByRole('button', { name: /Refresh/ });
118118
// Always rendered on the Connections header.
@@ -126,7 +126,7 @@ test.describe('Connections toast container', () => {
126126
const clients = new ClientsPage(page);
127127
await dashboard.navigate();
128128

129-
await page.locator('nav button:has-text("Apps")').click();
129+
await page.locator('nav button:has-text("Clients")').click();
130130
await expect(clients.heading).toBeVisible();
131131

132132
await expect(clients.toastContainer).toBeAttached();
@@ -138,7 +138,7 @@ test.describe('Connections toast container', () => {
138138
const clients = new ClientsPage(page);
139139
await dashboard.navigate();
140140

141-
await page.locator('nav button:has-text("Apps")').click();
141+
await page.locator('nav button:has-text("Clients")').click();
142142

143143
const clientCards = page.locator('[data-testid^="client-card-"]');
144144
const count = await clientCards.count();
@@ -162,7 +162,7 @@ test.describe('Connections toast container', () => {
162162
const clients = new ClientsPage(page);
163163
await dashboard.navigate();
164164

165-
await page.locator('nav button:has-text("Apps")').click();
165+
await page.locator('nav button:has-text("Clients")').click();
166166

167167
const clientCards = page.locator('[data-testid^="client-card-"]');
168168
const count = await clientCards.count();

tests/e2e/specs/clients.wdio.ts

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,10 @@ describe('Connections - Page shell', () => {
2222
const pageSource = await browser.getPageSource();
2323

2424
// Heading has been renamed.
25-
expect(pageSource.includes('Apps')).toBe(true);
25+
expect(pageSource.includes('Clients')).toBe(true);
2626

27-
// The page routes users to Workspaces for any routing questions.
28-
expect(pageSource.includes('Workspaces')).toBe(true);
27+
// The page routes users to the Mapping tab for any routing questions.
28+
expect(pageSource.includes('Mapping')).toBe(true);
2929
});
3030

3131
it('TC-CL-002: Open side panel and verify legacy routing controls are gone', async () => {
@@ -57,4 +57,32 @@ describe('Connections - Page shell', () => {
5757
expect(pageSource.includes("Let's hook up your first IDE")).toBe(true);
5858
}
5959
});
60+
61+
it('TC-CL-003: Register an API-key client and reveal the key once', async () => {
62+
// The desktop app auto-starts the gateway, so register_api_key_client can
63+
// mint a key. Open the Apps tab, register a client, and confirm the
64+
// generated mcpk_ key is shown exactly once.
65+
const connectionsBtn = await byTestId('nav-clients');
66+
await connectionsBtn.click();
67+
await browser.pause(1000);
68+
69+
const registerBtn = await byTestId('register-api-key-client-btn');
70+
await registerBtn.click();
71+
await browser.pause(800);
72+
73+
const nameInput = await byTestId('register-api-key-name');
74+
await nameInput.setValue('e2e-headless-bot');
75+
76+
const generateBtn = await byTestId('register-api-key-generate');
77+
await generateBtn.click();
78+
await browser.pause(1500);
79+
80+
await browser.saveScreenshot('./tests/e2e/screenshots/cl-03-api-key-created.png');
81+
82+
const keyEl = await byTestId('register-api-key-value');
83+
await expect(keyEl).toBeDisplayed();
84+
const keyText = await keyEl.getText();
85+
// Shown once, prefixed mcpk_ (never the stored hash).
86+
expect(keyText.startsWith('mcpk_')).toBe(true);
87+
});
6088
});

tests/e2e/specs/navigation.spec.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,9 @@ test.describe('Navigation', () => {
3030
await page.locator('nav button:has-text("Discover")').click({ force: true });
3131
await expect(page.locator('h1:has-text("Discover")')).toBeVisible();
3232

33-
// Spaces (use last() to avoid space switcher)
34-
await page.locator('nav button:has-text("Spaces")').last().click({ force: true });
33+
// Mapping (the workspace→tools mapping tab; nav label was renamed from
34+
// "Workspaces", but the page heading is still "Workspaces").
35+
await page.locator('nav button:has-text("Mapping")').click({ force: true });
3536
await expect(page.locator('h1:has-text("Workspaces")')).toBeVisible();
3637

3738
// FeatureSets

tests/e2e/specs/user-flows.spec.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,8 +44,9 @@ test.describe('Complete User Flows', () => {
4444
await page.locator('nav button:has-text("Discover")').click();
4545
await expect(page.locator('h1:has-text("Discover")')).toBeVisible();
4646

47-
// Spaces (use last() to avoid space switcher)
48-
await page.locator('nav button:has-text("Spaces")').last().click();
47+
// Mapping (the workspace→tools mapping tab; nav label was renamed from
48+
// "Workspaces", but the page heading is still "Workspaces").
49+
await page.locator('nav button:has-text("Mapping")').click();
4950
await expect(page.locator('h1:has-text("Workspaces")')).toBeVisible();
5051

5152
// FeatureSets

0 commit comments

Comments
 (0)