Skip to main content
Glama
scenario.yaml9.83 kB
# This test verifies that the MCPX UI can add a new server # It starts MCPX without configuration, verifies that no servers are connected, # opens the "Add Server" dialog, adds a server, checks that the server appears in the list. # It performs call MCP server and verifies that UI reflects call status. name: "Add Server Test" image: us-central1-docker.pkg.dev/prj-common-442813/mcpx/mcpx:v0.2.17-66354a7 env: {} dependentContainers: [] configMount: config cleanConfigMount: true # since mcp.json being produced by the test verboseOutput: false disableTest: true steps: - name: Load Control-Plane UI without config kind: browser toolName: browser_navigate payload: url: http://localhost:5173 expected: mode: regex value: "Ran Playwright code" - name: Switch to MCP Servers Tab kind: browser toolName: browser_evaluate payload: function: | () => { // Pick the visible header tablist (it has ≥4 tabs) const header = Array.from(document.querySelectorAll('[role="tablist"]')) .find(tl => tl.querySelectorAll('[role="tab"]').length >= 4 && tl.getBoundingClientRect().width > 0 ); const mcpTab = header?.querySelectorAll('[role="tab"]') && Array.from(header.querySelectorAll('[role="tab"]')) .find(t => t.textContent.trim() === 'MCP Servers'); if (!mcpTab) return 'tab-not-found'; ['pointerdown','mousedown','pointerup','mouseup','click'] .forEach(ev => mcpTab.dispatchEvent(new MouseEvent(ev,{bubbles:true,cancelable:true}))); return 'clicked'; } expected: mode: regex value: "clicked" - name: Wait for “No servers connected” message kind: browser toolName: browser_wait_for payload: text: No servers connected time: 7 expected: mode: contains value: "No servers connected" - name: Open “Add Server” dialog kind: browser toolName: browser_evaluate payload: function: | () => { const btn = [...document.querySelectorAll('button')] .find(b => b.textContent.trim() === 'Add Server'); if (!btn) return 'open-btn-not-found'; btn.click(); return 'opened'; } expected: mode: regex value: "opened" - name: Wait for “Add MCP Server” dialog to appear kind: browser toolName: browser_wait_for payload: text: Add MCP Server time: 7 expected: mode: contains value: "Waited for Add MCP Server" - name: Switch to Form tab, fill fields, then submit via form.requestSubmit() kind: browser toolName: browser_evaluate verboseOutput: false payload: function: | async () => { const sleep = (ms) => new Promise(r => setTimeout(r, ms)); const dlg = document.querySelector('[role="dialog"]'); if (!dlg) return 'no-dialog'; // 1) Go to the Form tab const tabs = [...dlg.querySelectorAll('[role="tab"]')]; const formTab = tabs.find(t => (t.textContent || '').trim().toLowerCase() === 'form'); if (!formTab) return 'no-form-tab'; formTab.focus(); formTab.dispatchEvent(new MouseEvent('mousedown', { bubbles: true })); formTab.click(); formTab.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', bubbles: true })); formTab.dispatchEvent(new KeyboardEvent('keyup', { key: 'Enter', bubbles: true })); // Wait for the form fields to appear for (let i = 0; i < 20; i++) { await sleep(100); if (dlg.querySelector('input[name="name"]')) break; if (i === 19) return 'form-panel-not-mounted'; } // 2) Fill RHF-registered inputs const fill = (sel, val) => { const el = dlg.querySelector(sel); if (!el) throw new Error('missing ' + sel); el.focus(); el.value = val; el.dispatchEvent(new Event('input', { bubbles: true })); el.dispatchEvent(new Event('change', { bubbles: true })); }; try { fill('input[name="name"]', 'time'); fill('input[name="command"]', 'docker'); fill('input[name="args"]', 'run -i --rm -e LOCAL_TIMEZONE mcp/time'); fill('textarea[name="env"]', '{"LOCAL_TIMEZONE":"UTC"}'); } catch { return 'fill-error'; } await sleep(150); // 3) Submit via the real <form> handler const form = dlg.querySelector('form'); if (!form) return 'form-not-found'; try { form.requestSubmit(); } catch { form.dispatchEvent(new Event('submit', { bubbles: true, cancelable: true })); } // 4) Wait for the dialog to close for (let i = 0; i < 20; i++) { await sleep(150); if (!document.querySelector('[role="dialog"]')) return 'done'; } return 'submitted-but-dialog-open'; } expected: mode: regex value: "done" - name: Wait for “Add MCP Server” dialog to close kind: browser toolName: browser_evaluate payload: function: | () => { const timeoutMs = 20000; const intervalMs = 200; const start = Date.now(); return new Promise((resolve, reject) => { (function check() { // if no dialog is found, we’re done if (!document.querySelector('[role="dialog"]')) { return resolve('dialog-closed'); } // if we’ve waited too long, error out if (Date.now() - start > timeoutMs) { return reject('dialog-still-open'); } // otherwise try again shortly setTimeout(check, intervalMs); })(); }); } expected: mode: regex value: "dialog-closed" - name: "Time MCP Server Call" kind: backend toolName: time__get_current_time payload: timezone: "UTC" expected: mode: regex value: "\"timezone\"\\s*:\\s*\"UTC\"" - name: "Load Control-Plane UI" kind: browser toolName: browser_navigate payload: url: "http://localhost:5173" expected: mode: contains value: "Page URL: http://localhost:5173/" - name: "Wait for Total Requests Label" kind: browser toolName: browser_wait_for payload: text: "Total Requests" time: 7 expected: mode: contains value: "Waited for Total Requests" - name: "Verify Total Requests Count" kind: browser toolName: browser_evaluate payload: function: | () => new Promise((resolve) => { const start = Date.now(); const poll = () => { const text = document.body?.innerText || ''; const match = text.match(/Total Requests\s*(\d+)/i); const value = match ? Number(match[1]) : 0; if (value > 0 || Date.now() - start > 15000) { resolve(value); } else { setTimeout(poll, 500); } }; poll(); }) expected: mode: regex value: "\"?[1-9]\\d*\"?" # accepts any integer ≥1 with or without quotes - name: "Wait for Last Activity Timestamp" kind: browser toolName: browser_wait_for payload: text: "Just now" time: 10 expected: mode: contains value: "Waited for Just now" - name: "Verify Last Activity" kind: browser toolName: browser_evaluate payload: function: | () => document.body.innerText.includes('Last Activity') && document.body.innerText.includes('Just now') expected: mode: regex value: "\"?true\"?" # --- now go to the Tools tab --- - name: Switch to Tools Tab kind: browser toolName: browser_evaluate payload: function: | () => { // Find the *visible* header tab list (it has ≥4 tabs) const header = Array.from(document.querySelectorAll('[role="tablist"]')) .find(tl => tl.querySelectorAll('[role="tab"]').length >= 4 && tl.getBoundingClientRect().width > 0 ); if (!header) return 'no-header'; // Find the “Tools” tab const toolsTab = Array.from(header.querySelectorAll('[role="tab"]')) .find(el => el.textContent?.trim() === 'Tools'); if (!toolsTab) return 'tools-tab-not-found'; // Dispatch the full sequence of events so React will pick it up ['pointerdown','mousedown','pointerup','mouseup','click'] .forEach(type => toolsTab.dispatchEvent(new MouseEvent(type, { bubbles: true, cancelable: true })) ); return 'clicked'; } expected: mode: regex value: "clicked" - name: Wait for “get_current_time” kind: browser toolName: browser_wait_for payload: text: get_current_time # appears only after Tools tab is active time: 7 expected: mode: contains value: Waited for get_current_time - name: "Verify Tools Available Count" kind: browser toolName: browser_evaluate payload: function: | () => { const el = [...document.querySelectorAll('*')] .find(n => n.textContent?.includes('Tools Available')); const m = el?.textContent?.match(/\d+/); return m ? Number(m[0]) : 0; } expected: mode: regex value: "\"?[1-9]\\d*\"?"

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/TheLunarCompany/lunar'

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