2
0

mcp-token-manager.ts 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403
  1. /**
  2. * MCP 服务器配置
  3. */
  4. export interface McpServerConfig {
  5. id: string;
  6. name: string;
  7. url: string;
  8. authType: 'none' | 'jwt';
  9. loginApi?: string;
  10. baseUrl?: string;
  11. description?: string;
  12. }
  13. /**
  14. * MCP 服务器配置列表
  15. */
  16. export const MCP_SERVERS: Record<string, McpServerConfig> = {
  17. 'novel-translator': {
  18. id: 'novel-translator',
  19. name: 'Novel Translator MCP',
  20. url: 'https://d8d-ai-vscode-8080-223-236-template-6-group.dev.d8d.fun/mcp',
  21. authType: 'none',
  22. },
  23. 'novel-platform-user': {
  24. id: 'novel-platform-user',
  25. name: 'Novel Platform User MCP',
  26. url: 'https://d8d-ai-vscode-8080-223-238-template-6-group.dev.d8d.fun/mcp/',
  27. authType: 'jwt',
  28. loginApi: '/api/v1/auth/login',
  29. baseUrl: 'https://d8d-ai-vscode-8080-223-238-template-6-group.dev.d8d.fun',
  30. },
  31. 'novel-platform-admin': {
  32. id: 'novel-platform-admin',
  33. name: 'Novel Platform Admin MCP',
  34. url: 'https://d8d-ai-vscode-8080-223-238-template-6-group.dev.d8d.fun/admin-mcp/',
  35. authType: 'jwt',
  36. loginApi: '/api/v1/auth/admin-login',
  37. baseUrl: 'https://d8d-ai-vscode-8080-223-238-template-6-group.dev.d8d.fun',
  38. },
  39. 'template-241-mcp-app': {
  40. id: 'template-241-mcp-app',
  41. name: 'Template 241 MCP App',
  42. url: 'https://d8d-ai-vscode-8080-223-241-template-6-group.dev.d8d.fun/mcp',
  43. authType: 'none',
  44. description: 'MCP App 架构 - 36 个 shadcn/ui 组件',
  45. },
  46. };
  47. /**
  48. * MCP 登录响应
  49. */
  50. export interface McpLoginResponse {
  51. success: boolean;
  52. token?: string;
  53. username?: string;
  54. error?: string;
  55. detail?: string;
  56. }
  57. /**
  58. * MCP 健康状态
  59. */
  60. export interface McpHealthStatus {
  61. healthy: boolean;
  62. latency?: number; // 响应延迟(毫秒)
  63. error?: string;
  64. }
  65. /**
  66. * MCP Token 管理器
  67. *
  68. * 核心概念:
  69. * - Web UI 是 MCP 测试工具,不需要全局登录
  70. * - 每个 MCP 有独立的认证状态
  71. * - 可以同时管理多个 MCP 的登录
  72. *
  73. * 状态区分:
  74. * - isHealthy: MCP 服务器是否正常运行(health check)
  75. * - isLoggedIn: 是否有有效的认证 token
  76. */
  77. export class McpTokenManager {
  78. private static instance: McpTokenManager;
  79. private healthCache: Map<string, { healthy: boolean; timestamp: number }> = new Map();
  80. private readonly HEALTH_CACHE_TTL = 10000; // 健康检查缓存 10 秒
  81. private constructor() {}
  82. static getInstance(): McpTokenManager {
  83. if (!McpTokenManager.instance) {
  84. McpTokenManager.instance = new McpTokenManager();
  85. }
  86. return McpTokenManager.instance;
  87. }
  88. /**
  89. * 保存 MCP Token
  90. */
  91. saveToken(mcpType: string, token: string, username: string): void {
  92. if (typeof window === 'undefined') return;
  93. const key = `mcp_token_${mcpType}`;
  94. localStorage.setItem(key, token);
  95. localStorage.setItem(`mcp_token_${mcpType}_time`, Date.now().toString());
  96. localStorage.setItem(`mcp_username_${mcpType}`, username);
  97. // 调试:验证保存
  98. console.log(`[McpTokenManager] Saved token for ${mcpType}:`, {
  99. key,
  100. tokenLength: token?.length,
  101. username,
  102. verified: localStorage.getItem(key) === token
  103. });
  104. }
  105. /**
  106. * 获取 MCP Token
  107. */
  108. getToken(mcpType: string): string | null {
  109. if (typeof window === 'undefined') return null;
  110. return localStorage.getItem(`mcp_token_${mcpType}`);
  111. }
  112. /**
  113. * 获取 MCP 用户名
  114. */
  115. getUsername(mcpType: string): string | null {
  116. if (typeof window === 'undefined') return null;
  117. return localStorage.getItem(`mcp_username_${mcpType}`);
  118. }
  119. /**
  120. * 检查 MCP 是否已登录
  121. */
  122. isLoggedIn(mcpType: string): boolean {
  123. return !!this.getToken(mcpType);
  124. }
  125. /**
  126. * 获取所有已登录的 MCP Token
  127. */
  128. getAllTokens(): Record<string, string> {
  129. const tokens: Record<string, string> = {};
  130. for (const mcpType of Object.keys(MCP_SERVERS)) {
  131. const token = this.getToken(mcpType);
  132. if (token) {
  133. tokens[mcpType] = token;
  134. }
  135. }
  136. return tokens;
  137. }
  138. /**
  139. * 获取所有已登录的 MCP 列表
  140. */
  141. getLoggedInMcpList(): string[] {
  142. return Object.keys(MCP_SERVERS).filter(mcpType => this.isLoggedIn(mcpType));
  143. }
  144. /**
  145. * 清除 MCP Token
  146. */
  147. clearToken(mcpType: string): void {
  148. if (typeof window === 'undefined') return;
  149. localStorage.removeItem(`mcp_token_${mcpType}`);
  150. localStorage.removeItem(`mcp_token_${mcpType}_time`);
  151. localStorage.removeItem(`mcp_username_${mcpType}`);
  152. }
  153. /**
  154. * 清除所有 MCP Token
  155. */
  156. clearAllTokens(): void {
  157. for (const mcpType of Object.keys(MCP_SERVERS)) {
  158. this.clearToken(mcpType);
  159. }
  160. }
  161. /**
  162. * 获取 Token 存储时长(毫秒)
  163. */
  164. getTokenAge(mcpType: string): number {
  165. if (typeof window === 'undefined') return 0;
  166. const time = localStorage.getItem(`mcp_token_${mcpType}_time`);
  167. return time ? Date.now() - parseInt(time) : 0;
  168. }
  169. /**
  170. * 获取 Token 剩余有效时间(假设 24 小时有效)
  171. */
  172. getTokenRemainingTime(mcpType: string): number {
  173. const TOKEN_VALIDITY = 24 * 60 * 60 * 1000; // 24 小时
  174. const age = this.getTokenAge(mcpType);
  175. return Math.max(0, TOKEN_VALIDITY - age);
  176. }
  177. /**
  178. * 登录 MCP 服务器
  179. */
  180. async loginMcp(mcpType: string, email: string, password: string): Promise<McpLoginResponse> {
  181. const config = MCP_SERVERS[mcpType];
  182. if (!config || config.authType !== 'jwt') {
  183. return { success: false, error: '该 MCP 不需要登录' };
  184. }
  185. // 使用本地后端代理端点
  186. const isAdmin = mcpType === 'novel-platform-admin';
  187. const proxyUrl = isAdmin ? '/api/auth/admin-login' : '/api/auth/login';
  188. try {
  189. const response = await fetch(proxyUrl, {
  190. method: 'POST',
  191. headers: { 'Content-Type': 'application/json' },
  192. body: JSON.stringify({ email, password }),
  193. });
  194. const data = await response.json();
  195. console.log(`[McpTokenManager.loginMcp] Response:`, {
  196. ok: response.ok,
  197. status: response.status,
  198. success: data.success,
  199. hasToken: !!data.token,
  200. username: data.username
  201. });
  202. if (response.ok && data.success && data.token) {
  203. this.saveToken(mcpType, data.token, data.username || email);
  204. return { success: true, token: data.token, username: data.username };
  205. } else {
  206. return {
  207. success: false,
  208. error: data.detail || data.error || '登录失败',
  209. };
  210. }
  211. } catch (e) {
  212. console.error(`[McpTokenManager.loginMcp] Error:`, e);
  213. return {
  214. success: false,
  215. error: e instanceof Error ? e.message : '网络错误',
  216. };
  217. }
  218. }
  219. /**
  220. * 登出 MCP 服务器
  221. */
  222. logoutMcp(mcpType: string): void {
  223. this.clearToken(mcpType);
  224. this.healthCache.delete(mcpType);
  225. }
  226. /**
  227. * 检查 MCP 服务器健康状态
  228. * 通过后端代理端点 /api/mcp/health/:mcpType 进行健康检查
  229. */
  230. async checkHealth(mcpType: string): Promise<McpHealthStatus> {
  231. // 检查缓存
  232. const cached = this.healthCache.get(mcpType);
  233. if (cached && Date.now() - cached.timestamp < this.HEALTH_CACHE_TTL) {
  234. return { healthy: cached.healthy };
  235. }
  236. const config = MCP_SERVERS[mcpType];
  237. if (!config) {
  238. return { healthy: false, error: '未知的 MCP 类型' };
  239. }
  240. try {
  241. const startTime = Date.now();
  242. // 使用后端代理端点进行健康检查
  243. const response = await fetch(`/api/mcp/health/${mcpType}`, {
  244. method: 'GET',
  245. headers: { 'Content-Type': 'application/json' },
  246. });
  247. const latency = Date.now() - startTime;
  248. if (response.ok) {
  249. const data = await response.json();
  250. const healthy = data.status === 'healthy' || data.healthy === true;
  251. // 更新缓存
  252. this.healthCache.set(mcpType, {
  253. healthy,
  254. timestamp: Date.now(),
  255. });
  256. return { healthy, latency };
  257. } else {
  258. const error = await response.text().catch(() => '未知错误');
  259. this.healthCache.set(mcpType, { healthy: false, timestamp: Date.now() });
  260. return { healthy: false, error: `HTTP ${response.status}: ${error}` };
  261. }
  262. } catch (e) {
  263. const errorMsg = e instanceof Error ? e.message : '网络错误';
  264. this.healthCache.set(mcpType, { healthy: false, timestamp: Date.now() });
  265. return { healthy: false, error: errorMsg };
  266. }
  267. }
  268. /**
  269. * 检查 MCP 服务器是否健康(使用缓存,不发起网络请求)
  270. */
  271. isHealthy(mcpType: string): boolean {
  272. const cached = this.healthCache.get(mcpType);
  273. if (!cached) return false; // 尚未检查过,默认不健康
  274. // 如果缓存过期,返回 false 以触发新的检查
  275. if (Date.now() - cached.timestamp > this.HEALTH_CACHE_TTL) return false;
  276. return cached.healthy;
  277. }
  278. /**
  279. * 清除健康检查缓存
  280. */
  281. clearHealthCache(mcpType?: string): void {
  282. if (mcpType) {
  283. this.healthCache.delete(mcpType);
  284. } else {
  285. this.healthCache.clear();
  286. }
  287. }
  288. // ==================== 启用/禁用状态管理 ====================
  289. /**
  290. * 设置 MCP 服务器启用状态
  291. */
  292. setEnabled(mcpType: string, enabled: boolean): void {
  293. console.log(`[McpTokenManager.setEnabled] Called with mcpType="${mcpType}", enabled=${enabled}`);
  294. if (typeof window === 'undefined') {
  295. console.log(`[McpTokenManager.setEnabled] SSR mode, skipping`);
  296. return;
  297. }
  298. const key = `mcp_enabled_${mcpType}`;
  299. const value = enabled.toString();
  300. localStorage.setItem(key, value);
  301. console.log(`[McpTokenManager.setEnabled] Saved: ${key}="${value}"`);
  302. // 验证保存是否成功
  303. const saved = localStorage.getItem(key);
  304. console.log(`[McpTokenManager.setEnabled] Verified: ${key}="${saved}"`);
  305. }
  306. /**
  307. * 获取 MCP 服务器启用状态
  308. * SSR 时返回 false(避免 hydration 不匹配)
  309. * 客户端默认返回 true(启用)
  310. */
  311. isEnabled(mcpType: string): boolean {
  312. if (typeof window === 'undefined') return false; // SSR 时返回 false
  313. const value = localStorage.getItem(`mcp_enabled_${mcpType}`);
  314. const result = value === null ? true : value === 'true';
  315. console.log(`[McpTokenManager.isEnabled] ${mcpType}: value="${value}", result=${result}`);
  316. return result;
  317. }
  318. /**
  319. * 切换 MCP 服务器启用状态
  320. */
  321. toggleEnabled(mcpType: string): boolean {
  322. console.log(`[McpTokenManager.toggleEnabled] Called with mcpType="${mcpType}"`);
  323. const currentState = this.isEnabled(mcpType);
  324. console.log(`[McpTokenManager.toggleEnabled] Current state: ${currentState}`);
  325. const newState = !currentState;
  326. console.log(`[McpTokenManager.toggleEnabled] New state: ${newState}`);
  327. this.setEnabled(mcpType, newState);
  328. return newState;
  329. }
  330. /**
  331. * 获取所有已启用的 MCP 列表
  332. * SSR 时返回空数组(避免 hydration 不匹配)
  333. */
  334. getEnabledMcpList(): string[] {
  335. if (typeof window === 'undefined') return []; // SSR 时返回空数组
  336. const result: string[] = [];
  337. console.log('[McpTokenManager.getEnabledMcpList] Checking all MCPs...');
  338. for (const mcpType of Object.keys(MCP_SERVERS)) {
  339. const value = localStorage.getItem(`mcp_enabled_${mcpType}`);
  340. const enabled = value === null ? true : value === 'true';
  341. console.log(` ${mcpType}: localStorage="${value}", enabled=${enabled}`);
  342. if (enabled) {
  343. result.push(mcpType);
  344. }
  345. }
  346. console.log(`[McpTokenManager.getEnabledMcpList] Result: [${result.join(', ')}]`);
  347. return result;
  348. }
  349. /**
  350. * 获取已启用且已登录的 MCP 数量
  351. */
  352. getEnabledLoggedInCount(): number {
  353. return Object.keys(MCP_SERVERS).filter(mcpType =>
  354. this.isEnabled(mcpType) && this.isLoggedIn(mcpType)
  355. ).length;
  356. }
  357. /**
  358. * 获取已启用的 MCP 数量
  359. */
  360. getEnabledCount(): number {
  361. return Object.keys(MCP_SERVERS).filter(mcpType => this.isEnabled(mcpType)).length;
  362. }
  363. }
  364. // 导出单例实例
  365. export const mcpTokenManager = McpTokenManager.getInstance();