json-object-editor
Version:
JOE the Json Object Editor | Platform Edition
259 lines (237 loc) • 9.96 kB
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>