feat(web): refactor MCP servers as sidebar entities and improve sidebar footer

- Refactor MCP servers to be managed as collapsible sidebar sub-items with
  ?id= detail routing and inline form (matching bots/pipelines pattern)
- Add MCPDetailContent with create/edit modes, enable toggle, and danger zone
- Extract MCPForm as standalone inline form from MCPFormDialog
- Move API Integration to standalone sidebar footer button
- Add GitHub star CTA with live star count badge in user dropdown menu
- Add MCP server status dot indicators in sidebar (green/gray for enabled/disabled)
- Add i18n keys for MCP detail page and GitHub star CTA in all 4 locales
This commit is contained in:
Junyan Qin
2026-03-27 19:59:34 +08:00
parent 4902c1d3b2
commit 23fa47b07e
11 changed files with 1359 additions and 100 deletions
+8 -90
View File
@@ -1,103 +1,21 @@
'use client';
import MCPServerComponent from '@/app/home/plugins/mcp-server/MCPServerComponent';
import MCPFormDialog from '@/app/home/plugins/mcp-server/mcp-form/MCPFormDialog';
import MCPDeleteConfirmDialog from '@/app/home/plugins/mcp-server/mcp-form/MCPDeleteConfirmDialog';
import { Button } from '@/components/ui/button';
import { PlusIcon } from 'lucide-react';
import React, { useState } from 'react';
import { httpClient } from '@/app/infra/http/HttpClient';
import { systemInfo } from '@/app/infra/http/HttpClient';
import { toast } from 'sonner';
import { useSearchParams } from 'next/navigation';
import { useTranslation } from 'react-i18next';
import MCPDetailContent from './MCPDetailContent';
export default function MCPPage() {
const { t } = useTranslation();
const [mcpFormOpen, setMcpFormOpen] = useState(false);
const [editingServerName, setEditingServerName] = useState<string | null>(
null,
);
const [isEditMode, setIsEditMode] = useState(false);
const [refreshKey, setRefreshKey] = useState(0);
const [showDeleteConfirmModal, setShowDeleteConfirmModal] = useState(false);
const searchParams = useSearchParams();
const detailId = searchParams.get('id');
async function checkExtensionsLimit(): Promise<boolean> {
const maxExtensions = systemInfo.limitation?.max_extensions ?? -1;
if (maxExtensions < 0) return true;
try {
const [pluginsResp, mcpResp] = await Promise.all([
httpClient.getPlugins(),
httpClient.getMCPServers(),
]);
const total =
(pluginsResp.plugins?.length ?? 0) + (mcpResp.servers?.length ?? 0);
if (total >= maxExtensions) {
toast.error(
t('limitation.maxExtensionsReached', { max: maxExtensions }),
);
return false;
}
} catch {
// If we can't check, let backend handle it
}
return true;
if (detailId) {
return <MCPDetailContent id={detailId} />;
}
return (
<div className="h-full flex flex-col">
<div className="flex flex-row justify-end items-center px-[0.8rem] pb-4 flex-shrink-0">
<Button
variant="default"
className="px-6 py-4 cursor-pointer"
onClick={async () => {
if (!(await checkExtensionsLimit())) return;
setIsEditMode(false);
setEditingServerName(null);
setMcpFormOpen(true);
}}
>
<PlusIcon className="w-4 h-4" />
{t('mcp.add')}
</Button>
</div>
<div className="flex-1 overflow-y-auto">
<MCPServerComponent
key={refreshKey}
onEditServer={(serverName) => {
setEditingServerName(serverName);
setIsEditMode(true);
setMcpFormOpen(true);
}}
/>
</div>
<MCPFormDialog
open={mcpFormOpen}
onOpenChange={setMcpFormOpen}
serverName={editingServerName}
isEditMode={isEditMode}
onSuccess={() => {
setEditingServerName(null);
setIsEditMode(false);
setRefreshKey((prev) => prev + 1);
}}
onDelete={() => {
setShowDeleteConfirmModal(true);
}}
/>
<MCPDeleteConfirmDialog
open={showDeleteConfirmModal}
onOpenChange={setShowDeleteConfirmModal}
serverName={editingServerName}
onSuccess={() => {
setMcpFormOpen(false);
setEditingServerName(null);
setIsEditMode(false);
setRefreshKey((prev) => prev + 1);
}}
/>
<div className="flex h-full items-center justify-center text-muted-foreground">
<p>{t('mcp.selectFromSidebar')}</p>
</div>
);
}