Skip to main content
Glama
test.html36.4 kB
<!DOCTYPE html> <html lang="zh-TW"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>MCP Maximo Server - 連接測試</title> <style> * { margin: 0; padding: 0; box-sizing: border-box; } body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Microsoft JhengHei', sans-serif; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); min-height: 100vh; padding: 20px; } .container { max-width: 900px; margin: 0 auto; } .header { text-align: center; color: white; margin-bottom: 30px; } .header h1 { font-size: 2.5rem; margin-bottom: 10px; } .header p { font-size: 1.1rem; opacity: 0.9; } .card { background: white; border-radius: 12px; padding: 30px; box-shadow: 0 10px 40px rgba(0, 0, 0, 0.1); margin-bottom: 20px; } .test-section { margin-bottom: 30px; } .test-section:last-child { margin-bottom: 0; } .test-section h2 { color: #333; margin-bottom: 15px; font-size: 1.5rem; display: flex; align-items: center; gap: 10px; } .test-button { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; border: none; padding: 12px 30px; border-radius: 8px; font-size: 1rem; cursor: pointer; transition: transform 0.2s, box-shadow 0.2s; margin-right: 10px; margin-bottom: 10px; } .test-button:hover { transform: translateY(-2px); box-shadow: 0 5px 20px rgba(102, 126, 234, 0.4); } .test-button:active { transform: translateY(0); } .test-button:disabled { opacity: 0.5; cursor: not-allowed; transform: none; } .result-box { margin-top: 15px; padding: 15px; border-radius: 8px; font-family: 'Courier New', monospace; font-size: 0.9rem; white-space: pre-wrap; word-wrap: break-word; max-height: 400px; overflow-y: auto; } .result-box.success { background: #d4edda; border: 1px solid #c3e6cb; color: #155724; } .result-box.error { background: #f8d7da; border: 1px solid #f5c6cb; color: #721c24; } .result-box.info { background: #d1ecf1; border: 1px solid #bee5eb; color: #0c5460; } .status-badge { display: inline-block; padding: 5px 12px; border-radius: 20px; font-size: 0.85rem; font-weight: bold; margin-left: 10px; } .status-badge.healthy { background: #d4edda; color: #155724; } .status-badge.unhealthy { background: #f8d7da; color: #721c24; } .status-badge.unknown { background: #e2e3e5; color: #383d41; } .loading { display: inline-block; width: 20px; height: 20px; border: 3px solid rgba(255,255,255,.3); border-radius: 50%; border-top-color: white; animation: spin 1s ease-in-out infinite; } @keyframes spin { to { transform: rotate(360deg); } } .input-group { margin-bottom: 15px; } .input-group label { display: block; margin-bottom: 5px; color: #555; font-weight: 500; } .input-group input { width: 100%; padding: 10px; border: 1px solid #ddd; border-radius: 6px; font-size: 1rem; } .input-group input:focus { outline: none; border-color: #667eea; box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1); } .tool-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 10px; margin-top: 15px; } .config-info { background: #f8f9fa; padding: 15px; border-radius: 8px; margin-bottom: 15px; } .config-info p { margin-bottom: 8px; color: #555; } .config-info strong { color: #333; } /* 日誌區域樣式 */ .log-container { background: #1e1e1e; border-radius: 8px; padding: 15px; max-height: 400px; overflow-y: auto; font-family: 'Courier New', monospace; font-size: 0.85rem; margin-top: 15px; } .log-entry { margin-bottom: 10px; padding: 8px; border-left: 3px solid #666; background: #2d2d2d; } .log-entry.log-request { border-left-color: #4a9eff; } .log-entry.log-response { border-left-color: #4caf50; } .log-entry.log-error { border-left-color: #f44336; } .log-timestamp { color: #888; font-size: 0.75rem; } .log-method { color: #4a9eff; font-weight: bold; } .log-url { color: #fff; } .log-status { color: #4caf50; } .log-status.error { color: #f44336; } .log-data { color: #ddd; margin-top: 5px; white-space: pre-wrap; word-wrap: break-word; } .log-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px; } .log-header h3 { color: #fff; font-size: 1rem; margin: 0; } .clear-log-btn { background: #444; color: white; border: none; padding: 5px 15px; border-radius: 4px; font-size: 0.85rem; cursor: pointer; transition: background 0.2s; } .clear-log-btn:hover { background: #555; } </style> </head> <body> <div class="container"> <div class="header"> <h1>🔧 MCP Maximo Server</h1> <p>連接測試與驗證工具</p> </div> <!-- 系統健康檢查 --> <div class="card"> <div class="test-section"> <h2> 🏥 系統健康檢查 <span id="health-badge" class="status-badge unknown">未檢測</span> </h2> <div class="config-info"> <p><strong>Server URL:</strong> <span id="server-url">http://localhost:8000</span></p> </div> <button class="test-button" onclick="testHealth()">檢查系統健康</button> <div id="health-result"></div> <!-- 此區塊的日誌 --> <div class="log-container" style="display: none;"> <div class="log-header"> <h3>📋 請求/回應日誌</h3> <button class="clear-log-btn" onclick="clearSectionLogs('health-logs')">清除</button> </div> <div id="health-logs"></div> </div> </div> </div> <!-- Maximo 連接測試 --> <div class="card"> <div class="test-section"> <h2>🔌 Maximo 連接測試</h2> <button class="test-button" onclick="testMaximoConnection()">測試 Maximo 連接</button> <div id="maximo-result"></div> <!-- 此區塊的日誌 --> <div class="log-container" style="display: none;"> <div class="log-header"> <h3>📋 請求/回應日誌</h3> <button class="clear-log-btn" onclick="clearSectionLogs('maximo-logs')">清除</button> </div> <div id="maximo-logs"></div> </div> </div> </div> <!-- MCP Tools 測試 --> <div class="card"> <div class="test-section"> <h2>🛠️ MCP Tools 功能測試</h2> <p style="color: #666; margin-bottom: 15px;">測試各項 Maximo API 工具是否正常運作(需要提供測試資料)</p> <h3 style="color: #555; margin-top: 20px; margin-bottom: 10px;">資產管理測試</h3> <div class="input-group"> <label>Asset Number (測試用資產編號):</label> <input type="text" id="test-assetnum" placeholder="例如: PUMP001"> </div> <div class="input-group"> <label>Site ID:</label> <input type="text" id="test-siteid" placeholder="例如: BEDFORD"> </div> <div class="tool-grid"> <button class="test-button" onclick="testGetAsset()">測試查詢資產</button> <button class="test-button" onclick="testSearchAssets()">測試搜尋資產</button> </div> <h3 style="color: #555; margin-top: 20px; margin-bottom: 10px;">工單管理測試</h3> <div class="input-group"> <label>Work Order Number (測試用工單編號):</label> <input type="text" id="test-wonum" placeholder="例如: WO1001"> </div> <div class="tool-grid"> <button class="test-button" onclick="testGetWorkOrder()">測試查詢工單</button> <button class="test-button" onclick="testSearchWorkOrders()">測試搜尋工單</button> </div> <h3 style="color: #555; margin-top: 20px; margin-bottom: 10px;">庫存管理測試</h3> <div class="input-group"> <label>Item Number (測試用物料編號):</label> <input type="text" id="test-itemnum" placeholder="例如: FILTER-001"> </div> <div class="tool-grid"> <button class="test-button" onclick="testGetInventory()">測試查詢庫存</button> <button class="test-button" onclick="testSearchInventory()">測試搜尋庫存</button> </div> <h3 style="color: #555; margin-top: 20px; margin-bottom: 10px;">用戶管理測試</h3> <div class="input-group"> <label>User ID (測試用用戶 ID):</label> <input type="text" id="test-userid" placeholder="例如: testuser"> </div> <div class="tool-grid"> <button class="test-button" onclick="testGetUserStatus()">測試查詢用戶狀態</button> <button class="test-button" onclick="testSearchUsers()">測試搜尋用戶</button> <button class="test-button" onclick="testUnlockUser()">測試解鎖帳號</button> </div> <div id="tools-result"></div> <!-- 此區塊的日誌 --> <div class="log-container" style="display: none;"> <div class="log-header"> <h3>📋 請求/回應日誌</h3> <button class="clear-log-btn" onclick="clearSectionLogs('tools-logs')">清除</button> </div> <div id="tools-logs"></div> </div> </div> </div> <!-- Maximo 認證設定 --> <div class="card"> <div class="test-section"> <h2>🔑 Maximo 認證設定</h2> <p style="color: #666; margin-bottom: 15px;">若需要測試需要認證的端點,請輸入 Maximo 用戶名和密碼</p> <div class="input-group"> <label>Maximo Username (選填):</label> <input type="text" id="maxauth-username" placeholder="輸入 Maximo 用戶名"> </div> <div class="input-group"> <label>Maximo Password (選填):</label> <input type="password" id="maxauth-password" placeholder="輸入 Maximo 密碼"> </div> </div> </div> </div> <script> // 從 URL 參數或 localStorage 獲取 server URL,預設使用 port 8001 const urlParams = new URLSearchParams(window.location.search); const defaultServer = window.location.port ? `http://${window.location.hostname}:${window.location.port}` : 'http://localhost:8001'; const serverUrl = urlParams.get('server') || localStorage.getItem('serverUrl') || defaultServer; document.getElementById('server-url').textContent = serverUrl; // Log management - track current log target let currentLogTarget = null; const logs = {}; function setLogTarget(targetId) { currentLogTarget = targetId; // Show the log container for this section const container = document.getElementById(targetId); if (container) { container.parentElement.style.display = 'block'; } } function addLog(type, method, url, status, data) { const timestamp = new Date().toLocaleString('zh-TW', { hour12: false, year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', second: '2-digit' }); const log = { type, method, url, status, data, timestamp }; // Store log in target-specific array if (currentLogTarget) { if (!logs[currentLogTarget]) { logs[currentLogTarget] = []; } logs[currentLogTarget].push(log); displayLog(log, currentLogTarget); } } function displayLog(log, targetId) { const logDiv = document.createElement('div'); logDiv.className = `log-entry log-${log.type}`; let statusHtml = ''; if (log.status !== null && log.status !== undefined) { const statusClass = log.status >= 400 ? 'error' : ''; statusHtml = `<div><span class="log-status ${statusClass}">Status: ${log.status}</span></div>`; } let dataPreview = ''; try { const dataStr = typeof log.data === 'string' ? log.data : JSON.stringify(log.data, null, 2); dataPreview = dataStr.length > 500 ? dataStr.substring(0, 500) + '...' : dataStr; } catch (e) { dataPreview = String(log.data); } logDiv.innerHTML = ` <div class="log-timestamp">${log.timestamp}</div> <div><span class="log-method">${log.method}</span> <span class="log-url">${log.url}</span></div> ${statusHtml} <div class="log-data">${dataPreview}</div> `; const container = document.getElementById(targetId); if (container) { container.insertBefore(logDiv, container.firstChild); // Keep only last 20 logs in each section while (container.children.length > 20) { container.removeChild(container.lastChild); } } } function clearSectionLogs(targetId) { if (logs[targetId]) { logs[targetId].length = 0; } const container = document.getElementById(targetId); if (container) { container.innerHTML = '<div style="color: #888; text-align: center; padding: 20px;">日誌已清除</div>'; } } // Wrap fetch to log all requests const originalFetch = window.fetch; window.fetch = async (...args) => { const [url, options = {}] = args; const method = options.method || 'GET'; // Log request let requestData = {}; if (options.body) { try { requestData = JSON.parse(options.body); } catch (e) { requestData = { body: options.body }; } } addLog('request', method, url, null, requestData); try { const response = await originalFetch(...args); const clone = response.clone(); // Try to parse response let responseData; try { responseData = await clone.json(); } catch (e) { responseData = { message: 'Non-JSON response' }; } addLog('response', method, url, response.status, responseData); return response; } catch (error) { addLog('error', method, url, null, { error: error.message, stack: error.stack }); throw error; } }; function getHeaders() { const username = document.getElementById('maxauth-username').value; const password = document.getElementById('maxauth-password').value; const headers = { 'Content-Type': 'application/json' }; if (username && password) { // Encode username:password to Base64 const credentials = `${username}:${password}`; const base64Credentials = btoa(credentials); headers['maxauth'] = base64Credentials; } return headers; } function showResult(elementId, message, type = 'info') { const element = document.getElementById(elementId); element.innerHTML = `<div class="result-box ${type}">${message}</div>`; } async function testHealth() { const button = event.target; button.disabled = true; button.innerHTML = '<span class="loading"></span> 檢測中...'; // Set log target for this test setLogTarget('health-logs'); try { const response = await fetch(`${serverUrl}/health`); const data = await response.json(); const badge = document.getElementById('health-badge'); if (data.status === 'healthy') { badge.className = 'status-badge healthy'; badge.textContent = '✓ 健康'; } else { badge.className = 'status-badge unhealthy'; badge.textContent = '✗ 異常'; } const resultText = `狀態: ${data.status} 版本: ${data.version} 環境: ${data.environment} 快取狀態: ${data.cache} Maximo 連接: ${data.maximo} 完整回應: ${JSON.stringify(data, null, 2)}`; showResult('health-result', resultText, data.status === 'healthy' ? 'success' : 'error'); } catch (error) { const badge = document.getElementById('health-badge'); badge.className = 'status-badge unhealthy'; badge.textContent = '✗ 錯誤'; showResult('health-result', `錯誤: ${error.message}\n\n請確認:\n1. MCP Server 是否正在運行\n2. Server URL 是否正確: ${serverUrl}\n3. 網路連接是否正常`, 'error'); } finally { button.disabled = false; button.textContent = '檢查系統健康'; } } async function testMaximoConnection() { const button = event.target; button.disabled = true; button.innerHTML = '<span class="loading"></span> 測試中...'; // Set log target for this test setLogTarget('maximo-logs'); try { const response = await fetch(`${serverUrl}/api/test-maximo`, { method: 'POST', headers: getHeaders() }); const data = await response.json(); if (data.success) { showResult('maximo-result', `✓ Maximo 連接成功!\n\n${JSON.stringify(data, null, 2)}`, 'success'); } else { showResult('maximo-result', `✗ Maximo 連接失敗\n\n${JSON.stringify(data, null, 2)}`, 'error'); } } catch (error) { showResult('maximo-result', `錯誤: ${error.message}\n\n請檢查:\n1. Maximo API URL 是否正確\n2. Maximo 認證資訊(用戶名和密碼)是否正確\n3. 網路是否可以連接到 Maximo`, 'error'); } finally { button.disabled = false; button.textContent = '測試 Maximo 連接'; } } async function testGetAsset() { const assetnum = document.getElementById('test-assetnum').value; const siteid = document.getElementById('test-siteid').value; if (!assetnum) { showResult('tools-result', '請輸入 Asset Number', 'error'); return; } const button = event.target; button.disabled = true; button.innerHTML = '<span class="loading"></span> 測試中...'; // Set log target for this test setLogTarget('tools-logs'); try { const response = await fetch(`${serverUrl}/api/test-tool`, { method: 'POST', headers: getHeaders(), body: JSON.stringify({ tool: 'get_asset', params: { assetnum, siteid } }) }); const data = await response.json(); if (response.ok) { showResult('tools-result', `✓ 查詢資產成功!\n\n${JSON.stringify(data, null, 2)}`, 'success'); } else { showResult('tools-result', `✗ 查詢失敗\n\n${JSON.stringify(data, null, 2)}`, 'error'); } } catch (error) { showResult('tools-result', `錯誤: ${error.message}`, 'error'); } finally { button.disabled = false; button.textContent = '測試查詢資產'; } } async function testSearchAssets() { const button = event.target; button.disabled = true; button.innerHTML = '<span class="loading"></span> 測試中...'; // Set log target for this test setLogTarget('tools-logs'); try { const response = await fetch(`${serverUrl}/api/test-tool`, { method: 'POST', headers: getHeaders(), body: JSON.stringify({ tool: 'search_assets', params: { page_size: 5 } }) }); const data = await response.json(); if (response.ok) { showResult('tools-result', `✓ 搜尋資產成功!\n找到 ${data.result?.length || 0} 筆資料\n\n${JSON.stringify(data, null, 2)}`, 'success'); } else { showResult('tools-result', `✗ 搜尋失敗\n\n${JSON.stringify(data, null, 2)}`, 'error'); } } catch (error) { showResult('tools-result', `錯誤: ${error.message}`, 'error'); } finally { button.disabled = false; button.textContent = '測試搜尋資產'; } } async function testGetWorkOrder() { const wonum = document.getElementById('test-wonum').value; const siteid = document.getElementById('test-siteid').value; if (!wonum) { showResult('tools-result', '請輸入 Work Order Number', 'error'); return; } const button = event.target; button.disabled = true; button.innerHTML = '<span class="loading"></span> 測試中...'; // Set log target for this test setLogTarget('tools-logs'); try { const response = await fetch(`${serverUrl}/api/test-tool`, { method: 'POST', headers: getHeaders(), body: JSON.stringify({ tool: 'get_work_order', params: { wonum, siteid } }) }); const data = await response.json(); if (response.ok) { showResult('tools-result', `✓ 查詢工單成功!\n\n${JSON.stringify(data, null, 2)}`, 'success'); } else { showResult('tools-result', `✗ 查詢失敗\n\n${JSON.stringify(data, null, 2)}`, 'error'); } } catch (error) { showResult('tools-result', `錯誤: ${error.message}`, 'error'); } finally { button.disabled = false; button.textContent = '測試查詢工單'; } } async function testSearchWorkOrders() { const button = event.target; button.disabled = true; button.innerHTML = '<span class="loading"></span> 測試中...'; // Set log target for this test setLogTarget('tools-logs'); try { const response = await fetch(`${serverUrl}/api/test-tool`, { method: 'POST', headers: getHeaders(), body: JSON.stringify({ tool: 'search_work_orders', params: { page_size: 5 } }) }); const data = await response.json(); if (response.ok) { showResult('tools-result', `✓ 搜尋工單成功!\n找到 ${data.result?.length || 0} 筆資料\n\n${JSON.stringify(data, null, 2)}`, 'success'); } else { showResult('tools-result', `✗ 搜尋失敗\n\n${JSON.stringify(data, null, 2)}`, 'error'); } } catch (error) { showResult('tools-result', `錯誤: ${error.message}`, 'error'); } finally { button.disabled = false; button.textContent = '測試搜尋工單'; } } async function testGetInventory() { const itemnum = document.getElementById('test-itemnum').value; const siteid = document.getElementById('test-siteid').value; if (!itemnum) { showResult('tools-result', '請輸入 Item Number', 'error'); return; } const button = event.target; button.disabled = true; button.innerHTML = '<span class="loading"></span> 測試中...'; // Set log target for this test setLogTarget('tools-logs'); try { const response = await fetch(`${serverUrl}/api/test-tool`, { method: 'POST', headers: getHeaders(), body: JSON.stringify({ tool: 'get_inventory', params: { itemnum, siteid } }) }); const data = await response.json(); if (response.ok) { showResult('tools-result', `✓ 查詢庫存成功!\n\n${JSON.stringify(data, null, 2)}`, 'success'); } else { showResult('tools-result', `✗ 查詢失敗\n\n${JSON.stringify(data, null, 2)}`, 'error'); } } catch (error) { showResult('tools-result', `錯誤: ${error.message}`, 'error'); } finally { button.disabled = false; button.textContent = '測試查詢庫存'; } } async function testSearchInventory() { const button = event.target; button.disabled = true; button.innerHTML = '<span class="loading"></span> 測試中...'; // Set log target for this test setLogTarget('tools-logs'); try { const response = await fetch(`${serverUrl}/api/test-tool`, { method: 'POST', headers: getHeaders(), body: JSON.stringify({ tool: 'search_inventory', params: { page_size: 5 } }) }); const data = await response.json(); if (response.ok) { showResult('tools-result', `✓ 搜尋庫存成功!\n找到 ${data.result?.length || 0} 筆資料\n\n${JSON.stringify(data, null, 2)}`, 'success'); } else { showResult('tools-result', `✗ 搜尋失敗\n\n${JSON.stringify(data, null, 2)}`, 'error'); } } catch (error) { showResult('tools-result', `錯誤: ${error.message}`, 'error'); } finally { button.disabled = false; button.textContent = '測試搜尋庫存'; } } async function testGetUserStatus() { const userid = document.getElementById('test-userid').value; if (!userid) { showResult('tools-result', '請輸入 User ID', 'error'); return; } const button = event.target; button.disabled = true; button.innerHTML = '<span class="loading"></span> 測試中...'; // Set log target for this test setLogTarget('tools-logs'); try { const response = await fetch(`${serverUrl}/api/test-tool`, { method: 'POST', headers: getHeaders(), body: JSON.stringify({ tool: 'get_user_status', params: { userid } }) }); const data = await response.json(); if (response.ok) { const user = data.result; const statusInfo = ` 用戶狀態資訊: - User ID: ${user.userid} - 顯示名稱: ${user.displayname || 'N/A'} - 狀態: ${user.status} - 是否啟用: ${user.is_active ? '是' : '否'} - 是否鎖定: ${user.is_locked ? '是' : '否'} - 登入失敗次數: ${user.failed_login_count} - Email: ${user.emailaddress || 'N/A'} - 電話: ${user.primaryphone || 'N/A'} 完整回應: ${JSON.stringify(data, null, 2)}`; showResult('tools-result', `✓ 查詢用戶狀態成功!\n${statusInfo}`, 'success'); } else { showResult('tools-result', `✗ 查詢失敗\n\n${JSON.stringify(data, null, 2)}`, 'error'); } } catch (error) { showResult('tools-result', `錯誤: ${error.message}`, 'error'); } finally { button.disabled = false; button.textContent = '測試查詢用戶狀態'; } } async function testSearchUsers() { const button = event.target; button.disabled = true; button.innerHTML = '<span class="loading"></span> 測試中...'; // Set log target for this test setLogTarget('tools-logs'); try { const response = await fetch(`${serverUrl}/api/test-tool`, { method: 'POST', headers: getHeaders(), body: JSON.stringify({ tool: 'search_users', params: { page_size: 5 } }) }); const data = await response.json(); if (response.ok) { const users = data.result || []; let summary = `找到 ${users.length} 個用戶\n\n`; if (users.length > 0) { summary += '用戶列表:\n'; users.forEach((user, idx) => { summary += `${idx + 1}. ${user.userid} (${user.displayname || 'N/A'}) - 狀態: ${user.status}${user.is_locked ? ' [鎖定]' : ''}\n`; }); } showResult('tools-result', `✓ 搜尋用戶成功!\n${summary}\n完整回應:\n${JSON.stringify(data, null, 2)}`, 'success'); } else { showResult('tools-result', `✗ 搜尋失敗\n\n${JSON.stringify(data, null, 2)}`, 'error'); } } catch (error) { showResult('tools-result', `錯誤: ${error.message}`, 'error'); } finally { button.disabled = false; button.textContent = '測試搜尋用戶'; } } async function testUnlockUser() { const userid = document.getElementById('test-userid').value; if (!userid) { showResult('tools-result', '請輸入 User ID', 'error'); return; } if (!confirm(`確定要解鎖用戶 "${userid}" 嗎?\n\n此操作將:\n1. 設定帳號狀態為 ACTIVE\n2. 清除鎖定狀態\n3. 重設登入失敗次數為 0`)) { return; } const button = event.target; button.disabled = true; button.innerHTML = '<span class="loading"></span> 解鎖中...'; // Set log target for this test setLogTarget('tools-logs'); try { const response = await fetch(`${serverUrl}/api/test-tool`, { method: 'POST', headers: getHeaders(), body: JSON.stringify({ tool: 'unlock_user_account', params: { userid, memo: '透過測試頁面解鎖' } }) }); const data = await response.json(); if (response.ok) { showResult('tools-result', `✓ 解鎖用戶成功!\n\n用戶 "${userid}" 已成功解鎖\n\n完整回應:\n${JSON.stringify(data, null, 2)}`, 'success'); } else { showResult('tools-result', `✗ 解鎖失敗\n\n${JSON.stringify(data, null, 2)}`, 'error'); } } catch (error) { showResult('tools-result', `錯誤: ${error.message}`, 'error'); } finally { button.disabled = false; button.textContent = '測試解鎖帳號'; } } // 頁面載入時執行健康檢查 window.addEventListener('load', () => { setTimeout(testHealth, 500); }); </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/nick0918964388/mcp-maximo-server'

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