Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion .github/ISSUE_TEMPLATE/config.yml
Original file line number Diff line number Diff line change
@@ -1,8 +1,17 @@
blank_issues_enabled: false
blank_issues_enabled: true
contact_links:
- name: Questions & Help
url: https://github.com/mcpmux/mcp-mux/discussions/categories/q-a
about: Ask questions and get help in GitHub Discussions
- name: Feature Ideas
url: https://github.com/mcpmux/mcp-mux/discussions/categories/ideas
about: Share and discuss feature ideas
- name: Contribute a Server Definition (PR)
url: https://github.com/mcpmux/mcp-servers/blob/main/CONTRIBUTING.md
about: Server definitions live in the mcp-servers repo and land via PR — read the guide
- name: Request a Server
url: https://github.com/mcpmux/mcp-servers/issues/new?template=request-server.yml
about: Ask the community to add an MCP server to the registry
- name: Report a Server Definition Bug
url: https://github.com/mcpmux/mcp-servers/issues/new?template=bug-report.yml
about: Found a broken or incorrect server in the registry? Report it here
7 changes: 7 additions & 0 deletions apps/desktop/src/components/ConfigEditorModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import Editor, { type Monaco } from '@monaco-editor/react';
import type { editor } from 'monaco-editor';
import { useToast, ToastContainer } from '@mcpmux/ui';
import USER_SPACE_CONFIG_SCHEMA from '../../../../schemas/user-space.schema.json';
import { RequestServerCTA } from './Contribute';

interface ConfigEditorModalProps {
spaceId: string;
Expand Down Expand Up @@ -221,6 +222,12 @@ export function ConfigEditorModal({ spaceId, spaceName, onClose, onSaved }: Conf
</span>
</div>

{/* Contribute / Request CTA — surfaces the registry templates so users
don't have to hand-roll a definition if one already exists upstream. */}
<div className="px-4 py-3 border-b border-[rgb(var(--border))] bg-[rgb(var(--surface-dim))]">
<RequestServerCTA />
</div>

{/* Editor Area */}
<div className="flex-1 relative min-h-0 bg-[#1e1e1e]">
{(isLoading || !editorReady) ? (
Expand Down
152 changes: 152 additions & 0 deletions apps/desktop/src/components/Contribute.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
import { useEffect, useRef, useState } from 'react';
import { Bug, Github, Heart, Lightbulb, Package, SendHorizontal } from 'lucide-react';
import { Button } from '@mcpmux/ui';
import { CONTRIBUTE, openExternal } from '@/lib/contribute';

/**
* Inline "Didn't find your server?" CTA used on empty search states in the
* Registry page and the Add-Custom-Server flow.
*
* Ships two buttons side-by-side: **Request** (opens a pre-labelled GitHub
* issue in mcp-servers with the search term in the title) and
* **Contribute** (opens the mcp-servers CONTRIBUTING guide).
*/
export function RequestServerCTA({
searchTerm,
className,
}: {
searchTerm?: string;
className?: string;
}) {
return (
<div
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 ?? ''}`}
data-testid="request-server-cta"
>
<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">
<Package className="h-4 w-4" />
</div>
<div className="flex-1 min-w-0">
<p className="text-sm font-medium">Don&apos;t see what you need?</p>
<p className="text-xs text-[rgb(var(--muted))] mt-0.5">
{searchTerm
? `We couldn't find "${searchTerm}". Request it from the community registry or open a PR yourself.`
: 'Request a new server in the community registry, or add one yourself via a pull request.'}
</p>
</div>
<div className="flex items-center gap-2 flex-shrink-0">
<Button
variant="primary"
size="sm"
onClick={() => openExternal(CONTRIBUTE.requestServer(searchTerm))}
data-testid="request-server-btn"
>
<SendHorizontal className="h-3 w-3 mr-1.5" />
Request
</Button>
<Button
variant="secondary"
size="sm"
onClick={() => openExternal(CONTRIBUTE.contributeServer)}
data-testid="contribute-server-btn"
>
<Github className="h-3 w-3 mr-1.5" />
Contribute
</Button>
</div>
</div>
);
}

/**
* A persistent "Contribute / Report" dropdown menu — the single global
* affordance for: open GitHub repo, report a bug, request a feature, open
* the server registry. Place wherever you want a friendly "help make mcpmux
* better" call-to-action.
*/
export function ContributeMenu({
variant = 'ghost',
size = 'sm',
}: {
variant?: 'primary' | 'secondary' | 'ghost';
size?: 'sm' | 'md';
}) {
const [open, setOpen] = useState(false);
const ref = useRef<HTMLDivElement>(null);

useEffect(() => {
if (!open) return;
const handler = (e: MouseEvent) => {
if (ref.current && !ref.current.contains(e.target as Node)) setOpen(false);
};
document.addEventListener('mousedown', handler);
return () => document.removeEventListener('mousedown', handler);
}, [open]);

const items = [
{
label: 'Request a new server',
caption: 'Ask the community to add an MCP server to the registry',
icon: Package,
href: CONTRIBUTE.requestServer(),
},
{
label: 'Report a bug',
caption: 'Something broken in the desktop app or gateway',
icon: Bug,
href: CONTRIBUTE.bug,
},
{
label: 'Suggest a feature',
caption: 'An idea for mcpmux itself',
icon: Lightbulb,
href: CONTRIBUTE.featureRequest,
},
{
label: 'Open on GitHub',
caption: 'Browse source, issues, pull requests',
icon: Github,
href: CONTRIBUTE.repo,
},
];

return (
<div className="relative inline-block" ref={ref}>
<Button
variant={variant}
size={size}
onClick={() => setOpen((v) => !v)}
data-testid="contribute-menu-trigger"
>
<Heart className="h-4 w-4 mr-1.5" />
Contribute
</Button>
{open && (
<div
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"
data-testid="contribute-menu"
>
{items.map((item) => (
<button
key={item.label}
type="button"
onClick={() => {
setOpen(false);
openExternal(item.href);
}}
className="w-full text-left flex items-start gap-3 px-3 py-2.5 rounded-lg hover:bg-[rgb(var(--surface))] transition-colors"
>
<item.icon className="h-4 w-4 mt-0.5 text-[rgb(var(--muted))] flex-shrink-0" />
<div className="flex-1 min-w-0">
<p className="text-sm font-medium">{item.label}</p>
<p className="text-[11px] text-[rgb(var(--muted))] leading-snug">
{item.caption}
</p>
</div>
</button>
))}
</div>
)}
</div>
);
}
29 changes: 20 additions & 9 deletions apps/desktop/src/features/registry/RegistryPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { ServerCard } from './ServerCard';
import { ServerDetailModal } from './ServerDetailModal';
import { useViewSpace, useNavigateTo } from '@/stores';
import { capture } from '@/lib/analytics';
import { RequestServerCTA, ContributeMenu } from '@/components/Contribute';

export function RegistryPage() {
const {
Expand Down Expand Up @@ -140,13 +141,18 @@ export function RegistryPage() {
<ToastContainer toasts={toasts} onClose={dismiss} />
{/* Header */}
<div className="p-6 border-b border-[rgb(var(--border-subtle))]">
<div className="flex items-center gap-3 mb-1">
<h1 className="text-2xl font-bold" data-testid="registry-title">Discover Servers</h1>
{isOffline && (
<span className="px-2 py-0.5 text-xs font-medium bg-amber-500/20 text-amber-600 dark:text-amber-400 rounded-full">
Offline
</span>
)}
<div className="flex items-center justify-between gap-3 mb-1">
<div className="flex items-center gap-3">
<h1 className="text-2xl font-bold" data-testid="registry-title">Discover Servers</h1>
{isOffline && (
<span className="px-2 py-0.5 text-xs font-medium bg-amber-500/20 text-amber-600 dark:text-amber-400 rounded-full">
Offline
</span>
)}
</div>
{/* Always-reachable contribute menu — users don't have to trigger
an empty search to find the request / bug / feature links. */}
<ContributeMenu variant="ghost" size="sm" />
</div>
<p className="text-sm text-[rgb(var(--muted))]">
{isOffline
Expand Down Expand Up @@ -242,7 +248,7 @@ export function RegistryPage() {
<div className="animate-spin rounded-full h-8 w-8 border-2 border-[rgb(var(--primary))] border-t-transparent" />
</div>
) : displayServers.length === 0 ? (
<div className="flex flex-col items-center justify-center h-full text-[rgb(var(--muted))]">
<div className="flex flex-col items-center justify-center h-full text-[rgb(var(--muted))] px-8">
<svg className="w-16 h-16 mb-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path
strokeLinecap="round"
Expand All @@ -252,7 +258,12 @@ export function RegistryPage() {
/>
</svg>
<p className="text-lg">No servers found</p>
<p className="text-sm">Try adjusting your search or filters</p>
<p className="text-sm mb-6">Try adjusting your search or filters</p>
{/* Empty-search CTA — push the user toward requesting or
contributing the missing server rather than just giving up. */}
<div className="w-full max-w-xl">
<RequestServerCTA searchTerm={searchQuery || undefined} />
</div>
</div>
) : (
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-4">
Expand Down
87 changes: 87 additions & 0 deletions apps/desktop/src/features/settings/SettingsPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,15 @@ import {
XCircle,
Trash2,
BarChart3,
Github,
Bug,
Lightbulb,
Package,
Heart,
} from 'lucide-react';
import { useAppStore, useTheme, useAnalyticsEnabled } from '@/stores';
import { UpdateChecker } from './UpdateChecker';
import { CONTRIBUTE, openExternal } from '@/lib/contribute';

interface StartupSettings {
autoLaunch: boolean;
Expand Down Expand Up @@ -337,6 +343,54 @@ export function SettingsPage() {
</CardContent>
</Card>

{/* Contribute & feedback — the single global "help make mcpmux
better" card. Mirrors the items in <ContributeMenu> so power
users have quick access without digging into GitHub. */}
<Card data-testid="settings-contribute-section">
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Heart className="h-5 w-5" />
Contribute &amp; feedback
</CardTitle>
<CardDescription>
mcpmux is open source. Request a server, report a bug, suggest a feature, or jump
straight to the source.
</CardDescription>
</CardHeader>
<CardContent>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
<ContributeRow
icon={Package}
title="Request a new server"
subtitle="Ask the community to add an MCP server to the registry"
onClick={() => openExternal(CONTRIBUTE.requestServer())}
testId="contribute-request-server"
/>
<ContributeRow
icon={Bug}
title="Report a bug"
subtitle="Something broken in the desktop app or gateway"
onClick={() => openExternal(CONTRIBUTE.bug)}
testId="contribute-report-bug"
/>
<ContributeRow
icon={Lightbulb}
title="Suggest a feature"
subtitle="An idea for mcpmux itself"
onClick={() => openExternal(CONTRIBUTE.featureRequest)}
testId="contribute-feature-request"
/>
<ContributeRow
icon={Github}
title="Open on GitHub"
subtitle="Browse source, issues, pull requests"
onClick={() => openExternal(CONTRIBUTE.repo)}
testId="contribute-open-github"
/>
</div>
</CardContent>
</Card>

{/* Logs Section */}
<Card>
<CardHeader>
Expand Down Expand Up @@ -407,3 +461,36 @@ export function SettingsPage() {
</>
);
}

/**
* Flat row used inside the Contribute card. Local to the Settings page — if
* we ever need this elsewhere, promote it into @mcpmux/ui.
*/
function ContributeRow({
icon: Icon,
title,
subtitle,
onClick,
testId,
}: {
icon: React.ComponentType<{ className?: string }>;
title: string;
subtitle: string;
onClick: () => void;
testId?: string;
}) {
return (
<button
type="button"
onClick={onClick}
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"
data-testid={testId}
>
<Icon className="h-4 w-4 mt-0.5 text-[rgb(var(--muted))] flex-shrink-0" />
<div className="min-w-0">
<p className="text-sm font-medium">{title}</p>
<p className="text-[11px] text-[rgb(var(--muted))] leading-snug mt-0.5">{subtitle}</p>
</div>
</button>
);
}
Loading
Loading