Skip to main content
Glama

UTCP-MCP Bridge

Official
index.html17.8 kB
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>UTCP-MCP Bridge</title> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.16/codemirror.min.css"> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.16/theme/dracula.min.css"> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.16/addon/lint/lint.min.css"> <style> body { margin: 0; font-family: 'Segoe UI', Arial, sans-serif; background: #f4f6fa; color: #222; } .container { display: flex; height: 100vh; } .sidebar { width: 300px; background: #232946; color: #fff; padding: 24px 0; box-shadow: 2px 0 8px rgba(0, 0, 0, 0.04); display: flex; flex-direction: column; } .sidebar h2 { margin: 0 0 24px 24px; font-size: 1.3em; letter-spacing: 1px; } .provider-list { list-style: none; padding: 0 0 0 24px; margin: 0; } .provider-list li { margin-bottom: 16px; cursor: pointer; padding: 6px 12px; border-radius: 6px; transition: background 0.2s; } .provider-list li.selected, .provider-list li:hover { background: #394867; } .main { flex: 1; padding: 32px 48px; overflow-y: auto; } .tools-header { font-size: 1.5em; margin-bottom: 18px; } .tool-list { display: grid; grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); gap: 24px; } .tool-card { background: #fff; border-radius: 10px; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06); padding: 20px 18px 16px 18px; display: flex; flex-direction: column; } .tool-card h3 { margin: 0 0 8px 0; font-size: 1.05em; color: #232946; word-break: break-all; white-space: normal; overflow-wrap: anywhere; cursor: pointer; } .tool-card .desc { font-size: 0.98em; color: #555; margin-bottom: 10px; } .tool-card .inputs { font-size: 0.93em; color: #888; } button { transition: background 0.2s; } button:hover { opacity: 0.9; } button:disabled { opacity: 0.6; cursor: not-allowed !important; } /* --- Modal Styles --- */ .modal-overlay { display: none; position: fixed; top: 0; left: 0; width: 100vw; height: 100vh; background: rgba(0, 0, 0, 0.25); z-index: 1000; align-items: center; justify-content: center; } .modal-content { background: #fff; padding: 24px; border-radius: 10px; width: 70vw; max-width: 1000px; height: 70vh; box-shadow: 0 4px 24px rgba(0, 0, 0, 0.18); display: flex; flex-direction: column; } .modal-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 16px; } .modal-header h3 { margin: 0; } .modal-btn { padding: 8px 20px; border-radius: 6px; color: #fff; border: none; cursor: pointer; font-size: 0.95em; } #editProvidersBtn { margin-right: 16px; padding: 6px 12px; border-radius: 6px; background: #394867; color: #fff; border: none; cursor: pointer; font-size: 0.9em; } #editProvidersBtn:hover { background: #2d3a5f; } #saveProvidersBtn { background: #394867; position: relative; } #saveProvidersBtn:hover:not(:disabled) { background: #2d3a5f; } .error-message { color: #d8000c; background-color: #ffcccc; border: 1px solid #d8000c; border-radius: 6px; padding: 10px; margin-top: 10px; font-size: 0.95em; display: none; /* Hidden by default */ } .error-message.show { display: block; } /* --- CodeMirror Styles --- */ .editor-container { flex-grow: 1; /* Allow editor to take up available space */ position: relative; overflow: hidden; /* Constrain CodeMirror within this container */ } .CodeMirror { border: 1px solid #bbb; border-radius: 6px; height: 100%; /* Fill the container */ font-size: 14px; } /* --- Animations --- */ @keyframes spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } } @media (max-width: 700px) { .container { flex-direction: column; } .sidebar { width: 100%; flex-direction: row; padding: 12px 0; } .main { padding: 18px 8px; } .modal-content { width: 90vw; height: 85vh; } } </style> </head> <body> <div class="container"> <aside class="sidebar"> <div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:8px;"> <h2 style="margin-bottom:0;">Providers</h2> <div> <button id="editProvidersBtn">Edit</button> </div> </div> <ul class="provider-list" id="providerList"> <li>Loading...</li> </ul> </aside> <main class="main"> <div class="tools-header" id="toolsHeader">Tools</div> <div class="tool-list" id="toolList"> <div>Loading...</div> </div> </main> </div> <div id="editProvidersModal" class="modal-overlay"> <div class="modal-content"> <div class="modal-header"> <h3>Edit providers.json</h3> <button id="saveProvidersBtn" class="modal-btn"> <span id="saveButtonText">Save</span> <span id="saveButtonSpinner" style="display:none;"> <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="animation:spin 1s linear infinite;"> <path d="M21 12a9 9 0 11-6.219-8.56" /> </svg> </span> </button> </div> <div class="editor-container"> <textarea id="providersJsonEditor">[]</textarea> </div> <div id="editProvidersError" class="error-message"></div> </div> </div> <script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.16/codemirror.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.16/mode/javascript/javascript.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.16/addon/lint/lint.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.16/addon/lint/json-lint.min.js"></script> <script src="https://unpkg.com/jsonlint@1.6.3/web/jsonlint.js"></script> <script> // --- State and DOM Elements --- let providers = []; let tools = []; let selectedProvider = null; let cmEditor = null; // To hold the CodeMirror instance const providerList = document.getElementById('providerList'); const toolList = document.getElementById('toolList'); const editProvidersBtn = document.getElementById('editProvidersBtn'); const editProvidersModal = document.getElementById('editProvidersModal'); const providersJsonEditorEl = document.getElementById('providersJsonEditor'); const editProvidersError = document.getElementById('editProvidersError'); const saveProvidersBtn = document.getElementById('saveProvidersBtn'); const saveButtonText = document.getElementById('saveButtonText'); const saveButtonSpinner = document.getElementById('saveButtonSpinner'); // --- API Functions --- async function fetchProviders() { const res = await fetch('/providers'); if (!res.ok) return []; const data = await res.json(); return data.providers || []; } async function fetchTools() { const res = await fetch('/tools'); if (!res.ok) return []; const data = await res.json(); return data.tools || []; } async function removeProvider(name) { const res = await fetch(`/providers/${encodeURIComponent(name)}`, { method: 'DELETE' }); if (!res.ok) { const data = await res.json().catch(() => ({})); alert(data.detail || 'Failed to remove provider'); return; } await refreshAll(); } // --- Rendering Functions --- function renderProviders() { providerList.innerHTML = ''; providers.forEach(provider => { const li = document.createElement('li'); const providerName = provider.name || provider; li.textContent = providerName; li.onclick = () => { selectedProvider = providerName; renderProviders(); renderTools(); }; if (providerName === selectedProvider) { li.classList.add('selected'); } const removeBtn = document.createElement('button'); removeBtn.textContent = '✕'; removeBtn.title = 'Remove provider'; removeBtn.style.cssText = 'margin-left:12px; background:#ff5555; color:#fff; border:none; border-radius:3px; cursor:pointer;'; removeBtn.onclick = async (e) => { e.stopPropagation(); if (!confirm(`Remove provider '${provider.name}'?`)) return; await removeProvider(provider.name); }; li.appendChild(removeBtn); providerList.appendChild(li); }); } function renderTools() { toolList.innerHTML = ''; let filtered = tools; if (selectedProvider) { filtered = tools.filter(tool => { if (!tool.name) return false; const toolProvider = tool.name.split('.')[0]; return toolProvider === selectedProvider; }); } if (filtered.length === 0) { toolList.innerHTML = '<div>No tools found for this provider.</div>'; return; } filtered.forEach(tool => { const card = document.createElement('div'); card.className = 'tool-card'; card.innerHTML = ` <h3 title="${tool.name}">${tool.name}</h3> <div class="desc">${tool.description || ''}</div> <div class="inputs"><b>Inputs:</b> ${tool.inputs ? Object.keys(tool.inputs).join(', ') : 'None'}</div> `; toolList.appendChild(card); }); } async function refreshAll() { providers = await fetchProviders(); tools = await fetchTools(); if (providers.length > 0 && !providers.some(p => (p.name || p) === selectedProvider)) { selectedProvider = providers[0].name || providers[0]; } renderProviders(); renderTools(); } // --- Edit Providers Modal Logic --- function setSaveButtonLoading(isLoading) { saveProvidersBtn.disabled = isLoading; saveButtonText.style.display = isLoading ? 'none' : 'inline'; saveButtonSpinner.style.display = isLoading ? 'inline-block' : 'none'; } function showEditorError(message) { editProvidersError.textContent = message; editProvidersError.classList.add('show'); } function clearEditorError() { editProvidersError.textContent = ''; editProvidersError.classList.remove('show'); } function closeModal() { editProvidersModal.style.display = 'none'; clearEditorError(); } function initializeCodeMirror() { if (cmEditor) return; // Already initialized cmEditor = CodeMirror.fromTextArea(providersJsonEditorEl, { mode: { name: "javascript", json: true }, theme: "dracula", lineNumbers: true, gutters: ["CodeMirror-lint-markers"], lint: true, autoCloseBrackets: true, matchBrackets: true, lineWrapping: true, // Wrap long lines }); } editProvidersBtn.onclick = async () => { try { const res = await fetch('/providers'); if (!res.ok) throw new Error(`HTTP ${res.status}: Failed to fetch providers`); const data = await res.json(); const providersJson = JSON.stringify(data.providers || [], null, 2); editProvidersModal.style.display = 'flex'; initializeCodeMirror(); cmEditor.setValue(providersJson); clearEditorError(); // Refresh to ensure proper layout after being displayed setTimeout(() => cmEditor.refresh(), 1); } catch (e) { alert('Failed to load providers: ' + e.message); } }; saveProvidersBtn.onclick = async () => { if (saveProvidersBtn.disabled) return; let newProviders; try { newProviders = JSON.parse(cmEditor.getValue()); if (!Array.isArray(newProviders)) throw new Error('Must be a JSON array'); } catch (e) { showEditorError('Invalid JSON: ' + e.message); return; } clearEditorError(); setSaveButtonLoading(true); try { const res = await fetch('/providers', { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(newProviders) }); if (!res.ok) { const data = await res.json().catch(() => ({})); showEditorError(data.detail || `HTTP ${res.status}: Failed to save providers.`); return; } const data = await res.json(); closeModal(); await refreshAll(); // Show feedback on the main edit button const originalText = 'Edit'; if (data.changed === false) { editProvidersBtn.textContent = 'No changes'; editProvidersBtn.style.background = '#6c757d'; } else { editProvidersBtn.textContent = 'Saved ✓'; editProvidersBtn.style.background = '#28a745'; } setTimeout(() => { editProvidersBtn.textContent = originalText; editProvidersBtn.style.background = '#394867'; }, 2000); } catch (e) { showEditorError('Network error: ' + e.message); } finally { setSaveButtonLoading(false); } }; // --- Initialization and Event Listeners --- // Close modal if user clicks outside of it editProvidersModal.onclick = (e) => { if (e.target === editProvidersModal && !saveProvidersBtn.disabled) { closeModal(); } }; document.addEventListener('keydown', (e) => { if (editProvidersModal.style.display === 'flex') { if (e.key === 'Escape' && !saveProvidersBtn.disabled) { closeModal(); } else if (e.key === 's' && (e.ctrlKey || e.metaKey)) { e.preventDefault(); // Prevent browser's save dialog if (!saveProvidersBtn.disabled) saveProvidersBtn.click(); } } }); window.onload = refreshAll; </script> </body> </html>

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/universal-tool-calling-protocol/utcp-mcp'

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