diff --git a/README.md b/README.md
index 190e81ba..e1bd6930 100644
--- a/README.md
+++ b/README.md
@@ -63,23 +63,41 @@ McpMux routes calls to the right server, refreshes OAuth tokens automatically, a
## Features
-**Spaces** — Isolated workspaces with their own servers and credentials. Switch between "Work" and "Personal" in one click.
+### Server Management
+
+Add, remove, and monitor all your MCP servers in one place. See connection status, auth type, and transport at a glance.
+
+
+
+### Server Registry
+
+Browse and one-click install from a curated registry of MCP servers. Filter by category, auth type, or hosting. Cached for offline use.
+
+
+
+### Spaces
+
+Isolated workspaces with their own servers and credentials. Switch between "Work" and "Personal" in one click — your AI clients follow automatically.

-**Encrypted Credentials** — Stored in your OS keychain (macOS Keychain, Windows Credential Manager, Linux Secret Service) with AES-256-GCM encryption. Not in plain-text JSON files.
+### Feature Sets
-**OAuth That Just Works** — Handles the full OAuth 2.1 + PKCE flow for remote servers. Automatic token refresh. Authenticate once and forget about it.
+Control exactly which tools, prompts, and resources are available. Create custom permission bundles like "Read Only" or "Dev Tools" and assign them to clients.
-**Server Registry** — Browse, search, and one-click install MCP servers. Cached for offline use.
+
-**Per-Client Permissions** — Control which tools each AI client can access with Feature Sets.
+### Connected Clients
-**System Tray** — Runs in the background, starts with your OS. Always available.
+See every AI client connected to your gateway. Manage OAuth registrations and control which space each client can access.
-
+
+
+### Settings
-**Auto-Updates** — Signed updates delivered automatically.
+Auto-updates, launch at startup, system tray, and appearance preferences.
+
+
---
diff --git a/apps/desktop/public/logos/atlassian.svg b/apps/desktop/public/logos/atlassian.svg
new file mode 100644
index 00000000..6eaac6cf
--- /dev/null
+++ b/apps/desktop/public/logos/atlassian.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/apps/desktop/public/logos/azure.svg b/apps/desktop/public/logos/azure.svg
new file mode 100644
index 00000000..9803e2d4
--- /dev/null
+++ b/apps/desktop/public/logos/azure.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/apps/desktop/public/logos/cloudflare.svg b/apps/desktop/public/logos/cloudflare.svg
new file mode 100644
index 00000000..66cc0208
--- /dev/null
+++ b/apps/desktop/public/logos/cloudflare.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/apps/desktop/public/logos/datadog.svg b/apps/desktop/public/logos/datadog.svg
new file mode 100644
index 00000000..ab437319
--- /dev/null
+++ b/apps/desktop/public/logos/datadog.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/apps/desktop/public/logos/filesystem.svg b/apps/desktop/public/logos/filesystem.svg
new file mode 100644
index 00000000..53c11aac
--- /dev/null
+++ b/apps/desktop/public/logos/filesystem.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/apps/desktop/public/logos/gdrive.svg b/apps/desktop/public/logos/gdrive.svg
new file mode 100644
index 00000000..7263ef31
--- /dev/null
+++ b/apps/desktop/public/logos/gdrive.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/apps/desktop/public/logos/github.svg b/apps/desktop/public/logos/github.svg
new file mode 100644
index 00000000..81920ca3
--- /dev/null
+++ b/apps/desktop/public/logos/github.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/apps/desktop/public/logos/linear.svg b/apps/desktop/public/logos/linear.svg
new file mode 100644
index 00000000..9ac44813
--- /dev/null
+++ b/apps/desktop/public/logos/linear.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/apps/desktop/public/logos/memory.svg b/apps/desktop/public/logos/memory.svg
new file mode 100644
index 00000000..38390179
--- /dev/null
+++ b/apps/desktop/public/logos/memory.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/apps/desktop/public/logos/mongodb.svg b/apps/desktop/public/logos/mongodb.svg
new file mode 100644
index 00000000..13d7d00a
--- /dev/null
+++ b/apps/desktop/public/logos/mongodb.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/apps/desktop/public/logos/notion.svg b/apps/desktop/public/logos/notion.svg
new file mode 100644
index 00000000..69afef32
--- /dev/null
+++ b/apps/desktop/public/logos/notion.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/apps/desktop/public/logos/postgresql.svg b/apps/desktop/public/logos/postgresql.svg
new file mode 100644
index 00000000..931bdae1
--- /dev/null
+++ b/apps/desktop/public/logos/postgresql.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/apps/desktop/public/logos/puppeteer.svg b/apps/desktop/public/logos/puppeteer.svg
new file mode 100644
index 00000000..ac85899f
--- /dev/null
+++ b/apps/desktop/public/logos/puppeteer.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/apps/desktop/public/logos/sentry.svg b/apps/desktop/public/logos/sentry.svg
new file mode 100644
index 00000000..47888975
--- /dev/null
+++ b/apps/desktop/public/logos/sentry.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/apps/desktop/public/logos/slack.svg b/apps/desktop/public/logos/slack.svg
new file mode 100644
index 00000000..bb2957e2
--- /dev/null
+++ b/apps/desktop/public/logos/slack.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/apps/desktop/public/logos/stripe.svg b/apps/desktop/public/logos/stripe.svg
new file mode 100644
index 00000000..48272a2c
--- /dev/null
+++ b/apps/desktop/public/logos/stripe.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/docs/screenshots/clients.png b/docs/screenshots/clients.png
new file mode 100644
index 00000000..755bd59c
Binary files /dev/null and b/docs/screenshots/clients.png differ
diff --git a/docs/screenshots/dashboard.png b/docs/screenshots/dashboard.png
index 1409e711..7869e308 100644
Binary files a/docs/screenshots/dashboard.png and b/docs/screenshots/dashboard.png differ
diff --git a/docs/screenshots/discover.png b/docs/screenshots/discover.png
new file mode 100644
index 00000000..0847b1fa
Binary files /dev/null and b/docs/screenshots/discover.png differ
diff --git a/docs/screenshots/featuresets.png b/docs/screenshots/featuresets.png
new file mode 100644
index 00000000..b62a1b5e
Binary files /dev/null and b/docs/screenshots/featuresets.png differ
diff --git a/docs/screenshots/servers.png b/docs/screenshots/servers.png
new file mode 100644
index 00000000..71dbec6b
Binary files /dev/null and b/docs/screenshots/servers.png differ
diff --git a/docs/screenshots/settings.png b/docs/screenshots/settings.png
index a59d9eba..f06057d0 100644
Binary files a/docs/screenshots/settings.png and b/docs/screenshots/settings.png differ
diff --git a/docs/screenshots/spaces.png b/docs/screenshots/spaces.png
index 835f5b6a..5e1d3a03 100644
Binary files a/docs/screenshots/spaces.png and b/docs/screenshots/spaces.png differ
diff --git a/scripts/take-screenshots.cjs b/scripts/take-screenshots.cjs
new file mode 100644
index 00000000..25daf3bc
--- /dev/null
+++ b/scripts/take-screenshots.cjs
@@ -0,0 +1,404 @@
+/**
+ * Comprehensive screenshot script with Tauri IPC mocking.
+ * Produces realistic light-theme screenshots for the README.
+ */
+const { chromium } = require('/home/user/mcp-mux/node_modules/.pnpm/playwright-core@1.58.1/node_modules/playwright-core');
+
+const BASE_URL = 'http://localhost:1420';
+const DIR = '/home/user/mcp-mux/docs/screenshots';
+
+// ── Mock data ──────────────────────────────────────────────
+
+const SPACE_DEFAULT = {
+ id: '00000000-0000-0000-0000-000000000001',
+ name: 'My Space',
+ icon: '🌐',
+ description: null,
+ is_default: true,
+ sort_order: 0,
+ created_at: '2026-01-15T10:00:00Z',
+ updated_at: '2026-01-15T10:00:00Z',
+};
+
+const SPACE_WORK = {
+ id: '00000000-0000-0000-0000-000000000002',
+ name: 'Work Projects',
+ icon: '💼',
+ description: 'Production servers for work',
+ is_default: false,
+ sort_order: 1,
+ created_at: '2026-01-20T10:00:00Z',
+ updated_at: '2026-01-20T10:00:00Z',
+};
+
+const SPACE_PERSONAL = {
+ id: '00000000-0000-0000-0000-000000000003',
+ name: 'Personal',
+ icon: '🏠',
+ description: 'Side projects and experiments',
+ is_default: false,
+ sort_order: 2,
+ created_at: '2026-01-22T10:00:00Z',
+ updated_at: '2026-01-22T10:00:00Z',
+};
+
+const SPACES = [SPACE_DEFAULT, SPACE_WORK, SPACE_PERSONAL];
+
+function mkInstalled(id, instId, serverId, name, desc, alias, icon, categories, transportType, auth, publisher, caps, inputDefs, inputVals, enabled, oauthConnected) {
+ const transport = transportType === 'stdio'
+ ? { type: 'stdio', command: 'npx', args: ['-y', `@modelcontextprotocol/server-${alias}`], metadata: { inputs: inputDefs || [] } }
+ : { type: 'http', url: `https://mcp.${alias}.com/sse`, headers: {}, metadata: { inputs: inputDefs || [] } };
+ return {
+ id: instId, space_id: SPACE_DEFAULT.id, server_id: serverId,
+ server_name: name, cached_definition: JSON.stringify({
+ id: serverId, name, description: desc, alias, icon, categories, transport, auth,
+ publisher, capabilities: caps,
+ }),
+ input_values: inputVals || {}, enabled, env_overrides: {}, args_append: [],
+ extra_headers: {}, oauth_connected: oauthConnected || false, source: 'Registry',
+ created_at: '2026-01-16T10:00:00Z', updated_at: '2026-01-16T10:00:00Z',
+ };
+}
+
+const INSTALLED_SERVERS = [
+ mkInstalled(1, 'inst-1', 'github-mcp', 'GitHub', 'GitHub API integration — issues, PRs, repos, and code search', 'github', '🐙', ['developer-tools'], 'stdio',
+ { type: 'api_key', instructions: null }, { name: 'Anthropic', verified: true, official: true }, { tools: true, resources: true, prompts: false },
+ [{ id: 'GITHUB_TOKEN', label: 'GitHub Token', type: 'password', required: true, secret: true }], { GITHUB_TOKEN: 'ghp_***' }, true, false),
+ mkInstalled(2, 'inst-2', 'atlassian-mcp', 'Atlassian', 'Jira issues, Confluence pages, and project management', 'atlassian', '🔷', ['productivity'], 'http',
+ { type: 'oauth' }, { name: 'Atlassian', verified: true, official: true }, { tools: true, resources: true, prompts: false },
+ [], {}, true, true),
+ mkInstalled(3, 'inst-3', 'cloudflare-mcp', 'Cloudflare', 'Manage Workers, KV, R2, and DNS via Cloudflare API', 'cloudflare', '☁️', ['cloud'], 'http',
+ { type: 'oauth' }, { name: 'Cloudflare', verified: true, official: true }, { tools: true, resources: true, prompts: false },
+ [], {}, true, true),
+ mkInstalled(4, 'inst-4', 'postgres-mcp', 'PostgreSQL', 'Query databases, inspect schemas, and run SQL', 'postgres', '🐘', ['developer-tools'], 'stdio',
+ { type: 'api_key', instructions: null }, { name: 'Anthropic', verified: true, official: true }, { tools: true, resources: true, prompts: false },
+ [{ id: 'DATABASE_URL', label: 'Database URL', type: 'url', required: true, secret: true }], { DATABASE_URL: 'postgres://***' }, true, false),
+ mkInstalled(5, 'inst-5', 'azure-mcp', 'Azure DevOps', 'Azure DevOps work items, repos, and pipelines', 'azure', '🔵', ['cloud'], 'http',
+ { type: 'oauth' }, { name: 'Microsoft', verified: true, official: true }, { tools: true, resources: true, prompts: false },
+ [], {}, true, true),
+ mkInstalled(6, 'inst-6', 'filesystem-mcp', 'Filesystem', 'Read, write, and search files on your local machine', 'filesystem', '📂', ['file-system'], 'stdio',
+ { type: 'none' }, { name: 'Anthropic', verified: true, official: true }, { tools: true, resources: true, prompts: false },
+ [{ id: 'ALLOWED_DIR', label: 'Allowed Directory', type: 'text', required: true, secret: false }], { ALLOWED_DIR: '/home/user/projects' }, true, false),
+];
+
+function mkFeature(serverId, serverName, type, name, desc) {
+ return { id: `${serverId}-${name}`, space_id: SPACE_DEFAULT.id, server_id: serverId, feature_type: type, feature_name: name, display_name: name, description: desc, input_schema: null, discovered_at: '2026-01-16T10:00:00Z', last_seen_at: '2026-02-07T10:00:00Z', is_available: true };
+}
+
+const SERVER_FEATURES = [
+ // GitHub
+ mkFeature('github-mcp', 'GitHub', 'tool', 'create_issue', 'Create a new GitHub issue'),
+ mkFeature('github-mcp', 'GitHub', 'tool', 'search_code', 'Search code across repositories'),
+ mkFeature('github-mcp', 'GitHub', 'tool', 'list_pull_requests', 'List pull requests for a repository'),
+ mkFeature('github-mcp', 'GitHub', 'tool', 'create_pull_request', 'Create a new pull request'),
+ mkFeature('github-mcp', 'GitHub', 'resource', 'repo://contents', 'Repository file contents'),
+ // Atlassian
+ mkFeature('atlassian-mcp', 'Atlassian', 'tool', 'create_jira_issue', 'Create a Jira issue'),
+ mkFeature('atlassian-mcp', 'Atlassian', 'tool', 'search_issues', 'Search Jira issues with JQL'),
+ mkFeature('atlassian-mcp', 'Atlassian', 'tool', 'get_confluence_page', 'Fetch a Confluence page'),
+ mkFeature('atlassian-mcp', 'Atlassian', 'tool', 'update_issue_status', 'Transition a Jira issue'),
+ mkFeature('atlassian-mcp', 'Atlassian', 'resource', 'jira://boards', 'Jira boards listing'),
+ // Cloudflare
+ mkFeature('cloudflare-mcp', 'Cloudflare', 'tool', 'deploy_worker', 'Deploy a Cloudflare Worker'),
+ mkFeature('cloudflare-mcp', 'Cloudflare', 'tool', 'list_kv_namespaces', 'List KV namespaces'),
+ mkFeature('cloudflare-mcp', 'Cloudflare', 'tool', 'manage_r2_bucket', 'Manage R2 storage buckets'),
+ mkFeature('cloudflare-mcp', 'Cloudflare', 'tool', 'update_dns_record', 'Update DNS records'),
+ mkFeature('cloudflare-mcp', 'Cloudflare', 'resource', 'workers://bindings', 'Worker bindings config'),
+ // PostgreSQL
+ mkFeature('postgres-mcp', 'PostgreSQL', 'tool', 'query', 'Execute a SQL query'),
+ mkFeature('postgres-mcp', 'PostgreSQL', 'tool', 'describe_table', 'Get table schema'),
+ mkFeature('postgres-mcp', 'PostgreSQL', 'resource', 'schema://tables', 'Database table listing'),
+ // Azure DevOps
+ mkFeature('azure-mcp', 'Azure DevOps', 'tool', 'create_work_item', 'Create an Azure DevOps work item'),
+ mkFeature('azure-mcp', 'Azure DevOps', 'tool', 'list_pipelines', 'List CI/CD pipelines'),
+ mkFeature('azure-mcp', 'Azure DevOps', 'tool', 'trigger_pipeline', 'Trigger a pipeline run'),
+ mkFeature('azure-mcp', 'Azure DevOps', 'resource', 'repos://list', 'Azure repos listing'),
+ // Filesystem
+ mkFeature('filesystem-mcp', 'Filesystem', 'tool', 'read_file', 'Read a file from disk'),
+ mkFeature('filesystem-mcp', 'Filesystem', 'tool', 'write_file', 'Write content to a file'),
+ mkFeature('filesystem-mcp', 'Filesystem', 'tool', 'search_files', 'Search files by pattern'),
+ mkFeature('filesystem-mcp', 'Filesystem', 'tool', 'list_directory', 'List directory contents'),
+ mkFeature('filesystem-mcp', 'Filesystem', 'resource', 'file://contents', 'File contents'),
+];
+
+const SERVER_STATUSES = {
+ 'github-mcp': { server_id: 'github-mcp', status: 'connected', flow_id: 1, has_connected_before: true, message: null },
+ 'atlassian-mcp': { server_id: 'atlassian-mcp', status: 'connected', flow_id: 1, has_connected_before: true, message: null },
+ 'cloudflare-mcp': { server_id: 'cloudflare-mcp', status: 'connected', flow_id: 1, has_connected_before: true, message: null },
+ 'postgres-mcp': { server_id: 'postgres-mcp', status: 'connected', flow_id: 1, has_connected_before: true, message: null },
+ 'azure-mcp': { server_id: 'azure-mcp', status: 'connected', flow_id: 1, has_connected_before: true, message: null },
+ 'filesystem-mcp': { server_id: 'filesystem-mcp', status: 'connected', flow_id: 1, has_connected_before: true, message: null },
+};
+
+const FEATURE_SETS = [
+ { id: 'fs-all', name: 'All Features', description: 'Access to all tools, prompts, and resources', icon: null, space_id: null, feature_set_type: 'all', server_id: null, is_builtin: true, is_deleted: false, members: [] },
+ { id: 'fs-default', name: 'Default', description: 'Default feature set for new clients', icon: null, space_id: null, feature_set_type: 'default', server_id: null, is_builtin: true, is_deleted: false, members: [] },
+ { id: 'fs-readonly', name: 'Read Only', description: 'Only read operations — no writes or deletes', icon: '🔒', space_id: SPACE_DEFAULT.id, feature_set_type: 'custom', server_id: null, is_builtin: false, is_deleted: false, members: [] },
+ { id: 'fs-dev', name: 'Dev Tools', description: 'GitHub + PostgreSQL + Filesystem access', icon: '🛠️', space_id: SPACE_DEFAULT.id, feature_set_type: 'custom', server_id: null, is_builtin: false, is_deleted: false, members: [] },
+];
+
+const OAUTH_CLIENTS = [
+ { client_id: 'cursor-001', registration_type: 'dcr', client_name: 'Cursor', client_alias: null, redirect_uris: ['http://localhost:9315/callback'], scope: null, approved: true, logo_uri: null, client_uri: null, software_id: 'cursor', software_version: '0.45.0', metadata_url: null, metadata_cached_at: null, metadata_cache_ttl: null, connection_mode: 'follow_active', locked_space_id: null, last_seen: '2026-02-07T09:30:00Z', created_at: '2026-01-20T10:00:00Z', has_active_tokens: true },
+ { client_id: 'vscode-001', registration_type: 'dcr', client_name: 'VS Code', client_alias: null, redirect_uris: ['http://localhost:9315/callback'], scope: null, approved: true, logo_uri: null, client_uri: null, software_id: 'vscode', software_version: '1.96.0', metadata_url: null, metadata_cached_at: null, metadata_cache_ttl: null, connection_mode: 'follow_active', locked_space_id: null, last_seen: '2026-02-07T08:45:00Z', created_at: '2026-01-22T10:00:00Z', has_active_tokens: true },
+];
+
+function mkRegistry(id, name, desc, alias, icon, categories, auth, transportType, publisher, caps, hostingType, badges) {
+ const source = { type: 'Registry', url: 'https://registry.mcpmux.com', name: 'Official Registry' };
+ const transport = transportType === 'stdio'
+ ? { type: 'stdio', command: 'npx', args: ['-y', `@modelcontextprotocol/server-${alias}`], env: {}, metadata: { inputs: [] } }
+ : { type: 'http', url: `https://mcp.${alias}.com/sse`, headers: {}, metadata: { inputs: [] } };
+ return { id, name, description: desc, alias, icon, categories, auth, publisher, transport, source, capabilities: caps, hosting_type: hostingType || 'local', badges: badges || [], license: 'MIT', installation: { difficulty: 'easy' } };
+}
+
+const REGISTRY_SERVERS = [
+ // Installed ones (will show "Installed" in discover) — IDs match INSTALLED_SERVERS
+ mkRegistry('github-mcp', 'GitHub', 'GitHub API integration — issues, PRs, repos, and code search', 'github', '🐙', ['developer-tools'], { type: 'api_key', instructions: null }, 'stdio', { name: 'Anthropic', domain: 'anthropic.com', verified: true, official: true }, { tools: true, resources: true, prompts: false }, 'local', ['official', 'verified', 'popular']),
+ mkRegistry('atlassian-mcp', 'Atlassian', 'Jira issues, Confluence pages, and project management', 'atlassian', '🔷', ['productivity'], { type: 'oauth' }, 'http', { name: 'Atlassian', domain: 'atlassian.com', verified: true, official: true }, { tools: true, resources: true, prompts: false }, 'remote', ['official', 'verified']),
+ mkRegistry('cloudflare-mcp', 'Cloudflare', 'Manage Workers, KV, R2, and DNS via Cloudflare API', 'cloudflare', '☁️', ['cloud'], { type: 'oauth' }, 'http', { name: 'Cloudflare', domain: 'cloudflare.com', verified: true, official: true }, { tools: true, resources: true, prompts: false }, 'remote', ['official', 'verified']),
+ mkRegistry('postgres-mcp', 'PostgreSQL', 'Query databases, inspect schemas, and run SQL', 'postgres', '🐘', ['developer-tools'], { type: 'api_key', instructions: null }, 'stdio', { name: 'Anthropic', domain: 'anthropic.com', verified: true, official: true }, { tools: true, resources: true, prompts: false }, 'local', ['official', 'popular']),
+ mkRegistry('azure-mcp', 'Azure DevOps', 'Azure DevOps work items, repos, and pipelines', 'azure', '🔵', ['cloud'], { type: 'oauth' }, 'http', { name: 'Microsoft', domain: 'microsoft.com', verified: true, official: true }, { tools: true, resources: true, prompts: false }, 'remote', ['official', 'verified']),
+ mkRegistry('filesystem-mcp', 'Filesystem', 'Read, write, and search files on your local machine', 'filesystem', '📂', ['file-system'], { type: 'none' }, 'stdio', { name: 'Anthropic', domain: 'anthropic.com', verified: true, official: true }, { tools: true, resources: true, prompts: false }, 'local', ['official']),
+ // Not installed — popular servers in the registry
+ mkRegistry('slack-mcp', 'Slack', 'Send messages, search channels, and manage Slack workspaces', 'slack', '💬', ['productivity'], { type: 'oauth' }, 'http', { name: 'Slack', domain: 'slack.com', verified: true, official: true }, { tools: true, resources: false, prompts: true }, 'remote', ['official', 'verified', 'popular']),
+ mkRegistry('gdrive-mcp', 'Google Drive', 'Search and read files from Google Drive', 'gdrive', '📁', ['cloud'], { type: 'oauth' }, 'stdio', { name: 'Google', domain: 'google.com', verified: true, official: true }, { tools: true, resources: true, prompts: false }, 'remote', ['official', 'popular']),
+ mkRegistry('stripe-mcp', 'Stripe', 'Manage payments, subscriptions, and invoices', 'stripe', '💳', ['developer-tools'], { type: 'api_key', instructions: null }, 'http', { name: 'Stripe', domain: 'stripe.com', verified: true, official: true }, { tools: true, resources: false, prompts: false }, 'remote', ['official', 'verified']),
+ mkRegistry('linear-mcp', 'Linear', 'Manage issues, projects, and teams in Linear', 'linear', '📐', ['productivity'], { type: 'api_key', instructions: null }, 'http', { name: 'Linear', domain: 'linear.app', verified: true, official: true }, { tools: true, resources: false, prompts: false }, 'remote', ['verified']),
+ mkRegistry('sentry-mcp', 'Sentry', 'Search and analyze errors and performance issues', 'sentry', '🔥', ['developer-tools'], { type: 'api_key', instructions: null }, 'stdio', { name: 'Sentry', domain: 'sentry.io', verified: true, official: false }, { tools: true, resources: false, prompts: false }, 'local', ['verified']),
+ mkRegistry('notion-mcp', 'Notion', 'Read and search Notion pages and databases', 'notion', '📝', ['productivity'], { type: 'api_key', instructions: null }, 'stdio', { name: 'Notion', domain: 'notion.so', verified: true, official: false }, { tools: true, resources: true, prompts: false }, 'local', ['verified', 'popular']),
+ mkRegistry('datadog-mcp', 'Datadog', 'Monitor infrastructure, APM traces, and logs', 'datadog', '🐶', ['developer-tools'], { type: 'api_key', instructions: null }, 'http', { name: 'Datadog', domain: 'datadoghq.com', verified: true, official: true }, { tools: true, resources: true, prompts: false }, 'remote', ['official']),
+ mkRegistry('mongodb-mcp', 'MongoDB', 'Query collections, manage indexes, and run aggregations', 'mongodb', '🍃', ['developer-tools'], { type: 'api_key', instructions: null }, 'stdio', { name: 'MongoDB', domain: 'mongodb.com', verified: true, official: true }, { tools: true, resources: true, prompts: false }, 'local', ['official', 'verified']),
+ mkRegistry('puppeteer-mcp', 'Puppeteer', 'Browser automation — navigate, screenshot, and interact with web pages', 'puppeteer', '🎭', ['developer-tools'], { type: 'none' }, 'stdio', { name: 'Anthropic', domain: 'anthropic.com', verified: true, official: true }, { tools: true, resources: false, prompts: false }, 'local', ['official']),
+ mkRegistry('memory-mcp', 'Memory', 'Persistent memory using a knowledge graph', 'memory', '🧠', ['developer-tools'], { type: 'none' }, 'stdio', { name: 'Anthropic', domain: 'anthropic.com', verified: true, official: true }, { tools: true, resources: false, prompts: false }, 'local', ['official']),
+];
+
+const REGISTRY_CATEGORIES = [
+ { id: 'developer-tools', name: 'Developer Tools', icon: '💻' },
+ { id: 'file-system', name: 'File System', icon: '📂' },
+ { id: 'cloud', name: 'Cloud', icon: '☁️' },
+ { id: 'productivity', name: 'Productivity', icon: '⚡' },
+ { id: 'search', name: 'Search', icon: '🔍' },
+];
+
+const UI_CONFIG = {
+ filters: [
+ { id: 'category', label: 'Category', type: 'single', options: [
+ { id: 'all', label: 'All Categories' },
+ { id: 'developer-tools', label: 'Developer Tools', icon: '💻', match: { field: 'categories', operator: 'contains', value: 'developer-tools' } },
+ { id: 'cloud', label: 'Cloud', icon: '☁️', match: { field: 'categories', operator: 'contains', value: 'cloud' } },
+ { id: 'productivity', label: 'Productivity', icon: '⚡', match: { field: 'categories', operator: 'contains', value: 'productivity' } },
+ ]},
+ { id: 'auth', label: 'Auth Required', type: 'single', options: [
+ { id: 'all', label: 'All' },
+ { id: 'none', label: 'No Auth', match: { field: 'auth.type', operator: 'eq', value: 'none' } },
+ { id: 'api_key', label: 'API Key', match: { field: 'auth.type', operator: 'eq', value: 'api_key' } },
+ { id: 'oauth', label: 'OAuth', match: { field: 'auth.type', operator: 'eq', value: 'oauth' } },
+ ]},
+ ],
+ sort_options: [{ id: 'name_asc', label: 'Name A-Z', rules: [{ field: 'name', direction: 'asc' }] }],
+ default_sort: 'name_asc',
+ items_per_page: 24,
+};
+
+const HOME_CONFIG = { featured_server_ids: ['github-mcp', 'slack-mcp', 'postgres-mcp', 'cloudflare-mcp', 'stripe-mcp', 'atlassian-mcp'] };
+
+// ── IPC Mock Handler ───────────────────────────────────────
+
+function buildMockHandler() {
+ return `
+ window.__TAURI_INTERNALS__ = window.__TAURI_INTERNALS__ || {};
+ window.__TAURI_EVENT_PLUGIN_INTERNALS__ = window.__TAURI_EVENT_PLUGIN_INTERNALS__ || {};
+
+ const SPACES = ${JSON.stringify(SPACES)};
+ const INSTALLED = ${JSON.stringify(INSTALLED_SERVERS)};
+ const FEATURES = ${JSON.stringify(SERVER_FEATURES)};
+ const STATUSES = ${JSON.stringify(SERVER_STATUSES)};
+ const FSETS = ${JSON.stringify(FEATURE_SETS)};
+ const OAUTH = ${JSON.stringify(OAUTH_CLIENTS)};
+ const REGISTRY = ${JSON.stringify(REGISTRY_SERVERS)};
+ const CATEGORIES = ${JSON.stringify(REGISTRY_CATEGORIES)};
+ const UI_CFG = ${JSON.stringify(UI_CONFIG)};
+ const HOME_CFG = ${JSON.stringify(HOME_CONFIG)};
+
+ const listeners = new Map();
+
+ window.__TAURI_INTERNALS__.invoke = async function(cmd, args) {
+ switch (cmd) {
+ case 'list_spaces': return SPACES;
+ case 'get_active_space': return SPACES[0];
+ case 'get_space': return SPACES.find(s => s.id === args?.id) || SPACES[0];
+ case 'set_active_space': return null;
+ case 'create_space': return { id: crypto.randomUUID(), name: args?.name, icon: args?.icon, description: null, is_default: false, sort_order: 3, created_at: new Date().toISOString(), updated_at: new Date().toISOString() };
+ case 'get_gateway_status': return { running: true, url: 'http://localhost:9315', active_sessions: 2, connected_backends: 6 };
+ case 'start_gateway': return null;
+ case 'stop_gateway': return null;
+ case 'list_installed_servers': return INSTALLED;
+ case 'discover_servers': return REGISTRY;
+ case 'get_registry_ui_config': return UI_CFG;
+ case 'get_registry_home_config': return HOME_CFG;
+ case 'is_registry_offline': return false;
+ case 'list_registry_categories': return CATEGORIES;
+ case 'get_server_definition': return REGISTRY.find(s => s.id === args?.serverId);
+ case 'install_server': return null;
+ case 'uninstall_server': return null;
+ case 'get_server_statuses': return STATUSES;
+ case 'list_server_features': return FEATURES;
+ case 'list_server_features_by_server': return FEATURES.filter(f => f.server_id === args?.serverId);
+ case 'list_server_features_by_type': return FEATURES.filter(f => f.feature_type === args?.featureType);
+ case 'list_feature_sets': return FSETS;
+ case 'list_feature_sets_by_space': return FSETS;
+ case 'get_builtin_feature_sets': return FSETS.filter(f => f.is_builtin);
+ case 'get_oauth_clients': return OAUTH;
+ case 'list_clients': return [];
+ case 'get_all_client_grants': return {};
+ case 'get_oauth_client_grants': return {};
+ case 'refresh_oauth_tokens_on_startup': return { servers_checked: 2, tokens_refreshed: 1, refresh_failed: 0 };
+ case 'connect_all_enabled_servers': return { connected: 6, reused: 0, failed: 0, oauth_required: 0, errors: [] };
+ case 'get_pool_stats': return { total_instances: 6, connected_instances: 6, total_space_server_mappings: 6 };
+ case 'get_server_logs': return [];
+ case 'get_server_log_file': return '/home/user/.local/share/com.mcpmux.desktop/logs';
+ case 'export_config': return '{}';
+ case 'init_preset_clients': return null;
+ case 'get_logs_path': return '/home/user/.local/share/com.mcpmux.desktop/logs';
+ case 'get_startup_settings': return { autoLaunch: true, startMinimized: false, closeToTray: true };
+ case 'update_startup_settings': return null;
+ case 'open_logs_folder': return null;
+ case 'get_version': return '0.1.0';
+ case 'plugin:updater|check': throw new Error('no update');
+ case 'plugin:updater|download_and_install': throw new Error('no update');
+ default:
+ if (cmd.startsWith('plugin:')) return null;
+ console.log('[mock] unhandled:', cmd, args);
+ return null;
+ }
+ };
+
+ window.__TAURI_INTERNALS__.transformCallback = function(cb, once) {
+ const id = Math.floor(Math.random() * 2147483647);
+ window.__TAURI_INTERNALS__._cbs = window.__TAURI_INTERNALS__._cbs || {};
+ window.__TAURI_INTERNALS__._cbs[id] = cb;
+ return id;
+ };
+ window.__TAURI_INTERNALS__.unregisterCallback = function(id) {
+ if (window.__TAURI_INTERNALS__._cbs) delete window.__TAURI_INTERNALS__._cbs[id];
+ };
+ window.__TAURI_INTERNALS__.runCallback = function(id, data) {
+ const cb = window.__TAURI_INTERNALS__._cbs?.[id];
+ if (cb) cb(data);
+ };
+ window.__TAURI_INTERNALS__.callbacks = new Map();
+
+ window.__TAURI_EVENT_PLUGIN_INTERNALS__.unregisterListener = function() {};
+ window.__TAURI_INTERNALS__.metadata = { currentWindow: { label: 'main' }, currentWebview: { windowLabel: 'main', label: 'main' } };
+ `;
+}
+
+// ── Screenshot logic ───────────────────────────────────────
+
+(async () => {
+ const browser = await chromium.launch({
+ headless: true,
+ executablePath: '/root/.cache/ms-playwright/chromium-1194/chrome-linux/chrome',
+ args: ['--no-sandbox', '--disable-setuid-sandbox', '--disable-gpu', '--disable-dev-shm-usage', '--disable-software-rasterizer', '--single-process', '--no-zygote'],
+ });
+
+ const ctx = await browser.newContext({
+ viewport: { width: 1280, height: 800 },
+ deviceScaleFactor: 2,
+ colorScheme: 'light',
+ });
+
+ const page = await ctx.newPage();
+ await page.addInitScript(buildMockHandler());
+
+ // Logo mapping: emoji → local SVG path served by Vite
+ const LOGO_MAP = {
+ '🐙': '/logos/github.svg',
+ '🔷': '/logos/atlassian.svg',
+ '☁️': '/logos/cloudflare.svg',
+ '🐘': '/logos/postgresql.svg',
+ '🔵': '/logos/azure.svg',
+ '📂': '/logos/filesystem.svg',
+ '💬': '/logos/slack.svg',
+ '📁': '/logos/gdrive.svg',
+ '💳': '/logos/stripe.svg',
+ '📐': '/logos/linear.svg',
+ '🔥': '/logos/sentry.svg',
+ '📝': '/logos/notion.svg',
+ '🐶': '/logos/datadog.svg',
+ '🍃': '/logos/mongodb.svg',
+ '🎭': '/logos/puppeteer.svg',
+ '🧠': '/logos/memory.svg',
+ '🔍': '/logos/filesystem.svg',
+ };
+
+ async function injectLogos() {
+ await page.evaluate((logoMap) => {
+ const allDivs = document.querySelectorAll('div');
+ for (const div of allDivs) {
+ const text = div.textContent?.trim();
+ if (!text || !logoMap[text]) continue;
+ // Only target leaf divs (no child elements, just text)
+ if (div.children.length > 0) continue;
+ const rect = div.getBoundingClientRect();
+ // Icon containers are small: Servers page ~40px, Discover page ~30px
+ if (rect.width < 10 || rect.width > 55) continue;
+ const logoUrl = logoMap[text];
+ const size = Math.min(rect.width, rect.height) * 0.7;
+ div.innerHTML = `
`;
+ div.style.display = 'flex';
+ div.style.alignItems = 'center';
+ div.style.justifyContent = 'center';
+ div.style.overflow = 'hidden';
+ }
+ }, LOGO_MAP);
+ await page.waitForTimeout(500);
+ }
+
+ async function shot(name) {
+ await page.waitForTimeout(800);
+ await injectLogos();
+ await page.screenshot({ path: `${DIR}/${name}.png` });
+ console.log(` ok ${name}`);
+ }
+
+ console.log('Taking screenshots (light theme)...\n');
+
+ // 1. Dashboard
+ await page.goto(BASE_URL);
+ await page.waitForLoadState('networkidle');
+ await shot('dashboard');
+
+ // 2. My Servers
+ await page.locator('nav button:has-text("My Servers")').click({ force: true });
+ await page.waitForLoadState('networkidle');
+ await shot('servers');
+
+ // 3. Discover / Registry
+ await page.locator('nav button:has-text("Discover")').click({ force: true });
+ await page.waitForLoadState('networkidle');
+ await shot('discover');
+
+ // 4. Spaces
+ await page.locator('nav button:has-text("Spaces")').last().click({ force: true });
+ await page.waitForLoadState('networkidle');
+ await shot('spaces');
+
+ // 5. Feature Sets
+ await page.locator('nav button:has-text("FeatureSets")').click({ force: true });
+ await page.waitForLoadState('networkidle');
+ await shot('featuresets');
+
+ // 6. Clients
+ await page.locator('nav button:has-text("Clients")').click({ force: true });
+ await page.waitForLoadState('networkidle');
+ await shot('clients');
+
+ // 7. Settings
+ await page.locator('nav button:has-text("Settings")').click({ force: true });
+ await page.waitForLoadState('networkidle');
+ await page.waitForTimeout(1000);
+ await shot('settings');
+
+ await browser.close();
+ console.log('\nDone!');
+})();