UNPKG

json-object-editor

Version:

JOE the Json Object Editor | Platform Edition

259 lines (237 loc) 9.96 kB
<!doctype html> <html> <head> <meta charset="utf-8"> <title>JOE MCP Test</title> <meta name="viewport" content="width=device-width, initial-scale=1"> <style> body{font-family:system-ui,-apple-system,Segoe UI,Roboto,Ubuntu,Cantarell,Noto Sans,sans-serif;margin:20px;} label{display:block;margin:8px 0 4px} select, input, textarea, button{font-size:14px} textarea{width:100%;height:160px;font-family:ui-monospace,Menlo,Consolas,monospace} pre{background:#f6f8fa;border:1px solid #e1e4e8;padding:10px;overflow:auto} .row{display:flex;gap:12px;align-items:center;flex-wrap:wrap} .small{font-size:12px;color:#666} .bad{color:#b00020} .good{color:#0a7d00} .preset{margin:8px 0;} </style> </head> <body> <div id="mcp-nav"></div> <script src="/JsonObjectEditor/_www/mcp-nav.js"></script> <h1>JOE MCP Test</h1> <div class="small">Use this page to discover tools and call the JSON-RPC endpoint.</div> <h3>Manifest</h3> <div class="row"> <label for="base">Base URL</label> <input id="base" value="" placeholder="http://localhost:{{PORT}}" style="min-width:280px"/> <button id="loadManifest">Load manifest</button> <span id="status" class="small"></span> </div> <label for="tool">Tools</label> <select id="tool"></select> <pre id="toolInfo"></pre> <h3>Presets</h3> <div class="preset"> <button id="presetFuzzyUser">fuzzySearch users: q="corey hadden"</button> <button id="presetFuzzyHouse">fuzzySearch houses: q="backyard"</button> <button id="presetListApps">listApps: all app definitions</button> <button id="presetGetSchemas">getSchemas: ["client","user"]</button> <button id="presetGetSchemasSummary">getSchemas (summaryOnly): ["task","project"]</button> <button id="presetSearchSlimRecent">search clients (slim, recent)</button> <button id="presetSearchWithCount">search clients (withCount)</button> <button id="presetSearchCountOnly">search clients (countOnly)</button> <button id="presetSaveObjects">saveObjects: batch save (example)</button> <button id="presetUnderstandObject">understandObject: by _id</button> </div> <h3>Call JSON-RPC</h3> <label for="params">Params (JSON)</label> <textarea id="params">{}</textarea> <div class="row"> <button id="call">POST /mcp</button> <span id="callStatus" class="small"></span> </div> <pre id="result"></pre> <script> (function(){ const $ = (id)=>document.getElementById(id); const base = $('base'); const loadBtn = $('loadManifest'); const status = $('status'); const toolSel = $('tool'); const toolInfo = $('toolInfo'); const params = $('params'); const callBtn = $('call'); const callStatus = $('callStatus'); const result = $('result'); const presetFuzzyUser = $('presetFuzzyUser'); const presetFuzzyHouse = $('presetFuzzyHouse'); const presetListApps = $('presetListApps'); const presetGetSchemas = $('presetGetSchemas'); const presetGetSchemasSummary = $('presetGetSchemasSummary'); const presetSearchSlimRecent = $('presetSearchSlimRecent'); const presetSearchWithCount = $('presetSearchWithCount'); const presetSearchCountOnly = $('presetSearchCountOnly'); const presetSaveObjects = $('presetSaveObjects'); const presetUnderstandObject = $('presetUnderstandObject'); // Try to infer base from window location base.value = base.value || (location.origin); let manifest = null; let idCounter = 1; function ensureTool(name, meta){ if (!manifest) { manifest = { tools: [] }; } manifest.tools = manifest.tools || []; var existing = (manifest.tools||[]).find(function(t){ return t && t.name === name; }); if (!existing && meta) { manifest.tools.push(meta); } // ensure option exists in select var hasOpt = false; for (var i=0;i<toolSel.options.length;i++){ if (toolSel.options[i].value === name) { hasOpt = true; break; } } if (!hasOpt){ var opt=document.createElement('option'); opt.value=name; opt.textContent=name; toolSel.appendChild(opt); } } function setStatus(el, msg, ok){ el.textContent = msg || ''; el.className = 'small ' + (ok===true?'good': ok===false?'bad':''); } async function fetchJSON(url, opts){ const res = await fetch(url, opts); const ct = res.headers.get('content-type')||''; const isJSON = ct.includes('application/json'); if(!res.ok){ let detail = isJSON ? await res.json().catch(()=>({})) : await res.text(); throw new Error('HTTP '+res.status+': '+(isJSON?JSON.stringify(detail):detail)); } return isJSON ? res.json() : res.text(); } loadBtn.onclick = async function(){ setStatus(status, 'Loading...', null); toolSel.innerHTML = ''; toolInfo.textContent=''; try{ const url = base.value.replace(/\/$/,'') + '/.well-known/mcp/manifest.json'; manifest = await fetchJSON(url); // Instance info handled by shared nav script (manifest.tools||[]).forEach(t=>{ const opt=document.createElement('option'); opt.value=t.name; opt.textContent=t.name; toolSel.appendChild(opt); }); if((manifest.tools||[]).length){ toolSel.selectedIndex=0; renderToolInfo(); } setStatus(status, 'Manifest loaded', true); }catch(e){ setStatus(status, e.message||String(e), false); } }; function renderToolInfo(){ const name = toolSel.value; const tool = (manifest.tools||[]).find(t=>t.name===name); toolInfo.textContent = tool ? JSON.stringify(tool, null, 2) : ''; // Prefill common params for convenience if(tool && tool.params){ params.value = JSON.stringify(Object.fromEntries(Object.keys(tool.params.properties||{}).map(k=>[k, null])), null, 2); } // Hydrate presets UI if (name === 'hydrate') { params.value = JSON.stringify({}, null, 2); } } toolSel.onchange = renderToolInfo; presetFuzzyUser.onclick = function(){ toolSel.value = 'fuzzySearch'; renderToolInfo(); params.value = JSON.stringify({ q: 'corey hadden', filters: { itemtype: 'user' }, threshold: 0.5 }, null, 2); }; presetFuzzyHouse.onclick = function(){ toolSel.value = 'fuzzySearch'; renderToolInfo(); params.value = JSON.stringify({ q: 'backyard', filters: { itemtype: 'house' }, threshold: 0.5 }, null, 2); }; presetListApps.onclick = function(){ toolSel.value = 'listApps'; renderToolInfo(); params.value = JSON.stringify({}, null, 2); }; presetGetSchemas.onclick = function(){ ensureTool('getSchemas', { name: 'getSchemas', description: 'Retrieve multiple schema definitions. If omitted, returns all.', params: { type: 'object', properties: { names: { type: 'array', items: { type: 'string' } } } }, returns: { type: 'object' } }); toolSel.value = 'getSchemas'; renderToolInfo(); params.value = JSON.stringify({ names: ["client", "user"] }, null, 2); }; presetGetSchemasSummary.onclick = function(){ ensureTool('getSchemas', { name: 'getSchemas', description: 'Retrieve multiple schemas. With summaryOnly=true, returns summaries; if names omitted, returns all.', params: { type: 'object', properties: { names: { type: 'array', items: { type: 'string' } }, summaryOnly: { type: 'boolean' } } }, returns: { type: 'object' } }); toolSel.value = 'getSchemas'; renderToolInfo(); params.value = JSON.stringify({ names: ["task", "project"], summaryOnly: true }, null, 2); }; presetSearchSlimRecent.onclick = function(){ toolSel.value = 'search'; renderToolInfo(); params.value = JSON.stringify({ itemtype: 'client', source: 'cache', query: { itemtype: 'client' }, limit: 25, sortBy: 'joeUpdated', sortDir: 'desc', slim: true }, null, 2); }; presetSearchWithCount.onclick = function(){ toolSel.value = 'search'; renderToolInfo(); params.value = JSON.stringify({ itemtype: 'client', source: 'cache', query: { itemtype: 'client' }, limit: 25, withCount: true, sortBy: 'joeUpdated', sortDir: 'desc' }, null, 2); }; presetSearchCountOnly.onclick = function(){ toolSel.value = 'search'; renderToolInfo(); params.value = JSON.stringify({ itemtype: 'client', source: 'cache', query: { itemtype: 'client' }, countOnly: true }, null, 2); }; presetSaveObjects.onclick = function(){ toolSel.value = 'saveObjects'; renderToolInfo(); // Example: two minimal objects; adjust itemtype/fields as needed params.value = JSON.stringify({ objects: [ { itemtype: "client", name: "Batch Client A" }, { itemtype: "client", name: "Batch Client B" } ], stopOnError: false, concurrency: 5 }, null, 2); }; presetUnderstandObject.onclick = function(){ toolSel.value = 'understandObject'; renderToolInfo(); // Provide a template; user should replace _id with a real object id. params.value = JSON.stringify({ _id: "REPLACE_WITH_OBJECT_ID", depth: 2 }, null, 2); }; callBtn.onclick = async function(){ setStatus(callStatus, 'Calling...', null); result.textContent=''; try{ const url = base.value.replace(/\/$/,'') + '/mcp'; let p = {}; try{ p = params.value ? JSON.parse(params.value) : {}; }catch(e){ throw new Error('Invalid JSON in params'); } const body = { jsonrpc: '2.0', id: String(idCounter++), method: toolSel.value, params: p }; const resp = await fetchJSON(url, { method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify(body)}); result.textContent = JSON.stringify(resp, null, 2); setStatus(callStatus, 'OK', true); }catch(e){ setStatus(callStatus, e.message||String(e), false); } }; // Auto-load manifest on open setTimeout(()=>loadBtn.click(), 50); })(); </script> </body> </html>