mirror of
https://github.com/samanhappy/mcphub.git
synced 2025-12-23 18:29:21 -05:00
Add one-click installation dialog for servers and groups
Co-authored-by: samanhappy <2755122+samanhappy@users.noreply.github.com>
This commit is contained in:
@@ -1,10 +1,11 @@
|
||||
import { useState, useRef, useEffect } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Group, Server, IGroupServerConfig } from '@/types'
|
||||
import { Edit, Trash, Copy, Check, Link, FileCode, DropdownIcon, Wrench } from '@/components/icons/LucideIcons'
|
||||
import { Edit, Trash, Copy, Check, Link, FileCode, DropdownIcon, Wrench, Download } from '@/components/icons/LucideIcons'
|
||||
import DeleteDialog from '@/components/ui/DeleteDialog'
|
||||
import { useToast } from '@/contexts/ToastContext'
|
||||
import { useSettingsData } from '@/hooks/useSettingsData'
|
||||
import InstallToClientDialog from '@/components/InstallToClientDialog'
|
||||
|
||||
interface GroupCardProps {
|
||||
group: Group
|
||||
@@ -26,6 +27,7 @@ const GroupCard = ({
|
||||
const [copied, setCopied] = useState(false)
|
||||
const [showCopyDropdown, setShowCopyDropdown] = useState(false)
|
||||
const [expandedServer, setExpandedServer] = useState<string | null>(null)
|
||||
const [showInstallDialog, setShowInstallDialog] = useState(false)
|
||||
const dropdownRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
// Close dropdown when clicking outside
|
||||
@@ -50,6 +52,10 @@ const GroupCard = ({
|
||||
setShowDeleteDialog(true)
|
||||
}
|
||||
|
||||
const handleInstall = () => {
|
||||
setShowInstallDialog(true)
|
||||
}
|
||||
|
||||
const handleConfirmDelete = () => {
|
||||
onDelete(group.id)
|
||||
setShowDeleteDialog(false)
|
||||
@@ -183,6 +189,13 @@ const GroupCard = ({
|
||||
<div className="bg-blue-50 text-blue-700 px-3 py-1 rounded-full text-sm btn-secondary">
|
||||
{t('groups.serverCount', { count: group.servers.length })}
|
||||
</div>
|
||||
<button
|
||||
onClick={handleInstall}
|
||||
className="text-purple-500 hover:text-purple-700"
|
||||
title={t('install.installButton')}
|
||||
>
|
||||
<Download size={18} />
|
||||
</button>
|
||||
<button
|
||||
onClick={handleEdit}
|
||||
className="text-gray-500 hover:text-gray-700"
|
||||
@@ -277,6 +290,20 @@ const GroupCard = ({
|
||||
serverName={group.name}
|
||||
isGroup={true}
|
||||
/>
|
||||
{showInstallDialog && installConfig && (
|
||||
<InstallToClientDialog
|
||||
groupId={group.id}
|
||||
groupName={group.name}
|
||||
config={{
|
||||
type: 'streamable-http',
|
||||
url: `${installConfig.protocol}://${installConfig.baseUrl}${installConfig.basePath}/mcp/${group.id}`,
|
||||
headers: {
|
||||
Authorization: `Bearer ${installConfig.token}`
|
||||
}
|
||||
}}
|
||||
onClose={() => setShowInstallDialog(false)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
219
frontend/src/components/InstallToClientDialog.tsx
Normal file
219
frontend/src/components/InstallToClientDialog.tsx
Normal file
@@ -0,0 +1,219 @@
|
||||
import React, { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Copy, Check } from 'lucide-react';
|
||||
|
||||
interface InstallToClientDialogProps {
|
||||
serverName?: string;
|
||||
groupId?: string;
|
||||
groupName?: string;
|
||||
config: any;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
const InstallToClientDialog: React.FC<InstallToClientDialogProps> = ({
|
||||
serverName,
|
||||
groupId,
|
||||
groupName,
|
||||
config,
|
||||
onClose,
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const [activeTab, setActiveTab] = useState<'cursor' | 'claude-code' | 'claude-desktop'>('cursor');
|
||||
const [copied, setCopied] = useState(false);
|
||||
|
||||
// Generate configuration based on the active tab
|
||||
const generateConfig = () => {
|
||||
if (groupId) {
|
||||
// For groups, generate group-based configuration
|
||||
return {
|
||||
mcpServers: {
|
||||
[`mcphub-${groupId}`]: config,
|
||||
},
|
||||
};
|
||||
} else {
|
||||
// For individual servers
|
||||
return {
|
||||
mcpServers: {
|
||||
[serverName || 'mcp-server']: config,
|
||||
},
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const configJson = JSON.stringify(generateConfig(), null, 2);
|
||||
|
||||
const handleCopyConfig = () => {
|
||||
navigator.clipboard.writeText(configJson).then(() => {
|
||||
setCopied(true);
|
||||
setTimeout(() => setCopied(false), 2000);
|
||||
});
|
||||
};
|
||||
|
||||
// Generate deep link for Cursor (if supported in the future)
|
||||
const handleInstallToCursor = () => {
|
||||
// For now, just copy the config since deep linking may not be widely supported
|
||||
handleCopyConfig();
|
||||
// In the future, this could be:
|
||||
// const deepLink = `cursor://install-mcp?config=${encodeURIComponent(configJson)}`;
|
||||
// window.open(deepLink, '_blank');
|
||||
};
|
||||
|
||||
const getStepsList = () => {
|
||||
const displayName = groupName || serverName || 'MCP server';
|
||||
|
||||
switch (activeTab) {
|
||||
case 'cursor':
|
||||
return [
|
||||
t('install.step1Cursor'),
|
||||
t('install.step2Cursor'),
|
||||
t('install.step3Cursor'),
|
||||
t('install.step4Cursor', { name: displayName }),
|
||||
];
|
||||
case 'claude-code':
|
||||
return [
|
||||
t('install.step1ClaudeCode'),
|
||||
t('install.step2ClaudeCode'),
|
||||
t('install.step3ClaudeCode'),
|
||||
t('install.step4ClaudeCode', { name: displayName }),
|
||||
];
|
||||
case 'claude-desktop':
|
||||
return [
|
||||
t('install.step1ClaudeDesktop'),
|
||||
t('install.step2ClaudeDesktop'),
|
||||
t('install.step3ClaudeDesktop'),
|
||||
t('install.step4ClaudeDesktop', { name: displayName }),
|
||||
];
|
||||
default:
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
|
||||
<div className="bg-white rounded-lg shadow-xl max-w-4xl w-full max-h-[90vh] overflow-hidden">
|
||||
<div className="flex justify-between items-center p-6 border-b">
|
||||
<h2 className="text-2xl font-bold text-gray-900">
|
||||
{groupId
|
||||
? t('install.installGroupTitle', { name: groupName })
|
||||
: t('install.installServerTitle', { name: serverName })}
|
||||
</h2>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="text-gray-500 hover:text-gray-700 transition-colors duration-200"
|
||||
aria-label={t('common.close')}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className="h-6 w-6"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M6 18L18 6M6 6l12 12"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="overflow-y-auto max-h-[calc(90vh-140px)]">
|
||||
{/* Tab Navigation */}
|
||||
<div className="border-b border-gray-200 px-6 pt-4">
|
||||
<nav className="-mb-px flex space-x-4">
|
||||
<button
|
||||
onClick={() => setActiveTab('cursor')}
|
||||
className={`py-2 px-1 border-b-2 font-medium text-sm transition-colors duration-200 ${
|
||||
activeTab === 'cursor'
|
||||
? 'border-blue-500 text-blue-600'
|
||||
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'
|
||||
}`}
|
||||
>
|
||||
Cursor
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setActiveTab('claude-code')}
|
||||
className={`py-2 px-1 border-b-2 font-medium text-sm transition-colors duration-200 ${
|
||||
activeTab === 'claude-code'
|
||||
? 'border-blue-500 text-blue-600'
|
||||
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'
|
||||
}`}
|
||||
>
|
||||
Claude Code
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setActiveTab('claude-desktop')}
|
||||
className={`py-2 px-1 border-b-2 font-medium text-sm transition-colors duration-200 ${
|
||||
activeTab === 'claude-desktop'
|
||||
? 'border-blue-500 text-blue-600'
|
||||
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'
|
||||
}`}
|
||||
>
|
||||
Claude Desktop
|
||||
</button>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
{/* Configuration Display */}
|
||||
<div className="p-6 space-y-6">
|
||||
<div className="bg-gray-50 rounded-lg p-4">
|
||||
<div className="flex justify-between items-center mb-2">
|
||||
<h3 className="text-sm font-medium text-gray-700">{t('install.configCode')}</h3>
|
||||
<button
|
||||
onClick={handleCopyConfig}
|
||||
className="flex items-center space-x-2 px-3 py-1.5 bg-blue-100 text-blue-800 rounded hover:bg-blue-200 transition-colors duration-200 text-sm"
|
||||
>
|
||||
{copied ? <Check size={16} /> : <Copy size={16} />}
|
||||
<span>{copied ? t('common.copied') : t('install.copyConfig')}</span>
|
||||
</button>
|
||||
</div>
|
||||
<pre className="bg-white border border-gray-200 rounded p-4 text-xs overflow-x-auto">
|
||||
<code>{configJson}</code>
|
||||
</pre>
|
||||
</div>
|
||||
|
||||
{/* Installation Steps */}
|
||||
<div className="bg-blue-50 rounded-lg p-4">
|
||||
<h3 className="text-sm font-semibold text-blue-900 mb-3">{t('install.steps')}</h3>
|
||||
<ol className="space-y-3">
|
||||
{getStepsList().map((step, index) => (
|
||||
<li key={index} className="flex items-start space-x-3">
|
||||
<span className="flex-shrink-0 w-6 h-6 bg-blue-500 text-white rounded-full flex items-center justify-center text-xs font-medium">
|
||||
{index + 1}
|
||||
</span>
|
||||
<span className="text-sm text-blue-900 pt-0.5">{step}</span>
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
<div className="flex justify-between items-center p-6 border-t bg-gray-50">
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="px-4 py-2 border border-gray-300 text-gray-700 rounded hover:bg-gray-100 transition-colors duration-200"
|
||||
>
|
||||
{t('common.close')}
|
||||
</button>
|
||||
<button
|
||||
onClick={handleInstallToCursor}
|
||||
className="px-6 py-2 bg-blue-600 text-white rounded hover:bg-blue-700 transition-colors duration-200 flex items-center space-x-2"
|
||||
>
|
||||
<Copy size={16} />
|
||||
<span>
|
||||
{activeTab === 'cursor' && t('install.installToCursor', { name: groupName || serverName })}
|
||||
{activeTab === 'claude-code' && t('install.installToClaudeCode', { name: groupName || serverName })}
|
||||
{activeTab === 'claude-desktop' && t('install.installToClaudeDesktop', { name: groupName || serverName })}
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default InstallToClientDialog;
|
||||
@@ -1,13 +1,14 @@
|
||||
import { useState, useRef, useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Server } from '@/types';
|
||||
import { ChevronDown, ChevronRight, AlertCircle, Copy, Check } from 'lucide-react';
|
||||
import { ChevronDown, ChevronRight, AlertCircle, Copy, Check, Download } from 'lucide-react';
|
||||
import { StatusBadge } from '@/components/ui/Badge';
|
||||
import ToolCard from '@/components/ui/ToolCard';
|
||||
import PromptCard from '@/components/ui/PromptCard';
|
||||
import DeleteDialog from '@/components/ui/DeleteDialog';
|
||||
import { useToast } from '@/contexts/ToastContext';
|
||||
import { useSettingsData } from '@/hooks/useSettingsData';
|
||||
import InstallToClientDialog from '@/components/InstallToClientDialog';
|
||||
|
||||
interface ServerCardProps {
|
||||
server: Server;
|
||||
@@ -25,6 +26,7 @@ const ServerCard = ({ server, onRemove, onEdit, onToggle, onRefresh }: ServerCar
|
||||
const [isToggling, setIsToggling] = useState(false);
|
||||
const [showErrorPopover, setShowErrorPopover] = useState(false);
|
||||
const [copied, setCopied] = useState(false);
|
||||
const [showInstallDialog, setShowInstallDialog] = useState(false);
|
||||
const errorPopoverRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -52,6 +54,11 @@ const ServerCard = ({ server, onRemove, onEdit, onToggle, onRefresh }: ServerCar
|
||||
onEdit(server);
|
||||
};
|
||||
|
||||
const handleInstall = (e: React.MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
setShowInstallDialog(true);
|
||||
};
|
||||
|
||||
const handleToggle = async (e: React.MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
if (isToggling || !onToggle) return;
|
||||
@@ -310,6 +317,13 @@ const ServerCard = ({ server, onRemove, onEdit, onToggle, onRefresh }: ServerCar
|
||||
<button onClick={handleCopyServerConfig} className={`px-3 py-1 btn-secondary`}>
|
||||
{t('server.copy')}
|
||||
</button>
|
||||
<button
|
||||
onClick={handleInstall}
|
||||
className="px-3 py-1 bg-purple-100 text-purple-800 rounded hover:bg-purple-200 text-sm btn-primary flex items-center space-x-1"
|
||||
>
|
||||
<Download size={14} />
|
||||
<span>{t('install.installButton')}</span>
|
||||
</button>
|
||||
<button
|
||||
onClick={handleEdit}
|
||||
className="px-3 py-1 bg-blue-100 text-blue-800 rounded hover:bg-blue-200 text-sm btn-primary"
|
||||
@@ -398,6 +412,13 @@ const ServerCard = ({ server, onRemove, onEdit, onToggle, onRefresh }: ServerCar
|
||||
onConfirm={handleConfirmDelete}
|
||||
serverName={server.name}
|
||||
/>
|
||||
{showInstallDialog && server.config && (
|
||||
<InstallToClientDialog
|
||||
serverName={server.name}
|
||||
config={server.config}
|
||||
onClose={() => setShowInstallDialog(false)}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -17,7 +17,8 @@ import {
|
||||
Link,
|
||||
FileCode,
|
||||
ChevronDown as DropdownIcon,
|
||||
Wrench
|
||||
Wrench,
|
||||
Download
|
||||
} from 'lucide-react'
|
||||
|
||||
export {
|
||||
@@ -39,7 +40,8 @@ export {
|
||||
Link,
|
||||
FileCode,
|
||||
DropdownIcon,
|
||||
Wrench
|
||||
Wrench,
|
||||
Download
|
||||
}
|
||||
|
||||
const LucideIcons = {
|
||||
|
||||
@@ -751,5 +751,28 @@
|
||||
"internalError": "Internal Error",
|
||||
"internalErrorMessage": "An unexpected error occurred while processing the OAuth callback.",
|
||||
"closeWindow": "Close Window"
|
||||
},
|
||||
"install": {
|
||||
"installServerTitle": "Install Server to {{name}}",
|
||||
"installGroupTitle": "Install Group {{name}}",
|
||||
"configCode": "Configuration Code",
|
||||
"copyConfig": "Copy Configuration",
|
||||
"steps": "Installation Steps",
|
||||
"step1Cursor": "Copy the configuration code above",
|
||||
"step2Cursor": "Open Cursor, go to Settings > Features > MCP",
|
||||
"step3Cursor": "Click 'Add New MCP Server' to add a new server",
|
||||
"step4Cursor": "Paste the configuration in the appropriate location and restart Cursor",
|
||||
"step1ClaudeCode": "Copy the configuration code above",
|
||||
"step2ClaudeCode": "Open Claude Code, go to Settings > Features > MCP",
|
||||
"step3ClaudeCode": "Click 'Add New MCP Server' to add a new server",
|
||||
"step4ClaudeCode": "Paste the configuration in the appropriate location and restart Claude Code",
|
||||
"step1ClaudeDesktop": "Copy the configuration code above",
|
||||
"step2ClaudeDesktop": "Open Claude Desktop, go to Settings > Developer",
|
||||
"step3ClaudeDesktop": "Click 'Edit Config' to edit the configuration file",
|
||||
"step4ClaudeDesktop": "Paste the configuration in the mcpServers section and restart Claude Desktop",
|
||||
"installToCursor": "Add {{name}} MCP server to Cursor",
|
||||
"installToClaudeCode": "Add {{name}} MCP server to Claude Code",
|
||||
"installToClaudeDesktop": "Add {{name}} MCP server to Claude Desktop",
|
||||
"installButton": "Install"
|
||||
}
|
||||
}
|
||||
@@ -751,5 +751,28 @@
|
||||
"internalError": "Erreur interne",
|
||||
"internalErrorMessage": "Une erreur inattendue s'est produite lors du traitement du callback OAuth.",
|
||||
"closeWindow": "Fermer la fenêtre"
|
||||
},
|
||||
"install": {
|
||||
"installServerTitle": "Installer le serveur sur {{name}}",
|
||||
"installGroupTitle": "Installer le groupe {{name}}",
|
||||
"configCode": "Code de configuration",
|
||||
"copyConfig": "Copier la configuration",
|
||||
"steps": "Étapes d'installation",
|
||||
"step1Cursor": "Copiez le code de configuration ci-dessus",
|
||||
"step2Cursor": "Ouvrez Cursor, allez dans Paramètres > Features > MCP",
|
||||
"step3Cursor": "Cliquez sur 'Add New MCP Server' pour ajouter un nouveau serveur",
|
||||
"step4Cursor": "Collez la configuration à l'emplacement approprié et redémarrez Cursor",
|
||||
"step1ClaudeCode": "Copiez le code de configuration ci-dessus",
|
||||
"step2ClaudeCode": "Ouvrez Claude Code, allez dans Paramètres > Features > MCP",
|
||||
"step3ClaudeCode": "Cliquez sur 'Add New MCP Server' pour ajouter un nouveau serveur",
|
||||
"step4ClaudeCode": "Collez la configuration à l'emplacement approprié et redémarrez Claude Code",
|
||||
"step1ClaudeDesktop": "Copiez le code de configuration ci-dessus",
|
||||
"step2ClaudeDesktop": "Ouvrez Claude Desktop, allez dans Paramètres > Développeur",
|
||||
"step3ClaudeDesktop": "Cliquez sur 'Edit Config' pour modifier le fichier de configuration",
|
||||
"step4ClaudeDesktop": "Collez la configuration dans la section mcpServers et redémarrez Claude Desktop",
|
||||
"installToCursor": "Ajouter le serveur MCP {{name}} à Cursor",
|
||||
"installToClaudeCode": "Ajouter le serveur MCP {{name}} à Claude Code",
|
||||
"installToClaudeDesktop": "Ajouter le serveur MCP {{name}} à Claude Desktop",
|
||||
"installButton": "Installer"
|
||||
}
|
||||
}
|
||||
@@ -753,5 +753,28 @@
|
||||
"internalError": "内部错误",
|
||||
"internalErrorMessage": "处理 OAuth 回调时发生意外错误。",
|
||||
"closeWindow": "关闭窗口"
|
||||
},
|
||||
"install": {
|
||||
"installServerTitle": "安装服务器到 {{name}}",
|
||||
"installGroupTitle": "安装分组 {{name}}",
|
||||
"configCode": "配置代码",
|
||||
"copyConfig": "复制配置",
|
||||
"steps": "安装步骤",
|
||||
"step1Cursor": "复制上面的配置代码",
|
||||
"step2Cursor": "打开 Cursor,进入设置 > Features > MCP",
|
||||
"step3Cursor": "点击 'Add New MCP Server' 添加新服务器",
|
||||
"step4Cursor": "将配置粘贴到相应位置并重启 Cursor",
|
||||
"step1ClaudeCode": "复制上面的配置代码",
|
||||
"step2ClaudeCode": "打开 Claude Code,进入设置 > Features > MCP",
|
||||
"step3ClaudeCode": "点击 'Add New MCP Server' 添加新服务器",
|
||||
"step4ClaudeCode": "将配置粘贴到相应位置并重启 Claude Code",
|
||||
"step1ClaudeDesktop": "复制上面的配置代码",
|
||||
"step2ClaudeDesktop": "打开 Claude Desktop,进入设置 > Developer",
|
||||
"step3ClaudeDesktop": "点击 'Edit Config' 编辑配置文件",
|
||||
"step4ClaudeDesktop": "将配置粘贴到 mcpServers 部分并重启 Claude Desktop",
|
||||
"installToCursor": "添加 {{name}} MCP 服务器到 Cursor",
|
||||
"installToClaudeCode": "添加 {{name}} MCP 服务器到 Claude Code",
|
||||
"installToClaudeDesktop": "添加 {{name}} MCP 服务器到 Claude Desktop",
|
||||
"installButton": "安装"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user