page.tsx 8.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198
  1. /**
  2. * MCP 管理页面
  3. *
  4. * 核心概念:
  5. * - Web UI 是 MCP 测试工具,不需要全局登录
  6. * - 每个 MCP 有独立的认证状态
  7. * - 可以同时管理多个 MCP 的登录
  8. *
  9. * 连接统计逻辑:
  10. * - **已连接** = 服务器健康(healthy),不管有没有登录
  11. * - **已登录** = 有有效 token,可以调用需要权限的工具
  12. * - User MCP 有些工具不需要登录也能用(比如 get_novels)
  13. */
  14. 'use client';
  15. import { useState, useCallback, useEffect } from 'react';
  16. import Link from 'next/link';
  17. import { McpServerCard } from '@/components/McpServerCard';
  18. import { MCP_SERVERS, mcpTokenManager } from '@/lib/mcp-token-manager';
  19. // 每个服务器的连接状态
  20. interface ServerConnectionStatus {
  21. healthy: boolean;
  22. loggedIn: boolean;
  23. }
  24. // 每个服务器的启用状态
  25. interface ServerEnabledStatus {
  26. enabled: boolean;
  27. }
  28. export default function McpPage() {
  29. const [mounted, setMounted] = useState(false);
  30. const [connectionStatuses, setConnectionStatuses] = useState<Record<string, ServerConnectionStatus>>({});
  31. const [enabledStatuses, setEnabledStatuses] = useState<Record<string, boolean>>({});
  32. const totalCount = Object.keys(MCP_SERVERS).length;
  33. // 客户端挂载后设置 mounted 标志
  34. useEffect(() => {
  35. setMounted(true);
  36. }, []);
  37. // 计算已启用数量
  38. const enabledCount = Object.values(enabledStatuses).filter(enabled => enabled).length;
  39. // 计算已连接数量:所有健康的 MCP 服务器
  40. const connectedCount = Object.values(connectionStatuses).filter(status => status.healthy).length;
  41. // 计算已登录数量:有 token 的 MCP 服务器
  42. const loggedInCount = Object.values(connectionStatuses).filter(status => status.loggedIn).length;
  43. // 初始化启用状态
  44. useEffect(() => {
  45. const initialEnabledStates: Record<string, boolean> = {};
  46. for (const mcpType of Object.keys(MCP_SERVERS)) {
  47. initialEnabledStates[mcpType] = mcpTokenManager.isEnabled(mcpType);
  48. }
  49. setEnabledStatuses(initialEnabledStates);
  50. }, []);
  51. // 处理子组件报告的连接状态变化
  52. // 使用 useCallback 避免每次渲染创建新函数引用,防止子组件 useEffect 无限循环
  53. const handleConnectionStatusChange = useCallback((mcpType: string, status: ServerConnectionStatus) => {
  54. setConnectionStatuses(prev => {
  55. // 只在状态真正改变时才更新,避免不必要的重新渲染
  56. const current = prev[mcpType];
  57. if (current && current.healthy === status.healthy && current.loggedIn === status.loggedIn) {
  58. return prev; // 状态未改变,返回原对象
  59. }
  60. return {
  61. ...prev,
  62. [mcpType]: status
  63. };
  64. });
  65. }, []); // 空依赖数组,函数引用永远不变
  66. // 处理子组件报告的启用状态变化
  67. const handleEnabledChange = useCallback((mcpType: string, enabled: boolean) => {
  68. setEnabledStatuses(prev => {
  69. const current = prev[mcpType];
  70. if (current === enabled) {
  71. return prev; // 状态未改变,返回原对象
  72. }
  73. return {
  74. ...prev,
  75. [mcpType]: enabled
  76. };
  77. });
  78. }, []);
  79. return (
  80. <div className="min-h-screen bg-gray-50 dark:bg-gray-900">
  81. {/* Header - 移动端两行布局 */}
  82. <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">
  83. <div className="flex flex-col md:flex-row md:items-center md:justify-between gap-2 md:gap-0">
  84. {/* 第一行:标题和导航 */}
  85. <div className="flex items-center justify-between">
  86. <h1 className="text-lg md:text-xl font-bold text-gray-800 dark:text-white">
  87. AI MCP Web UI
  88. </h1>
  89. <nav className="flex space-x-2 md:space-x-4">
  90. <Link href="/" className="text-sm md:text-base text-gray-600 dark:text-gray-300 hover:text-blue-500 px-2 py-1">
  91. 聊天
  92. </Link>
  93. <Link href="/mcp" className="text-sm md:text-base text-blue-500 dark:text-blue-400 font-medium px-2 py-1">
  94. MCP 管理
  95. </Link>
  96. </nav>
  97. </div>
  98. {/* 第二行(移动端)/ 右侧(桌面端):状态和按钮 */}
  99. <div className="flex items-center justify-between md:justify-end space-x-2 md:space-x-4">
  100. {/* MCP 统计 - 使用 suppressHydrationWarning 避免 SSR/CSR 不匹配 */}
  101. <div className="flex items-center space-x-1 md:space-x-2 text-xs md:text-sm" suppressHydrationWarning>
  102. <span className="font-medium text-purple-600 dark:text-purple-400">
  103. 启用 {mounted ? enabledCount : 0}/{totalCount}
  104. </span>
  105. {mounted && loggedInCount > 0 && (
  106. <span className="text-green-600 dark:text-green-400">
  107. · 已登录 {loggedInCount}
  108. </span>
  109. )}
  110. </div>
  111. <Link
  112. href="/"
  113. 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"
  114. >
  115. 开始聊天
  116. </Link>
  117. </div>
  118. </div>
  119. </header>
  120. {/* Main Content */}
  121. <main className="max-w-6xl mx-auto px-3 md:px-4 py-4 md:py-8">
  122. {/* Page Header - 移动端优化 */}
  123. <div className="mb-4 md:mb-8">
  124. <h2 className="text-xl md:text-2xl font-bold text-gray-800 dark:text-white mb-2">
  125. MCP 服务器管理
  126. </h2>
  127. <p className="text-sm md:text-base text-gray-600 dark:text-gray-400">
  128. 管理和配置您的 MCP 服务器连接
  129. </p>
  130. {/* 统计信息 - 移动端分两行,使用 suppressHydrationWarning 避免 SSR/CSR 不匹配 */}
  131. <div className="flex flex-wrap items-center gap-x-3 gap-y-1.5 mt-2 text-xs md:text-sm" suppressHydrationWarning>
  132. <span className="font-medium text-purple-600 dark:text-purple-400">
  133. 已启用 {mounted ? enabledCount : 0}/{totalCount}
  134. </span>
  135. <span className="hidden md:inline text-gray-400">|</span>
  136. <span className="font-medium text-green-600 dark:text-green-400">
  137. 已连接 {mounted ? connectedCount : 0}/{totalCount}
  138. </span>
  139. {mounted && loggedInCount > 0 && (
  140. <>
  141. <span className="hidden md:inline text-gray-400">|</span>
  142. <span className="font-medium text-blue-600 dark:text-blue-400">
  143. 已登录 {loggedInCount}/{totalCount}
  144. </span>
  145. </>
  146. )}
  147. </div>
  148. </div>
  149. {/* MCP Server Cards - 移动端增加间距 */}
  150. <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 md:gap-6">
  151. {Object.entries(MCP_SERVERS).map(([mcpType, config]) => (
  152. <McpServerCard
  153. key={mcpType}
  154. mcpType={mcpType}
  155. config={config}
  156. onConnectionStatusChange={handleConnectionStatusChange}
  157. onEnabledChange={handleEnabledChange}
  158. />
  159. ))}
  160. </div>
  161. {/* Info Section - 移动端优化 */}
  162. <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">
  163. <h3 className="text-base md:text-lg font-semibold text-blue-900 dark:text-blue-200 mb-3">
  164. 关于 MCP 认证
  165. </h3>
  166. <ul className="space-y-2 text-xs md:text-sm text-blue-800 dark:text-blue-300">
  167. <li>• <strong>Novel Translator MCP</strong>: 无需登录,可直接使用</li>
  168. <li>• <strong>Platform User MCP</strong>: 需要读者/作者账号登录(部分工具无需登录)</li>
  169. <li>• <strong>Platform Admin MCP</strong>: 需要管理员账号登录</li>
  170. <li>• <strong>Template 241 MCP App</strong>: 无需登录,36 个 shadcn/ui 组件架构</li>
  171. </ul>
  172. <p className="mt-3 text-xs md:text-sm text-blue-700 dark:text-blue-400">
  173. <strong>已连接</strong> 表示服务器在线,<strong>已登录</strong> 表示可以调用需要权限的工具。
  174. 登录后,对应的 MCP Token 会自动在聊天请求中携带,无需额外配置。
  175. </p>
  176. </div>
  177. </main>
  178. </div>
  179. );
  180. }