diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index bbecdcf2..fc66845d 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -1,4 +1,4 @@ -blank_issues_enabled: false +blank_issues_enabled: true contact_links: - name: Questions & Help url: https://github.com/mcpmux/mcp-mux/discussions/categories/q-a @@ -6,3 +6,12 @@ contact_links: - 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 diff --git a/apps/desktop/src/components/ConfigEditorModal.tsx b/apps/desktop/src/components/ConfigEditorModal.tsx index e508b0ae..4c8ffca6 100644 --- a/apps/desktop/src/components/ConfigEditorModal.tsx +++ b/apps/desktop/src/components/ConfigEditorModal.tsx @@ -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; @@ -221,6 +222,12 @@ export function ConfigEditorModal({ spaceId, spaceName, onClose, onSaved }: Conf + {/* Contribute / Request CTA — surfaces the registry templates so users + don't have to hand-roll a definition if one already exists upstream. */} +
+ +
+ {/* Editor Area */}
{(isLoading || !editorReady) ? ( diff --git a/apps/desktop/src/components/Contribute.tsx b/apps/desktop/src/components/Contribute.tsx new file mode 100644 index 00000000..b6ba1acf --- /dev/null +++ b/apps/desktop/src/components/Contribute.tsx @@ -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 ( +
+
+ +
+
+

Don't see what you need?

+

+ {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.'} +

+
+
+ + +
+
+ ); +} + +/** + * 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(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 ( +
+ + {open && ( +
+ {items.map((item) => ( + + ))} +
+ )} +
+ ); +} diff --git a/apps/desktop/src/features/registry/RegistryPage.tsx b/apps/desktop/src/features/registry/RegistryPage.tsx index dda4e672..bd9b43a6 100644 --- a/apps/desktop/src/features/registry/RegistryPage.tsx +++ b/apps/desktop/src/features/registry/RegistryPage.tsx @@ -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 { @@ -140,13 +141,18 @@ export function RegistryPage() { {/* Header */}
-
-

Discover Servers

- {isOffline && ( - - Offline - - )} +
+
+

Discover Servers

+ {isOffline && ( + + Offline + + )} +
+ {/* Always-reachable contribute menu — users don't have to trigger + an empty search to find the request / bug / feature links. */} +

{isOffline @@ -242,7 +248,7 @@ export function RegistryPage() {

) : displayServers.length === 0 ? ( -
+

No servers found

-

Try adjusting your search or filters

+

Try adjusting your search or filters

+ {/* Empty-search CTA — push the user toward requesting or + contributing the missing server rather than just giving up. */} +
+ +
) : (
diff --git a/apps/desktop/src/features/settings/SettingsPage.tsx b/apps/desktop/src/features/settings/SettingsPage.tsx index 1fda7757..2026c080 100644 --- a/apps/desktop/src/features/settings/SettingsPage.tsx +++ b/apps/desktop/src/features/settings/SettingsPage.tsx @@ -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; @@ -337,6 +343,54 @@ export function SettingsPage() { + {/* Contribute & feedback — the single global "help make mcpmux + better" card. Mirrors the items in so power + users have quick access without digging into GitHub. */} + + + + + Contribute & feedback + + + mcpmux is open source. Request a server, report a bug, suggest a feature, or jump + straight to the source. + + + +
+ openExternal(CONTRIBUTE.requestServer())} + testId="contribute-request-server" + /> + openExternal(CONTRIBUTE.bug)} + testId="contribute-report-bug" + /> + openExternal(CONTRIBUTE.featureRequest)} + testId="contribute-feature-request" + /> + openExternal(CONTRIBUTE.repo)} + testId="contribute-open-github" + /> +
+
+
+ {/* Logs Section */} @@ -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 ( + + ); +} diff --git a/apps/desktop/src/lib/contribute.ts b/apps/desktop/src/lib/contribute.ts new file mode 100644 index 00000000..8d121ad7 --- /dev/null +++ b/apps/desktop/src/lib/contribute.ts @@ -0,0 +1,61 @@ +/** + * Links + helpers for "Contribute / Request / Report" CTAs scattered across + * the app (registry empty-state, settings, etc.). + * + * All URLs live here so we can update the target org / repo / site from one + * place instead of grepping for hardcoded strings. + * + * Open-in-browser goes through `openUrl` (our Tauri command wrapping + * `tauri-plugin-opener`) so the user's default browser handles the URL + * rather than loading it inside the webview. + */ + +import { openUrl } from '@/lib/api/gateway'; + +export const CONTRIBUTE = { + /** Main desktop + gateway repo. */ + repo: 'https://github.com/mcpmux/mcp-mux', + /** Community-maintained server-definition registry. */ + serversRepo: 'https://github.com/mcpmux/mcp-servers', + /** Marketing site. */ + site: 'https://mcpmux.com', + /** New bug report, pre-filled with the bug_report template. */ + bug: 'https://github.com/mcpmux/mcp-mux/issues/new?template=bug_report.yml', + /** Feature request for the app itself, pre-filled with the feature_request template. */ + featureRequest: + 'https://github.com/mcpmux/mcp-mux/issues/new?template=feature_request.yml', + /** + * Request a new server definition in the community registry. Opens the + * `request-server.yml` issue template and encodes the user's search term + * into the title when provided. + */ + requestServer(searchTerm?: string): string { + const base = + 'https://github.com/mcpmux/mcp-servers/issues/new?template=request-server.yml'; + if (!searchTerm) return base; + const title = encodeURIComponent(`[Request] ${searchTerm.slice(0, 120)}`); + return `${base}&title=${title}`; + }, + /** + * Contribute a new server definition — points at the registry's + * CONTRIBUTING guide. Server definitions are JSON files landed via PR, + * not issues, so we send users straight down the fork → PR path. + */ + contributeServer: 'https://github.com/mcpmux/mcp-servers/blob/main/CONTRIBUTING.md', + /** Report a bug in an existing server definition. */ + serverDefinitionBug: + 'https://github.com/mcpmux/mcp-servers/issues/new?template=bug-report.yml', +} as const; + +/** + * Open an external URL via the Tauri opener plugin. Falls back to the plugin + * directly if our gateway wrapper fails (mirrors OAuthConsentModal's pattern). + */ +export async function openExternal(url: string): Promise { + try { + await openUrl(url); + } catch { + const { openUrl: plugin } = await import('@tauri-apps/plugin-opener'); + await plugin(url); + } +}