/** * MCP 服务器配置 */ export interface McpServerConfig { id: string; name: string; url: string; authType: 'none' | 'jwt'; loginApi?: string; baseUrl?: string; description?: string; } /** * MCP 服务器配置列表 */ export const MCP_SERVERS: Record = { 'novel-translator': { id: 'novel-translator', name: 'Novel Translator MCP', url: 'https://d8d-ai-vscode-8080-223-236-template-6-group.dev.d8d.fun/mcp', authType: 'none', }, 'novel-platform-user': { id: 'novel-platform-user', name: 'Novel Platform User MCP', url: 'https://d8d-ai-vscode-8080-223-238-template-6-group.dev.d8d.fun/mcp/', authType: 'jwt', loginApi: '/api/v1/auth/login', baseUrl: 'https://d8d-ai-vscode-8080-223-238-template-6-group.dev.d8d.fun', }, 'novel-platform-admin': { id: 'novel-platform-admin', name: 'Novel Platform Admin MCP', url: 'https://d8d-ai-vscode-8080-223-238-template-6-group.dev.d8d.fun/admin-mcp/', authType: 'jwt', loginApi: '/api/v1/auth/admin-login', baseUrl: 'https://d8d-ai-vscode-8080-223-238-template-6-group.dev.d8d.fun', }, 'template-241-mcp-app': { id: 'template-241-mcp-app', name: 'Template 241 MCP App', url: 'https://d8d-ai-vscode-8080-223-241-template-6-group.dev.d8d.fun/mcp', authType: 'none', description: 'MCP App 架构 - 36 个 shadcn/ui 组件', }, }; /** * MCP 登录响应 */ export interface McpLoginResponse { success: boolean; token?: string; username?: string; error?: string; detail?: string; } /** * MCP 健康状态 */ export interface McpHealthStatus { healthy: boolean; latency?: number; // 响应延迟(毫秒) error?: string; } /** * MCP Token 管理器 * * 核心概念: * - Web UI 是 MCP 测试工具,不需要全局登录 * - 每个 MCP 有独立的认证状态 * - 可以同时管理多个 MCP 的登录 * * 状态区分: * - isHealthy: MCP 服务器是否正常运行(health check) * - isLoggedIn: 是否有有效的认证 token */ export class McpTokenManager { private static instance: McpTokenManager; private healthCache: Map = new Map(); private readonly HEALTH_CACHE_TTL = 10000; // 健康检查缓存 10 秒 private constructor() {} static getInstance(): McpTokenManager { if (!McpTokenManager.instance) { McpTokenManager.instance = new McpTokenManager(); } return McpTokenManager.instance; } /** * 保存 MCP Token */ saveToken(mcpType: string, token: string, username: string): void { if (typeof window === 'undefined') return; const key = `mcp_token_${mcpType}`; localStorage.setItem(key, token); localStorage.setItem(`mcp_token_${mcpType}_time`, Date.now().toString()); localStorage.setItem(`mcp_username_${mcpType}`, username); // 调试:验证保存 console.log(`[McpTokenManager] Saved token for ${mcpType}:`, { key, tokenLength: token?.length, username, verified: localStorage.getItem(key) === token }); } /** * 获取 MCP Token */ getToken(mcpType: string): string | null { if (typeof window === 'undefined') return null; return localStorage.getItem(`mcp_token_${mcpType}`); } /** * 获取 MCP 用户名 */ getUsername(mcpType: string): string | null { if (typeof window === 'undefined') return null; return localStorage.getItem(`mcp_username_${mcpType}`); } /** * 检查 MCP 是否已登录 */ isLoggedIn(mcpType: string): boolean { return !!this.getToken(mcpType); } /** * 获取所有已登录的 MCP Token */ getAllTokens(): Record { const tokens: Record = {}; for (const mcpType of Object.keys(MCP_SERVERS)) { const token = this.getToken(mcpType); if (token) { tokens[mcpType] = token; } } return tokens; } /** * 获取所有已登录的 MCP 列表 */ getLoggedInMcpList(): string[] { return Object.keys(MCP_SERVERS).filter(mcpType => this.isLoggedIn(mcpType)); } /** * 清除 MCP Token */ clearToken(mcpType: string): void { if (typeof window === 'undefined') return; localStorage.removeItem(`mcp_token_${mcpType}`); localStorage.removeItem(`mcp_token_${mcpType}_time`); localStorage.removeItem(`mcp_username_${mcpType}`); } /** * 清除所有 MCP Token */ clearAllTokens(): void { for (const mcpType of Object.keys(MCP_SERVERS)) { this.clearToken(mcpType); } } /** * 获取 Token 存储时长(毫秒) */ getTokenAge(mcpType: string): number { if (typeof window === 'undefined') return 0; const time = localStorage.getItem(`mcp_token_${mcpType}_time`); return time ? Date.now() - parseInt(time) : 0; } /** * 获取 Token 剩余有效时间(假设 24 小时有效) */ getTokenRemainingTime(mcpType: string): number { const TOKEN_VALIDITY = 24 * 60 * 60 * 1000; // 24 小时 const age = this.getTokenAge(mcpType); return Math.max(0, TOKEN_VALIDITY - age); } /** * 登录 MCP 服务器 */ async loginMcp(mcpType: string, email: string, password: string): Promise { const config = MCP_SERVERS[mcpType]; if (!config || config.authType !== 'jwt') { return { success: false, error: '该 MCP 不需要登录' }; } // 使用本地后端代理端点 const isAdmin = mcpType === 'novel-platform-admin'; const proxyUrl = isAdmin ? '/api/auth/admin-login' : '/api/auth/login'; try { const response = await fetch(proxyUrl, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ email, password }), }); const data = await response.json(); console.log(`[McpTokenManager.loginMcp] Response:`, { ok: response.ok, status: response.status, success: data.success, hasToken: !!data.token, username: data.username }); if (response.ok && data.success && data.token) { this.saveToken(mcpType, data.token, data.username || email); return { success: true, token: data.token, username: data.username }; } else { return { success: false, error: data.detail || data.error || '登录失败', }; } } catch (e) { console.error(`[McpTokenManager.loginMcp] Error:`, e); return { success: false, error: e instanceof Error ? e.message : '网络错误', }; } } /** * 登出 MCP 服务器 */ logoutMcp(mcpType: string): void { this.clearToken(mcpType); this.healthCache.delete(mcpType); } /** * 检查 MCP 服务器健康状态 * 通过后端代理端点 /api/mcp/health/:mcpType 进行健康检查 */ async checkHealth(mcpType: string): Promise { // 检查缓存 const cached = this.healthCache.get(mcpType); if (cached && Date.now() - cached.timestamp < this.HEALTH_CACHE_TTL) { return { healthy: cached.healthy }; } const config = MCP_SERVERS[mcpType]; if (!config) { return { healthy: false, error: '未知的 MCP 类型' }; } try { const startTime = Date.now(); // 使用后端代理端点进行健康检查 const response = await fetch(`/api/mcp/health/${mcpType}`, { method: 'GET', headers: { 'Content-Type': 'application/json' }, }); const latency = Date.now() - startTime; if (response.ok) { const data = await response.json(); const healthy = data.status === 'healthy' || data.healthy === true; // 更新缓存 this.healthCache.set(mcpType, { healthy, timestamp: Date.now(), }); return { healthy, latency }; } else { const error = await response.text().catch(() => '未知错误'); this.healthCache.set(mcpType, { healthy: false, timestamp: Date.now() }); return { healthy: false, error: `HTTP ${response.status}: ${error}` }; } } catch (e) { const errorMsg = e instanceof Error ? e.message : '网络错误'; this.healthCache.set(mcpType, { healthy: false, timestamp: Date.now() }); return { healthy: false, error: errorMsg }; } } /** * 检查 MCP 服务器是否健康(使用缓存,不发起网络请求) */ isHealthy(mcpType: string): boolean { const cached = this.healthCache.get(mcpType); if (!cached) return false; // 尚未检查过,默认不健康 // 如果缓存过期,返回 false 以触发新的检查 if (Date.now() - cached.timestamp > this.HEALTH_CACHE_TTL) return false; return cached.healthy; } /** * 清除健康检查缓存 */ clearHealthCache(mcpType?: string): void { if (mcpType) { this.healthCache.delete(mcpType); } else { this.healthCache.clear(); } } // ==================== 启用/禁用状态管理 ==================== /** * 设置 MCP 服务器启用状态 */ setEnabled(mcpType: string, enabled: boolean): void { console.log(`[McpTokenManager.setEnabled] Called with mcpType="${mcpType}", enabled=${enabled}`); if (typeof window === 'undefined') { console.log(`[McpTokenManager.setEnabled] SSR mode, skipping`); return; } const key = `mcp_enabled_${mcpType}`; const value = enabled.toString(); localStorage.setItem(key, value); console.log(`[McpTokenManager.setEnabled] Saved: ${key}="${value}"`); // 验证保存是否成功 const saved = localStorage.getItem(key); console.log(`[McpTokenManager.setEnabled] Verified: ${key}="${saved}"`); } /** * 获取 MCP 服务器启用状态 * SSR 时返回 false(避免 hydration 不匹配) * 客户端默认返回 true(启用) */ isEnabled(mcpType: string): boolean { if (typeof window === 'undefined') return false; // SSR 时返回 false const value = localStorage.getItem(`mcp_enabled_${mcpType}`); const result = value === null ? true : value === 'true'; console.log(`[McpTokenManager.isEnabled] ${mcpType}: value="${value}", result=${result}`); return result; } /** * 切换 MCP 服务器启用状态 */ toggleEnabled(mcpType: string): boolean { console.log(`[McpTokenManager.toggleEnabled] Called with mcpType="${mcpType}"`); const currentState = this.isEnabled(mcpType); console.log(`[McpTokenManager.toggleEnabled] Current state: ${currentState}`); const newState = !currentState; console.log(`[McpTokenManager.toggleEnabled] New state: ${newState}`); this.setEnabled(mcpType, newState); return newState; } /** * 获取所有已启用的 MCP 列表 * SSR 时返回空数组(避免 hydration 不匹配) */ getEnabledMcpList(): string[] { if (typeof window === 'undefined') return []; // SSR 时返回空数组 const result: string[] = []; console.log('[McpTokenManager.getEnabledMcpList] Checking all MCPs...'); for (const mcpType of Object.keys(MCP_SERVERS)) { const value = localStorage.getItem(`mcp_enabled_${mcpType}`); const enabled = value === null ? true : value === 'true'; console.log(` ${mcpType}: localStorage="${value}", enabled=${enabled}`); if (enabled) { result.push(mcpType); } } console.log(`[McpTokenManager.getEnabledMcpList] Result: [${result.join(', ')}]`); return result; } /** * 获取已启用且已登录的 MCP 数量 */ getEnabledLoggedInCount(): number { return Object.keys(MCP_SERVERS).filter(mcpType => this.isEnabled(mcpType) && this.isLoggedIn(mcpType) ).length; } /** * 获取已启用的 MCP 数量 */ getEnabledCount(): number { return Object.keys(MCP_SERVERS).filter(mcpType => this.isEnabled(mcpType)).length; } } // 导出单例实例 export const mcpTokenManager = McpTokenManager.getInstance();