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. + +![My Servers](docs/screenshots/servers.png) + +### 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. + +![Discover Servers](docs/screenshots/discover.png) + +### Spaces + +Isolated workspaces with their own servers and credentials. Switch between "Work" and "Personal" in one click — your AI clients follow automatically. ![Workspaces](docs/screenshots/spaces.png) -**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. +![Feature Sets](docs/screenshots/featuresets.png) -**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](docs/screenshots/settings.png) +![Connected Clients](docs/screenshots/clients.png) + +### Settings -**Auto-Updates** — Signed updates delivered automatically. +Auto-updates, launch at startup, system tray, and appearance preferences. + +![Settings](docs/screenshots/settings.png) --- 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 @@ +Atlassian \ 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 @@ +Microsoft Azure \ 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 @@ +Cloudflare \ 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 @@ +Datadog \ 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 @@ +Files \ 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 @@ +Google Drive \ 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 @@ +GitHub \ 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 @@ +Linear \ 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 @@ +GraphQL \ 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 @@ +MongoDB \ 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 @@ +Notion \ 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 @@ +PostgreSQL \ 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 @@ +Puppeteer \ 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 @@ +Sentry \ 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 @@ +Slack \ 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 @@ +Stripe \ 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!'); +})();