Skip to main content
Glama
index.html15.5 kB
<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>MCP实时命令输出查看器</title> <style> * { margin: 0; padding: 0; box-sizing: border-box; } body { font-family: 'Consolas', 'Monaco', 'Courier New', monospace; background-color: #1e1e1e; color: #d4d4d4; height: 100vh; display: flex; flex-direction: column; } .header { background-color: #2d2d30; padding: 1rem; border-bottom: 1px solid #3e3e42; display: flex; justify-content: space-between; align-items: center; } .header h1 { color: #569cd6; font-size: 1.5rem; } .status { display: flex; align-items: center; gap: 0.5rem; } .status-indicator { width: 10px; height: 10px; border-radius: 50%; background-color: #f44747; } .status-indicator.connected { background-color: #4ec9b0; } .main-content { display: flex; flex: 1; overflow: hidden; } .sidebar { width: 300px; background-color: #252526; border-right: 1px solid #3e3e42; display: flex; flex-direction: column; } .sidebar-header { padding: 1rem; background-color: #2d2d30; border-bottom: 1px solid #3e3e42; font-weight: bold; } .session-list { flex: 1; overflow-y: auto; } .session-item { padding: 0.75rem 1rem; border-bottom: 1px solid #3e3e42; cursor: pointer; transition: background-color 0.2s; } .session-item:hover { background-color: #2a2d2e; } .session-item.active { background-color: #094771; border-left: 3px solid #007acc; } .session-id { font-weight: bold; color: #4ec9b0; font-size: 0.9rem; } .session-command { color: #ce9178; font-size: 0.8rem; margin-top: 0.25rem; word-break: break-all; } .session-status { display: flex; justify-content: space-between; align-items: center; margin-top: 0.25rem; font-size: 0.7rem; color: #858585; } .session-active { color: #4ec9b0; } .session-inactive { color: #f44747; } .output-panel { flex: 1; display: flex; flex-direction: column; background-color: #1e1e1e; } .output-header { padding: 1rem; background-color: #2d2d30; border-bottom: 1px solid #3e3e42; display: flex; justify-content: space-between; align-items: center; } .output-title { font-weight: bold; color: #569cd6; } .output-controls { display: flex; gap: 0.5rem; } .btn { padding: 0.25rem 0.75rem; background-color: #0e639c; color: white; border: none; border-radius: 3px; cursor: pointer; font-size: 0.8rem; transition: background-color 0.2s; } .btn:hover { background-color: #1177bb; } .btn.secondary { background-color: #5a5a5a; } .btn.secondary:hover { background-color: #6a6a6a; } .output-content { flex: 1; padding: 1rem; overflow-y: auto; white-space: pre-wrap; font-family: 'Consolas', 'Monaco', 'Courier New', monospace; font-size: 0.9rem; line-height: 1.4; background-color: #0d1117; } .no-session { display: flex; align-items: center; justify-content: center; height: 100%; color: #858585; font-style: italic; } .loading { color: #f9c74f; } .error { color: #f44747; } .success { color: #4ec9b0; } .auto-scroll-indicator { position: fixed; bottom: 20px; right: 20px; background-color: #0e639c; color: white; padding: 0.5rem 1rem; border-radius: 20px; font-size: 0.8rem; opacity: 0.8; transition: opacity 0.3s; } .auto-scroll-indicator.hidden { opacity: 0; pointer-events: none; } /* 滚动条样式 */ ::-webkit-scrollbar { width: 8px; } ::-webkit-scrollbar-track { background: #2d2d30; } ::-webkit-scrollbar-thumb { background: #424242; border-radius: 4px; } ::-webkit-scrollbar-thumb:hover { background: #4a4a4a; } </style> </head> <body> <div class="header"> <h1>MCP实时命令输出查看器</h1> <div class="status"> <div class="status-indicator" id="connectionStatus"></div> <span id="connectionText">连接中...</span> </div> </div> <div class="main-content"> <div class="sidebar"> <div class="sidebar-header">活跃会话</div> <div class="session-list" id="sessionList"> <div class="no-session">暂无活跃会话</div> </div> </div> <div class="output-panel"> <div class="output-header"> <div class="output-title" id="outputTitle">选择一个会话查看输出</div> <div class="output-controls"> <button class="btn secondary" id="clearBtn" onclick="clearOutput()">清空</button> <button class="btn" id="autoScrollBtn" onclick="toggleAutoScroll()">自动滚动: 开</button> </div> </div> <div class="output-content" id="outputContent"> <div class="no-session">请从左侧选择一个会话来查看实时输出</div> </div> </div> </div> <div class="auto-scroll-indicator hidden" id="autoScrollIndicator"> 自动滚动已启用 </div> <script> let ws = null; let currentSessionId = null; let sessions = new Map(); let autoScroll = true; let reconnectAttempts = 0; const maxReconnectAttempts = 5; // DOM元素 const connectionStatus = document.getElementById('connectionStatus'); const connectionText = document.getElementById('connectionText'); const sessionList = document.getElementById('sessionList'); const outputTitle = document.getElementById('outputTitle'); const outputContent = document.getElementById('outputContent'); const autoScrollBtn = document.getElementById('autoScrollBtn'); const autoScrollIndicator = document.getElementById('autoScrollIndicator'); // 初始化WebSocket连接 function initWebSocket() { const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; const wsUrl = `${protocol}//${window.location.host}`; ws = new WebSocket(wsUrl); ws.onopen = function() { console.log('WebSocket连接已建立'); connectionStatus.classList.add('connected'); connectionText.textContent = '已连接'; reconnectAttempts = 0; }; ws.onmessage = function(event) { try { const data = JSON.parse(event.data); handleWebSocketMessage(data); } catch (error) { console.error('解析WebSocket消息失败:', error); } }; ws.onclose = function() { console.log('WebSocket连接已关闭'); connectionStatus.classList.remove('connected'); connectionText.textContent = '连接断开'; // 尝试重连 if (reconnectAttempts < maxReconnectAttempts) { reconnectAttempts++; connectionText.textContent = `重连中... (${reconnectAttempts}/${maxReconnectAttempts})`; setTimeout(initWebSocket, 2000 * reconnectAttempts); } else { connectionText.textContent = '连接失败'; } }; ws.onerror = function(error) { console.error('WebSocket错误:', error); }; } // 处理WebSocket消息 function handleWebSocketMessage(data) { switch (data.type) { case 'session_list': data.sessions.forEach(session => { sessions.set(session.sessionId, session); }); updateSessionList(); break; case 'new_session': sessions.set(data.session.sessionId, { sessionId: data.session.sessionId, command: data.session.command, output: '', lastUpdate: data.session.lastUpdate, isActive: true }); updateSessionList(); break; case 'session_output': if (sessions.has(data.sessionId)) { const session = sessions.get(data.sessionId); if (data.fullOutput !== undefined) { session.output = data.fullOutput; } else { session.output += data.output || ''; } session.lastUpdate = data.lastUpdate; session.isActive = !data.isComplete; if (currentSessionId === data.sessionId) { updateOutputContent(); } updateSessionList(); } break; case 'session_ended': if (sessions.has(data.sessionId)) { const session = sessions.get(data.sessionId); session.isActive = false; session.lastUpdate = data.lastUpdate; updateSessionList(); } break; } } // 更新会话列表 function updateSessionList() { if (sessions.size === 0) { sessionList.innerHTML = '<div class="no-session">暂无活跃会话</div>'; return; } const sessionArray = Array.from(sessions.values()).sort((a, b) => new Date(b.lastUpdate) - new Date(a.lastUpdate) ); sessionList.innerHTML = sessionArray.map(session => ` <div class="session-item ${currentSessionId === session.sessionId ? 'active' : ''}" onclick="selectSession('${session.sessionId}')"> <div class="session-id">${session.sessionId}</div> <div class="session-command">${session.command || '未知命令'}</div> <div class="session-status"> <span class="${session.isActive ? 'session-active' : 'session-inactive'}"> ${session.isActive ? '运行中' : '已完成'} </span> <span>${new Date(session.lastUpdate).toLocaleTimeString()}</span> </div> </div> `).join(''); } // 选择会话 function selectSession(sessionId) { if (currentSessionId === sessionId) return; // 取消订阅当前会话 if (currentSessionId && ws && ws.readyState === WebSocket.OPEN) { ws.send(JSON.stringify({ type: 'unsubscribe_session', sessionId: currentSessionId })); } currentSessionId = sessionId; // 订阅新会话 if (ws && ws.readyState === WebSocket.OPEN) { ws.send(JSON.stringify({ type: 'subscribe_session', sessionId: sessionId })); } updateSessionList(); updateOutputContent(); } // 更新输出内容 function updateOutputContent() { if (!currentSessionId || !sessions.has(currentSessionId)) { outputTitle.textContent = '选择一个会话查看输出'; outputContent.innerHTML = '<div class="no-session">请从左侧选择一个会话来查看实时输出</div>'; return; } const session = sessions.get(currentSessionId); outputTitle.textContent = `会话: ${currentSessionId} - ${session.command || '未知命令'}`; const shouldScroll = autoScroll && (outputContent.scrollTop + outputContent.clientHeight >= outputContent.scrollHeight - 10); outputContent.textContent = session.output || '暂无输出...'; if (shouldScroll) { outputContent.scrollTop = outputContent.scrollHeight; } } // 清空输出 function clearOutput() { if (currentSessionId && sessions.has(currentSessionId)) { sessions.get(currentSessionId).output = ''; updateOutputContent(); } } // 切换自动滚动 function toggleAutoScroll() { autoScroll = !autoScroll; autoScrollBtn.textContent = `自动滚动: ${autoScroll ? '开' : '关'}`; autoScrollIndicator.classList.toggle('hidden', !autoScroll); if (autoScroll) { outputContent.scrollTop = outputContent.scrollHeight; } } // 监听滚动事件 outputContent.addEventListener('scroll', function() { const isAtBottom = outputContent.scrollTop + outputContent.clientHeight >= outputContent.scrollHeight - 10; if (!isAtBottom && autoScroll) { autoScroll = false; autoScrollBtn.textContent = '自动滚动: 关'; autoScrollIndicator.classList.add('hidden'); } }); // 初始化 initWebSocket(); </script> </body> </html>

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/sfz009900/kalilinuxmcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server