| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198 |
- /**
- * MCP 管理页面
- *
- * 核心概念:
- * - Web UI 是 MCP 测试工具,不需要全局登录
- * - 每个 MCP 有独立的认证状态
- * - 可以同时管理多个 MCP 的登录
- *
- * 连接统计逻辑:
- * - **已连接** = 服务器健康(healthy),不管有没有登录
- * - **已登录** = 有有效 token,可以调用需要权限的工具
- * - User MCP 有些工具不需要登录也能用(比如 get_novels)
- */
- 'use client';
- import { useState, useCallback, useEffect } from 'react';
- import Link from 'next/link';
- import { McpServerCard } from '@/components/McpServerCard';
- import { MCP_SERVERS, mcpTokenManager } from '@/lib/mcp-token-manager';
- // 每个服务器的连接状态
- interface ServerConnectionStatus {
- healthy: boolean;
- loggedIn: boolean;
- }
- // 每个服务器的启用状态
- interface ServerEnabledStatus {
- enabled: boolean;
- }
- export default function McpPage() {
- const [mounted, setMounted] = useState(false);
- const [connectionStatuses, setConnectionStatuses] = useState<Record<string, ServerConnectionStatus>>({});
- const [enabledStatuses, setEnabledStatuses] = useState<Record<string, boolean>>({});
- const totalCount = Object.keys(MCP_SERVERS).length;
- // 客户端挂载后设置 mounted 标志
- useEffect(() => {
- setMounted(true);
- }, []);
- // 计算已启用数量
- const enabledCount = Object.values(enabledStatuses).filter(enabled => enabled).length;
- // 计算已连接数量:所有健康的 MCP 服务器
- const connectedCount = Object.values(connectionStatuses).filter(status => status.healthy).length;
- // 计算已登录数量:有 token 的 MCP 服务器
- const loggedInCount = Object.values(connectionStatuses).filter(status => status.loggedIn).length;
- // 初始化启用状态
- useEffect(() => {
- const initialEnabledStates: Record<string, boolean> = {};
- for (const mcpType of Object.keys(MCP_SERVERS)) {
- initialEnabledStates[mcpType] = mcpTokenManager.isEnabled(mcpType);
- }
- setEnabledStatuses(initialEnabledStates);
- }, []);
- // 处理子组件报告的连接状态变化
- // 使用 useCallback 避免每次渲染创建新函数引用,防止子组件 useEffect 无限循环
- const handleConnectionStatusChange = useCallback((mcpType: string, status: ServerConnectionStatus) => {
- setConnectionStatuses(prev => {
- // 只在状态真正改变时才更新,避免不必要的重新渲染
- const current = prev[mcpType];
- if (current && current.healthy === status.healthy && current.loggedIn === status.loggedIn) {
- return prev; // 状态未改变,返回原对象
- }
- return {
- ...prev,
- [mcpType]: status
- };
- });
- }, []); // 空依赖数组,函数引用永远不变
- // 处理子组件报告的启用状态变化
- const handleEnabledChange = useCallback((mcpType: string, enabled: boolean) => {
- setEnabledStatuses(prev => {
- const current = prev[mcpType];
- if (current === enabled) {
- return prev; // 状态未改变,返回原对象
- }
- return {
- ...prev,
- [mcpType]: enabled
- };
- });
- }, []);
- return (
- <div className="min-h-screen bg-gray-50 dark:bg-gray-900">
- {/* Header - 移动端两行布局 */}
- <header className="bg-white dark:bg-gray-800 border-b dark:border-gray-700 px-3 py-2 md:px-6 md:py-4 safe-area-inset-top">
- <div className="flex flex-col md:flex-row md:items-center md:justify-between gap-2 md:gap-0">
- {/* 第一行:标题和导航 */}
- <div className="flex items-center justify-between">
- <h1 className="text-lg md:text-xl font-bold text-gray-800 dark:text-white">
- AI MCP Web UI
- </h1>
- <nav className="flex space-x-2 md:space-x-4">
- <Link href="/" className="text-sm md:text-base text-gray-600 dark:text-gray-300 hover:text-blue-500 px-2 py-1">
- 聊天
- </Link>
- <Link href="/mcp" className="text-sm md:text-base text-blue-500 dark:text-blue-400 font-medium px-2 py-1">
- MCP 管理
- </Link>
- </nav>
- </div>
- {/* 第二行(移动端)/ 右侧(桌面端):状态和按钮 */}
- <div className="flex items-center justify-between md:justify-end space-x-2 md:space-x-4">
- {/* MCP 统计 - 使用 suppressHydrationWarning 避免 SSR/CSR 不匹配 */}
- <div className="flex items-center space-x-1 md:space-x-2 text-xs md:text-sm" suppressHydrationWarning>
- <span className="font-medium text-purple-600 dark:text-purple-400">
- 启用 {mounted ? enabledCount : 0}/{totalCount}
- </span>
- {mounted && loggedInCount > 0 && (
- <span className="text-green-600 dark:text-green-400">
- · 已登录 {loggedInCount}
- </span>
- )}
- </div>
- <Link
- href="/"
- className="text-xs md:text-sm px-3 md:px-4 py-1.5 md:py-2 bg-blue-500 text-white rounded-lg hover:bg-blue-600 transition-colors"
- >
- 开始聊天
- </Link>
- </div>
- </div>
- </header>
- {/* Main Content */}
- <main className="max-w-6xl mx-auto px-3 md:px-4 py-4 md:py-8">
- {/* Page Header - 移动端优化 */}
- <div className="mb-4 md:mb-8">
- <h2 className="text-xl md:text-2xl font-bold text-gray-800 dark:text-white mb-2">
- MCP 服务器管理
- </h2>
- <p className="text-sm md:text-base text-gray-600 dark:text-gray-400">
- 管理和配置您的 MCP 服务器连接
- </p>
- {/* 统计信息 - 移动端分两行,使用 suppressHydrationWarning 避免 SSR/CSR 不匹配 */}
- <div className="flex flex-wrap items-center gap-x-3 gap-y-1.5 mt-2 text-xs md:text-sm" suppressHydrationWarning>
- <span className="font-medium text-purple-600 dark:text-purple-400">
- 已启用 {mounted ? enabledCount : 0}/{totalCount}
- </span>
- <span className="hidden md:inline text-gray-400">|</span>
- <span className="font-medium text-green-600 dark:text-green-400">
- 已连接 {mounted ? connectedCount : 0}/{totalCount}
- </span>
- {mounted && loggedInCount > 0 && (
- <>
- <span className="hidden md:inline text-gray-400">|</span>
- <span className="font-medium text-blue-600 dark:text-blue-400">
- 已登录 {loggedInCount}/{totalCount}
- </span>
- </>
- )}
- </div>
- </div>
- {/* MCP Server Cards - 移动端增加间距 */}
- <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 md:gap-6">
- {Object.entries(MCP_SERVERS).map(([mcpType, config]) => (
- <McpServerCard
- key={mcpType}
- mcpType={mcpType}
- config={config}
- onConnectionStatusChange={handleConnectionStatusChange}
- onEnabledChange={handleEnabledChange}
- />
- ))}
- </div>
- {/* Info Section - 移动端优化 */}
- <div className="mt-6 md:mt-8 p-4 md:p-6 bg-blue-50 dark:bg-blue-900/20 rounded-lg border dark:border-blue-800">
- <h3 className="text-base md:text-lg font-semibold text-blue-900 dark:text-blue-200 mb-3">
- 关于 MCP 认证
- </h3>
- <ul className="space-y-2 text-xs md:text-sm text-blue-800 dark:text-blue-300">
- <li>• <strong>Novel Translator MCP</strong>: 无需登录,可直接使用</li>
- <li>• <strong>Platform User MCP</strong>: 需要读者/作者账号登录(部分工具无需登录)</li>
- <li>• <strong>Platform Admin MCP</strong>: 需要管理员账号登录</li>
- <li>• <strong>Template 241 MCP App</strong>: 无需登录,36 个 shadcn/ui 组件架构</li>
- </ul>
- <p className="mt-3 text-xs md:text-sm text-blue-700 dark:text-blue-400">
- <strong>已连接</strong> 表示服务器在线,<strong>已登录</strong> 表示可以调用需要权限的工具。
- 登录后,对应的 MCP Token 会自动在聊天请求中携带,无需额外配置。
- </p>
- </div>
- </main>
- </div>
- );
- }
|