Skip to content

Commit 3cd9d8a

Browse files
committed
feat(ui): Contribute / Request affordances across the app
Users who hit a dead-end ("searched for a server that isn't in the registry", "found a bug", "wanted to suggest a feature") had no clear place to send that signal — they'd just close the app. This commit adds three coordinated entry points, all wired to the mcpmux and mcp-servers GitHub repos via the Tauri opener plugin. New shared module `lib/contribute.ts` * Centralises every external URL (main repo, servers repo, marketing site, bug-report, feature-request, request-server templates). * `CONTRIBUTE.requestServer(searchTerm?)` URL-encodes the search term into the GitHub issue title so a user coming from the empty-search state lands on a pre-populated form. * `openExternal(url)` wraps `openUrl` with the same opener-plugin fallback OAuthConsentModal uses. New shared components `components/Contribute.tsx` * `<RequestServerCTA searchTerm?>` — inline gradient card used on the empty-search state. Two-button footer: Request (gh issue) + Contribute (mcp-servers CONTRIBUTING.md). * `<ContributeMenu>` — dropdown with Request new server / Report bug / Suggest feature / Open on GitHub. Reusable anywhere; lives in the Registry header today. Placements: * Registry page — ContributeMenu in the header (always visible), and RequestServerCTA rendered in the empty-results state with the active searchQuery threaded into the issue title. * Settings — new "Contribute & feedback" card with a 2-column grid of Request-server / Report-bug / Suggest-feature / Open-on-GitHub tiles (ContributeRow helper local to this file). All links open in the user's default browser via the opener plugin; no in-webview navigation. `pnpm typecheck` + lint clean (warnings unchanged at 31). Signed-off-by: Mohammod Al Amin Ashik <maa.ashik00@gmail.com>
1 parent fb58d9c commit 3cd9d8a

4 files changed

Lines changed: 312 additions & 9 deletions

File tree

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
import { useEffect, useRef, useState } from 'react';
2+
import { Bug, Github, Heart, Lightbulb, Package, SendHorizontal } from 'lucide-react';
3+
import { Button } from '@mcpmux/ui';
4+
import { CONTRIBUTE, openExternal } from '@/lib/contribute';
5+
6+
/**
7+
* Inline "Didn't find your server?" CTA used on empty search states in the
8+
* Registry page and the Add-Custom-Server flow.
9+
*
10+
* Ships two buttons side-by-side: **Request** (opens a pre-labelled GitHub
11+
* issue in mcp-servers with the search term in the title) and
12+
* **Contribute** (opens the mcp-servers CONTRIBUTING guide).
13+
*/
14+
export function RequestServerCTA({
15+
searchTerm,
16+
className,
17+
}: {
18+
searchTerm?: string;
19+
className?: string;
20+
}) {
21+
return (
22+
<div
23+
className={`rounded-xl border border-primary-200/60 dark:border-primary-800/40 bg-gradient-to-br from-primary-50/50 to-transparent dark:from-primary-900/10 p-4 flex flex-col sm:flex-row items-start sm:items-center gap-3 ${className ?? ''}`}
24+
data-testid="request-server-cta"
25+
>
26+
<div className="flex h-9 w-9 items-center justify-center rounded-lg bg-primary-500/10 text-primary-600 dark:text-primary-300 flex-shrink-0">
27+
<Package className="h-4 w-4" />
28+
</div>
29+
<div className="flex-1 min-w-0">
30+
<p className="text-sm font-medium">Don&apos;t see what you need?</p>
31+
<p className="text-xs text-[rgb(var(--muted))] mt-0.5">
32+
{searchTerm
33+
? `We couldn't find "${searchTerm}". Request it from the community registry or open a PR yourself.`
34+
: 'Request a new server in the community registry, or add one yourself via a pull request.'}
35+
</p>
36+
</div>
37+
<div className="flex items-center gap-2 flex-shrink-0">
38+
<Button
39+
variant="primary"
40+
size="sm"
41+
onClick={() => openExternal(CONTRIBUTE.requestServer(searchTerm))}
42+
data-testid="request-server-btn"
43+
>
44+
<SendHorizontal className="h-3 w-3 mr-1.5" />
45+
Request
46+
</Button>
47+
<Button
48+
variant="secondary"
49+
size="sm"
50+
onClick={() => openExternal(CONTRIBUTE.contributeServer)}
51+
data-testid="contribute-server-btn"
52+
>
53+
<Github className="h-3 w-3 mr-1.5" />
54+
Contribute
55+
</Button>
56+
</div>
57+
</div>
58+
);
59+
}
60+
61+
/**
62+
* A persistent "Contribute / Report" dropdown menu — the single global
63+
* affordance for: open GitHub repo, report a bug, request a feature, open
64+
* the server registry. Place wherever you want a friendly "help make mcpmux
65+
* better" call-to-action.
66+
*/
67+
export function ContributeMenu({
68+
variant = 'ghost',
69+
size = 'sm',
70+
}: {
71+
variant?: 'primary' | 'secondary' | 'ghost';
72+
size?: 'sm' | 'md';
73+
}) {
74+
const [open, setOpen] = useState(false);
75+
const ref = useRef<HTMLDivElement>(null);
76+
77+
useEffect(() => {
78+
if (!open) return;
79+
const handler = (e: MouseEvent) => {
80+
if (ref.current && !ref.current.contains(e.target as Node)) setOpen(false);
81+
};
82+
document.addEventListener('mousedown', handler);
83+
return () => document.removeEventListener('mousedown', handler);
84+
}, [open]);
85+
86+
const items = [
87+
{
88+
label: 'Request a new server',
89+
caption: 'Ask the community to add an MCP server to the registry',
90+
icon: Package,
91+
href: CONTRIBUTE.requestServer(),
92+
},
93+
{
94+
label: 'Report a bug',
95+
caption: 'Something broken in the desktop app or gateway',
96+
icon: Bug,
97+
href: CONTRIBUTE.bug,
98+
},
99+
{
100+
label: 'Suggest a feature',
101+
caption: 'An idea for mcpmux itself',
102+
icon: Lightbulb,
103+
href: CONTRIBUTE.featureRequest,
104+
},
105+
{
106+
label: 'Open on GitHub',
107+
caption: 'Browse source, issues, pull requests',
108+
icon: Github,
109+
href: CONTRIBUTE.repo,
110+
},
111+
];
112+
113+
return (
114+
<div className="relative inline-block" ref={ref}>
115+
<Button
116+
variant={variant}
117+
size={size}
118+
onClick={() => setOpen((v) => !v)}
119+
data-testid="contribute-menu-trigger"
120+
>
121+
<Heart className="h-4 w-4 mr-1.5" />
122+
Contribute
123+
</Button>
124+
{open && (
125+
<div
126+
className="absolute right-0 mt-2 z-20 w-72 rounded-xl border border-[rgb(var(--border))] bg-white dark:bg-zinc-900 shadow-xl p-1"
127+
data-testid="contribute-menu"
128+
>
129+
{items.map((item) => (
130+
<button
131+
key={item.label}
132+
type="button"
133+
onClick={() => {
134+
setOpen(false);
135+
openExternal(item.href);
136+
}}
137+
className="w-full text-left flex items-start gap-3 px-3 py-2.5 rounded-lg hover:bg-[rgb(var(--surface))] transition-colors"
138+
>
139+
<item.icon className="h-4 w-4 mt-0.5 text-[rgb(var(--muted))] flex-shrink-0" />
140+
<div className="flex-1 min-w-0">
141+
<p className="text-sm font-medium">{item.label}</p>
142+
<p className="text-[11px] text-[rgb(var(--muted))] leading-snug">
143+
{item.caption}
144+
</p>
145+
</div>
146+
</button>
147+
))}
148+
</div>
149+
)}
150+
</div>
151+
);
152+
}

apps/desktop/src/features/registry/RegistryPage.tsx

Lines changed: 20 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { ServerCard } from './ServerCard';
1212
import { ServerDetailModal } from './ServerDetailModal';
1313
import { useViewSpace, useNavigateTo } from '@/stores';
1414
import { capture } from '@/lib/analytics';
15+
import { RequestServerCTA, ContributeMenu } from '@/components/Contribute';
1516

1617
export function RegistryPage() {
1718
const {
@@ -140,13 +141,18 @@ export function RegistryPage() {
140141
<ToastContainer toasts={toasts} onClose={dismiss} />
141142
{/* Header */}
142143
<div className="p-6 border-b border-[rgb(var(--border-subtle))]">
143-
<div className="flex items-center gap-3 mb-1">
144-
<h1 className="text-2xl font-bold" data-testid="registry-title">Discover Servers</h1>
145-
{isOffline && (
146-
<span className="px-2 py-0.5 text-xs font-medium bg-amber-500/20 text-amber-600 dark:text-amber-400 rounded-full">
147-
Offline
148-
</span>
149-
)}
144+
<div className="flex items-center justify-between gap-3 mb-1">
145+
<div className="flex items-center gap-3">
146+
<h1 className="text-2xl font-bold" data-testid="registry-title">Discover Servers</h1>
147+
{isOffline && (
148+
<span className="px-2 py-0.5 text-xs font-medium bg-amber-500/20 text-amber-600 dark:text-amber-400 rounded-full">
149+
Offline
150+
</span>
151+
)}
152+
</div>
153+
{/* Always-reachable contribute menu — users don't have to trigger
154+
an empty search to find the request / bug / feature links. */}
155+
<ContributeMenu variant="ghost" size="sm" />
150156
</div>
151157
<p className="text-sm text-[rgb(var(--muted))]">
152158
{isOffline
@@ -242,7 +248,7 @@ export function RegistryPage() {
242248
<div className="animate-spin rounded-full h-8 w-8 border-2 border-[rgb(var(--primary))] border-t-transparent" />
243249
</div>
244250
) : displayServers.length === 0 ? (
245-
<div className="flex flex-col items-center justify-center h-full text-[rgb(var(--muted))]">
251+
<div className="flex flex-col items-center justify-center h-full text-[rgb(var(--muted))] px-8">
246252
<svg className="w-16 h-16 mb-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
247253
<path
248254
strokeLinecap="round"
@@ -252,7 +258,12 @@ export function RegistryPage() {
252258
/>
253259
</svg>
254260
<p className="text-lg">No servers found</p>
255-
<p className="text-sm">Try adjusting your search or filters</p>
261+
<p className="text-sm mb-6">Try adjusting your search or filters</p>
262+
{/* Empty-search CTA — push the user toward requesting or
263+
contributing the missing server rather than just giving up. */}
264+
<div className="w-full max-w-xl">
265+
<RequestServerCTA searchTerm={searchQuery || undefined} />
266+
</div>
256267
</div>
257268
) : (
258269
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-4">

apps/desktop/src/features/settings/SettingsPage.tsx

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,15 @@ import {
2323
XCircle,
2424
Trash2,
2525
BarChart3,
26+
Github,
27+
Bug,
28+
Lightbulb,
29+
Package,
30+
Heart,
2631
} from 'lucide-react';
2732
import { useAppStore, useTheme, useAnalyticsEnabled } from '@/stores';
2833
import { UpdateChecker } from './UpdateChecker';
34+
import { CONTRIBUTE, openExternal } from '@/lib/contribute';
2935

3036
interface StartupSettings {
3137
autoLaunch: boolean;
@@ -337,6 +343,54 @@ export function SettingsPage() {
337343
</CardContent>
338344
</Card>
339345

346+
{/* Contribute & feedback — the single global "help make mcpmux
347+
better" card. Mirrors the items in <ContributeMenu> so power
348+
users have quick access without digging into GitHub. */}
349+
<Card data-testid="settings-contribute-section">
350+
<CardHeader>
351+
<CardTitle className="flex items-center gap-2">
352+
<Heart className="h-5 w-5" />
353+
Contribute &amp; feedback
354+
</CardTitle>
355+
<CardDescription>
356+
mcpmux is open source. Request a server, report a bug, suggest a feature, or jump
357+
straight to the source.
358+
</CardDescription>
359+
</CardHeader>
360+
<CardContent>
361+
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
362+
<ContributeRow
363+
icon={Package}
364+
title="Request a new server"
365+
subtitle="Ask the community to add an MCP server to the registry"
366+
onClick={() => openExternal(CONTRIBUTE.requestServer())}
367+
testId="contribute-request-server"
368+
/>
369+
<ContributeRow
370+
icon={Bug}
371+
title="Report a bug"
372+
subtitle="Something broken in the desktop app or gateway"
373+
onClick={() => openExternal(CONTRIBUTE.bug)}
374+
testId="contribute-report-bug"
375+
/>
376+
<ContributeRow
377+
icon={Lightbulb}
378+
title="Suggest a feature"
379+
subtitle="An idea for mcpmux itself"
380+
onClick={() => openExternal(CONTRIBUTE.featureRequest)}
381+
testId="contribute-feature-request"
382+
/>
383+
<ContributeRow
384+
icon={Github}
385+
title="Open on GitHub"
386+
subtitle="Browse source, issues, pull requests"
387+
onClick={() => openExternal(CONTRIBUTE.repo)}
388+
testId="contribute-open-github"
389+
/>
390+
</div>
391+
</CardContent>
392+
</Card>
393+
340394
{/* Logs Section */}
341395
<Card>
342396
<CardHeader>
@@ -407,3 +461,36 @@ export function SettingsPage() {
407461
</>
408462
);
409463
}
464+
465+
/**
466+
* Flat row used inside the Contribute card. Local to the Settings page — if
467+
* we ever need this elsewhere, promote it into @mcpmux/ui.
468+
*/
469+
function ContributeRow({
470+
icon: Icon,
471+
title,
472+
subtitle,
473+
onClick,
474+
testId,
475+
}: {
476+
icon: React.ComponentType<{ className?: string }>;
477+
title: string;
478+
subtitle: string;
479+
onClick: () => void;
480+
testId?: string;
481+
}) {
482+
return (
483+
<button
484+
type="button"
485+
onClick={onClick}
486+
className="text-left flex items-start gap-3 p-3 rounded-lg border border-[rgb(var(--border-subtle))] bg-[rgb(var(--surface))] hover:border-primary-400/60 hover:bg-primary-500/5 transition-colors"
487+
data-testid={testId}
488+
>
489+
<Icon className="h-4 w-4 mt-0.5 text-[rgb(var(--muted))] flex-shrink-0" />
490+
<div className="min-w-0">
491+
<p className="text-sm font-medium">{title}</p>
492+
<p className="text-[11px] text-[rgb(var(--muted))] leading-snug mt-0.5">{subtitle}</p>
493+
</div>
494+
</button>
495+
);
496+
}

apps/desktop/src/lib/contribute.ts

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
/**
2+
* Links + helpers for "Contribute / Request / Report" CTAs scattered across
3+
* the app (registry empty-state, settings, etc.).
4+
*
5+
* All URLs live here so we can update the target org / repo / site from one
6+
* place instead of grepping for hardcoded strings.
7+
*
8+
* Open-in-browser goes through `openUrl` (our Tauri command wrapping
9+
* `tauri-plugin-opener`) so the user's default browser handles the URL
10+
* rather than loading it inside the webview.
11+
*/
12+
13+
import { openUrl } from '@/lib/api/gateway';
14+
15+
export const CONTRIBUTE = {
16+
/** Main desktop + gateway repo. */
17+
repo: 'https://github.com/mcpmux/mcp-mux',
18+
/** Community-maintained server-definition registry. */
19+
serversRepo: 'https://github.com/mcpmux/mcp-servers',
20+
/** Marketing site. */
21+
site: 'https://mcpmux.com',
22+
/** New bug report, pre-labelled. */
23+
bug: 'https://github.com/mcpmux/mcp-mux/issues/new?labels=bug',
24+
/** Feature request for the app itself. */
25+
featureRequest:
26+
'https://github.com/mcpmux/mcp-mux/issues/new?labels=enhancement',
27+
/**
28+
* Request a new server definition in the community registry. Encodes the
29+
* user's search term into the issue title when provided.
30+
*/
31+
requestServer(searchTerm?: string): string {
32+
const base =
33+
'https://github.com/mcpmux/mcp-servers/issues/new?labels=server-request';
34+
if (!searchTerm) return base;
35+
const title = encodeURIComponent(`Request: ${searchTerm.slice(0, 120)}`);
36+
return `${base}&title=${title}`;
37+
},
38+
/** Root of the server-definitions contributing guide. */
39+
contributeServer: 'https://github.com/mcpmux/mcp-servers/blob/main/CONTRIBUTING.md',
40+
} as const;
41+
42+
/**
43+
* Open an external URL via the Tauri opener plugin. Falls back to the plugin
44+
* directly if our gateway wrapper fails (mirrors OAuthConsentModal's pattern).
45+
*/
46+
export async function openExternal(url: string): Promise<void> {
47+
try {
48+
await openUrl(url);
49+
} catch {
50+
const { openUrl: plugin } = await import('@tauri-apps/plugin-opener');
51+
await plugin(url);
52+
}
53+
}

0 commit comments

Comments
 (0)