Skip to main content
Glama
scenario.yaml16.6 kB
# Adds an MCP server from the Examples tab (Time via uvx) and verifies it renders name: Add MCP server from Examples (Time via uvx) image: us-central1-docker.pkg.dev/prj-common-442813/mcpx/mcpx:v0.2.17-66354a7 env: {} dependentContainers: [] configMount: . cleanConfigMount: true verboseOutput: false steps: - name: Load Control-Plane UI kind: browser toolName: browser_navigate payload: url: http://localhost:5173 expected: mode: regex value: "Ran Playwright code" - name: Wait for Add Server button kind: browser toolName: browser_wait_for payload: text: "Add Server" time: 12 expected: mode: contains value: "Waited for Add Server" - name: Click “Add Server” button kind: browser toolName: browser_evaluate payload: function: | () => { const btn = Array.from(document.querySelectorAll('button')) .find(b => /(^|\s)Add Server(\s|$)/i.test(b.textContent || '') && b.offsetParent !== null); btn?.click(); return btn ? 'add-server-clicked' : 'add-server-not-found'; } expected: mode: contains value: "add-server-clicked" - name: Wait for Add MCP Server dialog kind: browser toolName: browser_wait_for payload: text: "Add MCP Server" time: 10 expected: mode: contains value: "Waited for Add MCP Server" - name: Switch to “Examples” tab kind: browser toolName: browser_evaluate payload: function: | () => { const dlg = document.querySelector('[role="dialog"]'); if (!dlg) return 'no-dialog'; const tab = Array.from(dlg.querySelectorAll('[role="tab"],button')) .find(el => /examples/i.test(el.textContent || '')); if (!tab) return 'examples-tab-not-found'; ['pointerdown','mousedown','pointerup','mouseup','click'].forEach(t => tab.dispatchEvent(new MouseEvent(t, { bubbles: true, cancelable: true })) ); return 'examples-tab-open'; } expected: mode: contains value: "examples-tab-open" - name: Select “Time (stdio-uvx)” in examples (robust) kind: browser toolName: browser_evaluate payload: function: | () => new Promise((resolve) => { const dlg = document.querySelector('[role="dialog"]'); if (!dlg) return resolve('no-dialog'); const combo = dlg.querySelector('[role="combobox"],[aria-haspopup="listbox"]') || Array.from(dlg.querySelectorAll('button,[role="button"]')) .find(b => /Select Server Type/i.test(b.textContent || '')); if (!combo) return resolve('combobox-not-found'); ['pointerdown','mousedown','pointerup','mouseup','click'].forEach(t => combo.dispatchEvent(new MouseEvent(t, { bubbles: true, cancelable: true })) ); const deadline = Date.now() + 5000; (function pick() { const list = document.querySelector('[role="listbox"]'); if (list && list.getBoundingClientRect().height > 0) { const target = Array.from(list.querySelectorAll('[role="option"],[data-radix-collection-item],li,div')) .find(el => /^\s*Time\s*\(stdio-uvx\)\s*$/i.test(el.textContent || '')); if (!target) { if (Date.now() > deadline) return resolve('time-option-not-found'); return setTimeout(pick, 100); } target.scrollIntoView({ block: 'center' }); ['pointerdown','mousedown','pointerup','mouseup','click'].forEach(t => target.dispatchEvent(new MouseEvent(t, { bubbles: true, cancelable: true })) ); target.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', bubbles: true })); target.dispatchEvent(new KeyboardEvent('keyup', { key: 'Enter', bubbles: true })); const doneBy = Date.now() + 2000; (function settle() { const stillOpen = document.querySelector('[role="listbox"]'); const label = (combo.textContent || '').trim(); if (!stillOpen && /Time\s*\(stdio-uvx\)/i.test(label)) { return resolve('time-option-picked'); } if (Date.now() > doneBy) return resolve('time-option-clicked'); setTimeout(settle, 80); })(); return; } if (Date.now() > deadline) return resolve('listbox-not-open'); setTimeout(pick, 80); })(); }) expected: mode: regex value: "time-option-picked|time-option-clicked" - name: Confirm examples preview is Time kind: browser toolName: browser_evaluate payload: function: | () => { const dlg = document.querySelector('[role="dialog"]'); if (!dlg) return 'no-dialog'; const previewText = Array.from( dlg.querySelectorAll('section,div,pre,code,textarea,[data-preview]') ).map(n => n.textContent || '').join('\n'); const ok = /time\s*\(stdio-uvx\)/i.test(previewText) || /uvx/i.test(previewText) || /mcp-server-time/i.test(previewText); return ok ? 'time-preview-ok' : 'time-preview-missing'; } expected: mode: contains value: "time-preview-ok" - name: Click “Use This Example” kind: browser toolName: browser_evaluate payload: function: | () => { const dlg = document.querySelector('[role="dialog"]'); if (!dlg) return 'no-dialog'; const btn = Array.from(dlg.querySelectorAll('button')) .find(b => /Use This Example/i.test(b.textContent || '')); if (!btn) return 'use-example-not-found'; ['pointerdown','mousedown','pointerup','mouseup','click'].forEach((type) => btn.dispatchEvent(new MouseEvent(type, { bubbles: true, cancelable: true })) ); return 'used-example'; } expected: mode: contains value: "used-example" - name: Switch back to JSON tab kind: browser toolName: browser_evaluate payload: function: | () => { const dlg = document.querySelector('[role="dialog"]'); if (!dlg) return 'no-dialog'; const tab = Array.from(dlg.querySelectorAll('[role="tab"],button')) .find(el => /json\s*config/i.test(el.textContent || '')); if (!tab) return 'json-tab-not-found'; ['pointerdown','mousedown','pointerup','mouseup','click'].forEach((type) => tab.dispatchEvent(new MouseEvent(type, { bubbles: true, cancelable: true })) ); return 'json-tab-open'; } expected: mode: contains value: "json-tab-open" - name: Verify editor has time JSON kind: browser toolName: browser_evaluate payload: function: | () => new Promise((resolve) => { const deadline = Date.now() + 15000; const readBuffer = (dlg) => { const M = window.monaco; let buf = ''; let modelsSnapshot = []; if (M?.editor?.getModels) { const models = M.editor.getModels(); modelsSnapshot = models.map((model) => model.getValue()); if (models?.length) { buf = models.reduce( (acc, model) => (model.getValue().length > acc.length ? model.getValue() : acc), '' ); } } if (!buf) { const ta = dlg.querySelector( 'textarea[aria-label="Editor content"], textarea' ); buf = (ta && ta.value) || ''; } return { buf, modelsSnapshot }; }; const poll = () => { const dlg = document.querySelector('[role="dialog"]'); if (!dlg) { return resolve('no-dialog'); } const { buf, modelsSnapshot } = readBuffer(dlg); const text = dlg.innerText || ''; const ok = (/"time"\s*:/.test(buf) || /"time"\s*:/.test(text)) && (/(uvx|mcp-server-time)/i.test(buf) || /(uvx|mcp-server-time)/i.test(text)); if (ok) { return resolve('editor-time-ok'); } if (Date.now() > deadline) { const sample = buf.slice(0, 160); const textSample = text.slice(0, 160); return resolve( `editor-not-time:${sample}|text:${textSample}|models:${modelsSnapshot .map((m) => m.slice(0, 80)) .join('||')}` ); } setTimeout(poll, 250); }; poll(); }) expected: mode: contains value: "editor-time-ok" - name: Wait until Add Server is enabled kind: browser toolName: browser_evaluate payload: function: | () => new Promise((resolve, reject) => { const timeout = Date.now() + 15000; (function tick() { const dlg = document.querySelector('[role="dialog"]'); const btn = dlg && Array.from(dlg.querySelectorAll('button')) .find(b => /Add Server/i.test(b.textContent || '')); if (btn && !btn.disabled) return resolve('add-server-enabled'); if (Date.now() > timeout) return reject('add-server-still-disabled'); setTimeout(tick, 200); })(); }) expected: mode: contains value: "add-server-enabled" - name: Submit Add Server kind: browser toolName: browser_evaluate payload: function: | () => { const dlg = document.querySelector('[role="dialog"]'); if (!dlg) return 'no-dialog'; const form = dlg.querySelector('form'); if (form) { try { form.requestSubmit(); return 'form-submitted'; } catch {} } const btn = Array.from(dlg.querySelectorAll('button')) .reverse() .find(b => /Add Server/i.test(b.textContent || '') && !b.disabled); if (btn) { ['pointerdown','mousedown','pointerup','mouseup','click'].forEach(t => btn.dispatchEvent(new MouseEvent(t, { bubbles: true, cancelable: true })) ); return 'modal-submit-clicked'; } return 'add-server-button-not-found-or-disabled'; } expected: mode: regex value: "form-submitted|modal-submit-clicked" - name: Wait for “Add MCP Server” dialog to close kind: browser toolName: browser_evaluate payload: function: | () => new Promise((resolve, reject) => { const deadline = Date.now() + 22000; (function poll() { if (!document.querySelector('[role="dialog"]')) return resolve('dialog-closed'); if (Date.now() > deadline) return reject('dialog-still-open'); setTimeout(poll, 200); })(); }) expected: mode: regex value: "dialog-closed" - name: Soft refresh Control-Plane UI kind: browser toolName: browser_navigate payload: url: http://localhost:5173 expected: mode: regex value: "Ran Playwright code" - name: Wait for UI chrome kind: browser toolName: browser_wait_for payload: text: "Add Server" time: 12 expected: mode: contains value: "Waited for Add Server" - name: Wait for time node to render kind: browser toolName: browser_evaluate payload: function: | () => new Promise((resolve, reject) => { const deadline = Date.now() + 30000; // 30s (function poll() { const root = document.querySelector('main') || document.body; // Any heading containing "time" (handles "time PENDING", etc.) const byHeading = Array .from(root.querySelectorAll('h1,h2,h3,h4,[role="heading"]')) .find(h => /\btime\b/i.test((h.textContent || '').replace(/\s+/g, ' ').trim())); // React Flow node variants const byNode = Array .from(root.querySelectorAll('.react-flow__node,[data-id],[data-nodeid]')) .find(n => /\btime\b/i.test(n.textContent || '')); // Generic card fallback (handles "2 Tools" card layout) const byCard = Array .from(root.querySelectorAll('div,article,section')) .find(el => /\btime\b/i.test(el.textContent || '') && /\bTools?\b/i.test(el.textContent || '')); if (byHeading || byNode || byCard) return resolve('time-tile-ready'); if (Date.now() > deadline) return reject('time-tile-timeout'); setTimeout(poll, 200); })(); }) expected: mode: contains value: "time-tile-ready" - name: Open time server drawer kind: browser toolName: browser_evaluate payload: function: | () => { const root = document.querySelector('main') || document.body; const header = Array.from(root.querySelectorAll('h3,[role="heading"]')) .find(h => (h.textContent || '').trim().toLowerCase() === 'time'); let target = header?.closest('.react-flow__node,[data-id],[data-nodeid],button,[role="button"]') || header; if (!target) { target = Array.from(root.querySelectorAll('.react-flow__node,[data-id]')) .find(n => /time/i.test(n.textContent || '')); } if (!target) return 'time-tile-not-found'; ['pointerdown','mousedown','pointerup','mouseup','click'].forEach(type => target.dispatchEvent(new MouseEvent(type, { bubbles: true, cancelable: true })) ); return 'clicked'; } expected: mode: regex value: "clicked" - name: Wait for get_current_time tool chip kind: browser toolName: browser_evaluate payload: function: | () => new Promise((resolve, reject) => { const deadline = Date.now() + 15000; (function poll() { const root = document.querySelector('[role="dialog"]') || document.body; const texts = Array.from( root.querySelectorAll('div,span,button,[role="button"],[data-nodeid],.react-flow__node'), ).map((el) => (el.textContent || '').toLowerCase()); const found = texts.some((text) => { const normalized = text.replace(/\s+/g, ' ').trim(); const canonical = normalized.replace(/[\s_-]+/g, ''); return canonical.includes('getcurrenttime'); }); if (found) return resolve('get-current-time-found'); if (Date.now() > deadline) return reject('get-current-time-timeout'); setTimeout(poll, 200); })(); }) expected: mode: contains value: "get-current-time-found" - name: Wait for convert_time tool chip kind: browser toolName: browser_evaluate payload: function: | () => new Promise((resolve, reject) => { const deadline = Date.now() + 15000; (function poll() { const root = document.querySelector('[role="dialog"]') || document.body; const texts = Array.from( root.querySelectorAll('div,span,button,[role="button"],[data-nodeid],.react-flow__node'), ).map((el) => (el.textContent || '').toLowerCase()); const found = texts.some((text) => { const normalized = text.replace(/\s+/g, ' ').trim(); const canonical = normalized.replace(/[\s_-]+/g, ''); return canonical.includes('converttime'); }); if (found) return resolve('convert-time-found'); if (Date.now() > deadline) return reject('convert-time-timeout'); setTimeout(poll, 200); })(); }) expected: mode: contains value: "convert-time-found" - name: Wait for Tools (2) indicator kind: browser toolName: browser_wait_for payload: text: "Tools (2)" time: 8 expected: mode: contains value: "Waited for Tools (2)"

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