json-object-editor 0.10.653 → 0.10.657

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,3 +1,49 @@
1
+ ## 0.10.657
2
+
3
+ - MCP as an optional layer on all AI surfaces
4
+ - `ai_prompt`: added MCP config block (`mcp_enabled`, `mcp_toolset`, `mcp_selected_tools`, `mcp_instructions_mode`) so prompts can opt into MCP tools with named toolsets (`read-only`, `minimal`, `all`, `custom`) and auto‑generated tool instructions.
5
+ - Autofill: field‑level `ai` configs now accept the same MCP keys, enabling per‑field MCP tooling for `_joe.Ai.populateField` runs.
6
+ - `ai_response`: now records MCP config and `mcp_tools_used[]` for every run, plus `used_openai_file_ids[]` for attached OpenAI Files.
7
+ - Uploader file roles + AI‑aware attachments
8
+ - Uploader fields can declare `file_roles` (array of `{ value, label?, default? }`); the UI renders a per‑file role `<select>` and persists the choice to `file_role` on each file object.
9
+ - `executeJOEAiPrompt` now injects an `uploaded_files[]` header into the user input, listing `{ itemtype, field, name, role, openai_file_id }` for each attached file so prompts can distinguish “transcript” vs “summary” sources.
10
+ - `runWithTools` now applies `attachFilesToResponsesPayload` to both the initial tool‑planning call and the final Responses call (including the token‑limit retry), so MCP runs consistently see file attachments.
11
+ - Storage / history robustness
12
+ - Hardened `JOE.Storage.save` history diffing by sanitizing cached vs current documents before `$c.changes(...)` to avoid a `craydent-object.equals` edge case where comparing `null`/`undefined` could throw on `.toString()`. This only affects `_history.changes`, not saved documents.
13
+
14
+ ## 0.10.654
15
+
16
+ - Uploader + OpenAI Files integration
17
+ - After successful S3 upload, the server now (optionally) uploads the same bytes to OpenAI Files when `OPENAI_API_KEY` is set. Purpose is hard‑coded to `assistants`.
18
+ - Added `openai_file_id`, `openai_purpose`, and `etag` to the upload response from `/API/plugin/awsConnect`.
19
+ - Client persists OpenAI metadata on each file object and shows the OpenAI file id directly under the filename in the uploader preview.
20
+ - Per‑file “Upload to OpenAI” / “Retry OpenAI” action added (visible when no id or when status=error). This calls `POST /API/plugin/chatgpt/filesRetryFromUrl` and updates the file metadata, then quick‑saves.
21
+ - Removed the previous “OpenAI: OK” status chip to reduce noise; the file id now communicates success.
22
+
23
+ - Prompts and Responses (OpenAI)
24
+ - `ai_prompt`: new `attachments_mode` (`direct` | `file_search`) controls how selected files are used.
25
+ - `direct`: sends files via Responses content parts (`input_file` with `file_id`).
26
+ - `file_search`: creates a vector store on the fly and attaches it to `file_search` tool resources.
27
+ - Automatic safety retry: if a model rejects `temperature/top_p`, we strip and retry once.
28
+ - Select Prompt: prompt discovery now matches by `datasets[]` OR `content_items[].itemtype` (status must be active).
29
+ - `ai_response`: added read‑only display for `used_openai_file_ids[]`; fixed renderer.
30
+ - Bugfix: `referenced_objects` now populates for Select Prompt runs (accepts `data.params` shape).
31
+
32
+ - UX
33
+ - “Run AI Prompt” and “Run Thought Agent” buttons now disable and show a subtle pulse while running to prevent double‑clicks.
34
+
35
+ - New ChatGPT plugin helpers
36
+ - `filesUploadFromBufferHelper({ buffer, filename, contentType, purpose })` – internal helper used by the S3 plugin.
37
+ - `filesRetryFromUrl({ url, filename?, contentType? })` – public endpoint to (re)upload an existing S3 file to OpenAI (returns `openai_file_id`).
38
+ - Fixed ESM import error by dynamically importing `got` inside the plugin.
39
+
40
+ - Uploader polish
41
+ - “Upload File” button is now shown automatically whenever pending files exist (e.g., base64 present, no URL yet).
42
+ - UI surfaces verbose OpenAI errors inline on failure.
43
+
44
+ - Docs updated
45
+ - README’s File Uploads section expanded to cover OpenAI Files flow, retry endpoint, and how to use file ids.
46
+
1
47
  ## 0.10.653 - JSON field added
2
48
  - Added `json` field type for editing/storing JSON subobjects in schemas (object-only, default `{}`).
3
49
  - Uses the existing Ace-based code editor in JSON mode with blur/save pretty-printing for consistent formatting.
@@ -996,4 +1042,4 @@ icons added, bug fixes.
996
1042
  *v0.4.1
997
1043
  files directory added. defaults to /_files/_
998
1044
  *V0.4.0
999
- now ready to be loaded as a node require.
1045
+ now ready to be loaded as a node require.
@@ -1,276 +1,287 @@
1
- <!doctype html>
2
- <html>
3
- <head>
4
- <meta charset="utf-8">
5
- <title>JOE MCP Test</title>
6
- <meta name="viewport" content="width=device-width, initial-scale=1">
7
- <style>
8
- body{font-family:system-ui,-apple-system,Segoe UI,Roboto,Ubuntu,Cantarell,Noto Sans,sans-serif;margin:20px;}
9
- label{display:block;margin:8px 0 4px}
10
- select, input, textarea, button{font-size:14px}
11
- textarea{width:100%;height:160px;font-family:ui-monospace,Menlo,Consolas,monospace}
12
- pre{background:#f6f8fa;border:1px solid #e1e4e8;padding:10px;overflow:auto}
13
- .row{display:flex;gap:12px;align-items:center;flex-wrap:wrap}
14
- .small{font-size:12px;color:#666}
15
- .bad{color:#b00020}
16
- .good{color:#0a7d00}
17
- .preset{margin:8px 0;}
18
- </style>
19
- </head>
20
- <body>
21
- <div id="mcp-nav"></div>
22
- <script src="/JsonObjectEditor/_www/mcp-nav.js"></script>
23
- <h1>JOE MCP Test</h1>
24
- <div class="small">Use this page to discover tools and call the JSON-RPC endpoint.</div>
25
-
26
- <h3>Manifest</h3>
27
- <div class="row">
28
- <label for="base">Base URL</label>
29
- <input id="base" value="" placeholder="http://localhost:{{PORT}}" style="min-width:280px"/>
30
- <button id="loadManifest">Load manifest</button>
31
- <span id="status" class="small"></span>
32
- </div>
33
- <label for="tool">Tools</label>
34
- <select id="tool"></select>
35
- <pre id="toolInfo"></pre>
36
-
37
- <h3>Presets</h3>
38
- <div class="preset">
39
- <button id="presetFuzzyUser">fuzzySearch users: q="corey hadden"</button>
40
- <button id="presetFuzzyHouse">fuzzySearch houses: q="backyard"</button>
41
- <button id="presetListApps">listApps: all app definitions</button>
42
- <button id="presetGetSchemas">getSchemas: ["client","user"]</button>
43
- <button id="presetGetSchemasSummary">getSchemas (summaryOnly): ["task","project"]</button>
44
- <button id="presetSearchSlimRecent">search clients (slim, recent)</button>
45
- <button id="presetSearchWithCount">search clients (withCount)</button>
46
- <button id="presetSearchCountOnly">search clients (countOnly)</button>
47
- <button id="presetSaveObjects">saveObjects: batch save (example)</button>
48
- <button id="presetUnderstandObject">understandObject: by _id</button>
49
- <button id="presetFindByTag">findObjectsByTag: by tags (example)</button>
50
- <button id="presetFindByStatus">findObjectsByStatus: by status (example)</button>
51
- </div>
52
-
53
- <h3>Call JSON-RPC</h3>
54
- <label for="params">Params (JSON)</label>
55
- <textarea id="params">{}</textarea>
56
- <div class="row">
57
- <button id="call">POST /mcp</button>
58
- <span id="callStatus" class="small"></span>
59
- </div>
60
- <pre id="result"></pre>
61
-
62
- <script>
63
- (function(){
64
- const $ = (id)=>document.getElementById(id);
65
- const base = $('base');
66
- const loadBtn = $('loadManifest');
67
- const status = $('status');
68
- const toolSel = $('tool');
69
- const toolInfo = $('toolInfo');
70
- const params = $('params');
71
- const callBtn = $('call');
72
- const callStatus = $('callStatus');
73
- const result = $('result');
74
- const presetFuzzyUser = $('presetFuzzyUser');
75
- const presetFuzzyHouse = $('presetFuzzyHouse');
76
- const presetListApps = $('presetListApps');
77
- const presetGetSchemas = $('presetGetSchemas');
78
- const presetGetSchemasSummary = $('presetGetSchemasSummary');
79
- const presetSearchSlimRecent = $('presetSearchSlimRecent');
80
- const presetSearchWithCount = $('presetSearchWithCount');
81
- const presetSearchCountOnly = $('presetSearchCountOnly');
82
- const presetSaveObjects = $('presetSaveObjects');
83
- const presetUnderstandObject = $('presetUnderstandObject');
84
- const presetFindByTag = $('presetFindByTag');
85
- const presetFindByStatus = $('presetFindByStatus');
86
-
87
- // Try to infer base from window location
88
- base.value = base.value || (location.origin);
89
-
90
- let manifest = null;
91
- let idCounter = 1;
92
-
93
- function ensureTool(name, meta){
94
- if (!manifest) { manifest = { tools: [] }; }
95
- manifest.tools = manifest.tools || [];
96
- var existing = (manifest.tools||[]).find(function(t){ return t && t.name === name; });
97
- if (!existing && meta) { manifest.tools.push(meta); }
98
- // ensure option exists in select
99
- var hasOpt = false;
100
- for (var i=0;i<toolSel.options.length;i++){ if (toolSel.options[i].value === name) { hasOpt = true; break; } }
101
- if (!hasOpt){ var opt=document.createElement('option'); opt.value=name; opt.textContent=name; toolSel.appendChild(opt); }
102
- }
103
-
104
- function setStatus(el, msg, ok){
105
- el.textContent = msg || '';
106
- el.className = 'small ' + (ok===true?'good': ok===false?'bad':'');
107
- }
108
-
109
- async function fetchJSON(url, opts){
110
- const res = await fetch(url, opts);
111
- const ct = res.headers.get('content-type')||'';
112
- const isJSON = ct.includes('application/json');
113
- if(!res.ok){
114
- let detail = isJSON ? await res.json().catch(()=>({})) : await res.text();
115
- throw new Error('HTTP '+res.status+': '+(isJSON?JSON.stringify(detail):detail));
116
- }
117
- return isJSON ? res.json() : res.text();
118
- }
119
-
120
- loadBtn.onclick = async function(){
121
- setStatus(status, 'Loading...', null);
122
- toolSel.innerHTML = '';
123
- toolInfo.textContent='';
124
- try{
125
- const url = base.value.replace(/\/$/,'') + '/.well-known/mcp/manifest.json';
126
- manifest = await fetchJSON(url);
127
- // Instance info handled by shared nav script
128
- (manifest.tools||[]).forEach(t=>{
129
- const opt=document.createElement('option');
130
- opt.value=t.name; opt.textContent=t.name;
131
- toolSel.appendChild(opt);
132
- });
133
- if((manifest.tools||[]).length){
134
- toolSel.selectedIndex=0; renderToolInfo();
135
- }
136
- setStatus(status, 'Manifest loaded', true);
137
- }catch(e){
138
- setStatus(status, e.message||String(e), false);
139
- }
140
- };
141
-
142
- function renderToolInfo(){
143
- const name = toolSel.value;
144
- const tool = (manifest.tools||[]).find(t=>t.name===name);
145
- toolInfo.textContent = tool ? JSON.stringify(tool, null, 2) : '';
146
- // Prefill common params for convenience
147
- if(tool && tool.params){
148
- params.value = JSON.stringify(Object.fromEntries(Object.keys(tool.params.properties||{}).map(k=>[k, null])), null, 2);
149
- }
150
- // Hydrate presets UI
151
- if (name === 'hydrate') {
152
- params.value = JSON.stringify({}, null, 2);
153
- }
154
- }
155
- toolSel.onchange = renderToolInfo;
156
-
157
- presetFuzzyUser.onclick = function(){
158
- toolSel.value = 'fuzzySearch';
159
- renderToolInfo();
160
- params.value = JSON.stringify({ q: 'corey hadden', filters: { itemtype: 'user' }, threshold: 0.5 }, null, 2);
161
- };
162
- presetFuzzyHouse.onclick = function(){
163
- toolSel.value = 'fuzzySearch';
164
- renderToolInfo();
165
- params.value = JSON.stringify({ q: 'backyard', filters: { itemtype: 'house' }, threshold: 0.5 }, null, 2);
166
- };
167
- presetListApps.onclick = function(){
168
- toolSel.value = 'listApps';
169
- renderToolInfo();
170
- params.value = JSON.stringify({}, null, 2);
171
- };
172
-
173
- presetGetSchemas.onclick = function(){
174
- ensureTool('getSchemas', {
175
- name: 'getSchemas',
176
- description: 'Retrieve multiple schema definitions. If omitted, returns all.',
177
- params: { type: 'object', properties: { names: { type: 'array', items: { type: 'string' } } } },
178
- returns: { type: 'object' }
179
- });
180
- toolSel.value = 'getSchemas';
181
- renderToolInfo();
182
- params.value = JSON.stringify({ names: ["client", "user"] }, null, 2);
183
- };
184
-
185
- presetGetSchemasSummary.onclick = function(){
186
- ensureTool('getSchemas', {
187
- name: 'getSchemas',
188
- description: 'Retrieve multiple schemas. With summaryOnly=true, returns summaries; if names omitted, returns all.',
189
- params: { type: 'object', properties: { names: { type: 'array', items: { type: 'string' } }, summaryOnly: { type: 'boolean' } } },
190
- returns: { type: 'object' }
191
- });
192
- toolSel.value = 'getSchemas';
193
- renderToolInfo();
194
- params.value = JSON.stringify({ names: ["task", "project"], summaryOnly: true }, null, 2);
195
- };
196
-
197
- presetSearchSlimRecent.onclick = function(){
198
- toolSel.value = 'search';
199
- renderToolInfo();
200
- params.value = JSON.stringify({ itemtype: 'client', source: 'cache', query: { itemtype: 'client' }, limit: 25, sortBy: 'joeUpdated', sortDir: 'desc', slim: true }, null, 2);
201
- };
202
-
203
- presetSearchWithCount.onclick = function(){
204
- toolSel.value = 'search';
205
- renderToolInfo();
206
- params.value = JSON.stringify({ itemtype: 'client', source: 'cache', query: { itemtype: 'client' }, limit: 25, withCount: true, sortBy: 'joeUpdated', sortDir: 'desc' }, null, 2);
207
- };
208
-
209
- presetSearchCountOnly.onclick = function(){
210
- toolSel.value = 'search';
211
- renderToolInfo();
212
- params.value = JSON.stringify({ itemtype: 'client', source: 'cache', query: { itemtype: 'client' }, countOnly: true }, null, 2);
213
- };
214
-
215
- presetSaveObjects.onclick = function(){
216
- toolSel.value = 'saveObjects';
217
- renderToolInfo();
218
- // Example: two minimal objects; adjust itemtype/fields as needed
219
- params.value = JSON.stringify({
220
- objects: [
221
- { itemtype: "client", name: "Batch Client A" },
222
- { itemtype: "client", name: "Batch Client B" }
223
- ],
224
- stopOnError: false,
225
- concurrency: 5
226
- }, null, 2);
227
- };
228
-
229
- presetUnderstandObject.onclick = function(){
230
- toolSel.value = 'understandObject';
231
- renderToolInfo();
232
- // Provide a template; user should replace _id with a real object id.
233
- params.value = JSON.stringify({ _id: "REPLACE_WITH_OBJECT_ID", depth: 2 }, null, 2);
234
- };
235
-
236
- presetFindByTag.onclick = function(){
237
- toolSel.value = 'findObjectsByTag';
238
- renderToolInfo();
239
- // Example: find objects with all of these tags
240
- params.value = JSON.stringify({ tags: ["REPLACE_WITH_TAG_ID_1", "REPLACE_WITH_TAG_ID_2"], itemtype: "task" }, null, 2);
241
- };
242
-
243
- presetFindByStatus.onclick = function(){
244
- toolSel.value = 'findObjectsByStatus';
245
- renderToolInfo();
246
- // Example: find objects with this status
247
- params.value = JSON.stringify({ status: "REPLACE_WITH_STATUS_ID", itemtype: "task" }, null, 2);
248
- };
249
-
250
- callBtn.onclick = async function(){
251
- setStatus(callStatus, 'Calling...', null);
252
- result.textContent='';
253
- try{
254
- const url = base.value.replace(/\/$/,'') + '/mcp';
255
- let p = {};
256
- try{ p = params.value ? JSON.parse(params.value) : {}; }catch(e){ throw new Error('Invalid JSON in params'); }
257
- const body = {
258
- jsonrpc: '2.0',
259
- id: String(idCounter++),
260
- method: toolSel.value,
261
- params: p
262
- };
263
- const resp = await fetchJSON(url, { method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify(body)});
264
- result.textContent = JSON.stringify(resp, null, 2);
265
- setStatus(callStatus, 'OK', true);
266
- }catch(e){
267
- setStatus(callStatus, e.message||String(e), false);
268
- }
269
- };
270
-
271
- // Auto-load manifest on open
272
- setTimeout(()=>loadBtn.click(), 50);
273
- })();
274
- </script>
275
- </body>
276
- </html>
1
+ <!doctype html>
2
+ <html>
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <title>JOE MCP Test</title>
6
+ <meta name="viewport" content="width=device-width, initial-scale=1">
7
+ <style>
8
+ body{font-family:system-ui,-apple-system,Segoe UI,Roboto,Ubuntu,Cantarell,Noto Sans,sans-serif;margin:20px;}
9
+ label{display:block;margin:8px 0 4px}
10
+ select, input, textarea, button{font-size:14px}
11
+ textarea{width:100%;height:160px;font-family:ui-monospace,Menlo,Consolas,monospace}
12
+ pre{background:#f6f8fa;border:1px solid #e1e4e8;padding:10px;overflow:auto}
13
+ .row{display:flex;gap:12px;align-items:center;flex-wrap:wrap}
14
+ .small{font-size:12px;color:#666}
15
+ .bad{color:#b00020}
16
+ .good{color:#0a7d00}
17
+ .preset{margin:8px 0;}
18
+ </style>
19
+ </head>
20
+ <body>
21
+ <div id="mcp-nav"></div>
22
+ <script src="/JsonObjectEditor/_www/mcp-nav.js"></script>
23
+ <h1>JOE MCP Test</h1>
24
+ <div class="small">Use this page to discover tools and call the JSON-RPC endpoint.</div>
25
+
26
+ <h3>Toolsets (from manifest)</h3>
27
+ <pre id="toolsets"></pre>
28
+
29
+ <h3>Manifest</h3>
30
+ <div class="row">
31
+ <label for="base">Base URL</label>
32
+ <input id="base" value="" placeholder="http://localhost:{{PORT}}" style="min-width:280px"/>
33
+ <button id="loadManifest">Load manifest</button>
34
+ <span id="status" class="small"></span>
35
+ </div>
36
+ <label for="tool">Tools</label>
37
+ <select id="tool"></select>
38
+ <pre id="toolInfo"></pre>
39
+
40
+ <h3>Presets</h3>
41
+ <div class="preset">
42
+ <button id="presetFuzzyUser">fuzzySearch users: q="corey hadden"</button>
43
+ <button id="presetFuzzyHouse">fuzzySearch houses: q="backyard"</button>
44
+ <button id="presetListApps">listApps: all app definitions</button>
45
+ <button id="presetGetSchemas">getSchemas: ["client","user"]</button>
46
+ <button id="presetGetSchemasSummary">getSchemas (summaryOnly): ["task","project"]</button>
47
+ <button id="presetSearchSlimRecent">search clients (slim, recent)</button>
48
+ <button id="presetSearchWithCount">search clients (withCount)</button>
49
+ <button id="presetSearchCountOnly">search clients (countOnly)</button>
50
+ <button id="presetSaveObjects">saveObjects: batch save (example)</button>
51
+ <button id="presetUnderstandObject">understandObject: by _id</button>
52
+ <button id="presetFindByTag">findObjectsByTag: by tags (example)</button>
53
+ <button id="presetFindByStatus">findObjectsByStatus: by status (example)</button>
54
+ </div>
55
+
56
+ <h3>Call JSON-RPC</h3>
57
+ <label for="params">Params (JSON)</label>
58
+ <textarea id="params">{}</textarea>
59
+ <div class="row">
60
+ <button id="call">POST /mcp</button>
61
+ <span id="callStatus" class="small"></span>
62
+ </div>
63
+ <pre id="result"></pre>
64
+
65
+ <script>
66
+ (function(){
67
+ const $ = (id)=>document.getElementById(id);
68
+ const base = $('base');
69
+ const loadBtn = $('loadManifest');
70
+ const toolsetsEl = document.getElementById('toolsets');
71
+ const status = $('status');
72
+ const toolSel = $('tool');
73
+ const toolInfo = $('toolInfo');
74
+ const params = $('params');
75
+ const callBtn = $('call');
76
+ const callStatus = $('callStatus');
77
+ const result = $('result');
78
+ const presetFuzzyUser = $('presetFuzzyUser');
79
+ const presetFuzzyHouse = $('presetFuzzyHouse');
80
+ const presetListApps = $('presetListApps');
81
+ const presetGetSchemas = $('presetGetSchemas');
82
+ const presetGetSchemasSummary = $('presetGetSchemasSummary');
83
+ const presetSearchSlimRecent = $('presetSearchSlimRecent');
84
+ const presetSearchWithCount = $('presetSearchWithCount');
85
+ const presetSearchCountOnly = $('presetSearchCountOnly');
86
+ const presetSaveObjects = $('presetSaveObjects');
87
+ const presetUnderstandObject = $('presetUnderstandObject');
88
+ const presetFindByTag = $('presetFindByTag');
89
+ const presetFindByStatus = $('presetFindByStatus');
90
+
91
+ // Try to infer base from window location
92
+ base.value = base.value || (location.origin);
93
+
94
+ let manifest = null;
95
+ let idCounter = 1;
96
+
97
+ function ensureTool(name, meta){
98
+ if (!manifest) { manifest = { tools: [] }; }
99
+ manifest.tools = manifest.tools || [];
100
+ var existing = (manifest.tools||[]).find(function(t){ return t && t.name === name; });
101
+ if (!existing && meta) { manifest.tools.push(meta); }
102
+ // ensure option exists in select
103
+ var hasOpt = false;
104
+ for (var i=0;i<toolSel.options.length;i++){ if (toolSel.options[i].value === name) { hasOpt = true; break; } }
105
+ if (!hasOpt){ var opt=document.createElement('option'); opt.value=name; opt.textContent=name; toolSel.appendChild(opt); }
106
+ }
107
+
108
+ function setStatus(el, msg, ok){
109
+ el.textContent = msg || '';
110
+ el.className = 'small ' + (ok===true?'good': ok===false?'bad':'');
111
+ }
112
+
113
+ async function fetchJSON(url, opts){
114
+ const res = await fetch(url, opts);
115
+ const ct = res.headers.get('content-type')||'';
116
+ const isJSON = ct.includes('application/json');
117
+ if(!res.ok){
118
+ let detail = isJSON ? await res.json().catch(()=>({})) : await res.text();
119
+ throw new Error('HTTP '+res.status+': '+(isJSON?JSON.stringify(detail):detail));
120
+ }
121
+ return isJSON ? res.json() : res.text();
122
+ }
123
+
124
+ loadBtn.onclick = async function(){
125
+ setStatus(status, 'Loading...', null);
126
+ toolSel.innerHTML = '';
127
+ toolInfo.textContent='';
128
+ try{
129
+ const url = base.value.replace(/\/$/,'') + '/.well-known/mcp/manifest.json';
130
+ manifest = await fetchJSON(url);
131
+ // Instance info handled by shared nav script
132
+ (manifest.tools||[]).forEach(t=>{
133
+ const opt=document.createElement('option');
134
+ opt.value=t.name; opt.textContent=t.name;
135
+ toolSel.appendChild(opt);
136
+ });
137
+ if((manifest.tools||[]).length){
138
+ toolSel.selectedIndex=0; renderToolInfo();
139
+ }
140
+ if (toolsetsEl) {
141
+ try{
142
+ toolsetsEl.textContent = JSON.stringify(manifest.toolsets || {}, null, 2);
143
+ }catch(_e){
144
+ toolsetsEl.textContent = '';
145
+ }
146
+ }
147
+ setStatus(status, 'Manifest loaded', true);
148
+ }catch(e){
149
+ setStatus(status, e.message||String(e), false);
150
+ }
151
+ };
152
+
153
+ function renderToolInfo(){
154
+ const name = toolSel.value;
155
+ const tool = (manifest.tools||[]).find(t=>t.name===name);
156
+ toolInfo.textContent = tool ? JSON.stringify(tool, null, 2) : '';
157
+ // Prefill common params for convenience
158
+ if(tool && tool.params){
159
+ params.value = JSON.stringify(Object.fromEntries(Object.keys(tool.params.properties||{}).map(k=>[k, null])), null, 2);
160
+ }
161
+ // Hydrate presets UI
162
+ if (name === 'hydrate') {
163
+ params.value = JSON.stringify({}, null, 2);
164
+ }
165
+ }
166
+ toolSel.onchange = renderToolInfo;
167
+
168
+ presetFuzzyUser.onclick = function(){
169
+ toolSel.value = 'fuzzySearch';
170
+ renderToolInfo();
171
+ params.value = JSON.stringify({ q: 'corey hadden', filters: { itemtype: 'user' }, threshold: 0.5 }, null, 2);
172
+ };
173
+ presetFuzzyHouse.onclick = function(){
174
+ toolSel.value = 'fuzzySearch';
175
+ renderToolInfo();
176
+ params.value = JSON.stringify({ q: 'backyard', filters: { itemtype: 'house' }, threshold: 0.5 }, null, 2);
177
+ };
178
+ presetListApps.onclick = function(){
179
+ toolSel.value = 'listApps';
180
+ renderToolInfo();
181
+ params.value = JSON.stringify({}, null, 2);
182
+ };
183
+
184
+ presetGetSchemas.onclick = function(){
185
+ ensureTool('getSchemas', {
186
+ name: 'getSchemas',
187
+ description: 'Retrieve multiple schema definitions. If omitted, returns all.',
188
+ params: { type: 'object', properties: { names: { type: 'array', items: { type: 'string' } } } },
189
+ returns: { type: 'object' }
190
+ });
191
+ toolSel.value = 'getSchemas';
192
+ renderToolInfo();
193
+ params.value = JSON.stringify({ names: ["client", "user"] }, null, 2);
194
+ };
195
+
196
+ presetGetSchemasSummary.onclick = function(){
197
+ ensureTool('getSchemas', {
198
+ name: 'getSchemas',
199
+ description: 'Retrieve multiple schemas. With summaryOnly=true, returns summaries; if names omitted, returns all.',
200
+ params: { type: 'object', properties: { names: { type: 'array', items: { type: 'string' } }, summaryOnly: { type: 'boolean' } } },
201
+ returns: { type: 'object' }
202
+ });
203
+ toolSel.value = 'getSchemas';
204
+ renderToolInfo();
205
+ params.value = JSON.stringify({ names: ["task", "project"], summaryOnly: true }, null, 2);
206
+ };
207
+
208
+ presetSearchSlimRecent.onclick = function(){
209
+ toolSel.value = 'search';
210
+ renderToolInfo();
211
+ params.value = JSON.stringify({ itemtype: 'client', source: 'cache', query: { itemtype: 'client' }, limit: 25, sortBy: 'joeUpdated', sortDir: 'desc', slim: true }, null, 2);
212
+ };
213
+
214
+ presetSearchWithCount.onclick = function(){
215
+ toolSel.value = 'search';
216
+ renderToolInfo();
217
+ params.value = JSON.stringify({ itemtype: 'client', source: 'cache', query: { itemtype: 'client' }, limit: 25, withCount: true, sortBy: 'joeUpdated', sortDir: 'desc' }, null, 2);
218
+ };
219
+
220
+ presetSearchCountOnly.onclick = function(){
221
+ toolSel.value = 'search';
222
+ renderToolInfo();
223
+ params.value = JSON.stringify({ itemtype: 'client', source: 'cache', query: { itemtype: 'client' }, countOnly: true }, null, 2);
224
+ };
225
+
226
+ presetSaveObjects.onclick = function(){
227
+ toolSel.value = 'saveObjects';
228
+ renderToolInfo();
229
+ // Example: two minimal objects; adjust itemtype/fields as needed
230
+ params.value = JSON.stringify({
231
+ objects: [
232
+ { itemtype: "client", name: "Batch Client A" },
233
+ { itemtype: "client", name: "Batch Client B" }
234
+ ],
235
+ stopOnError: false,
236
+ concurrency: 5
237
+ }, null, 2);
238
+ };
239
+
240
+ presetUnderstandObject.onclick = function(){
241
+ toolSel.value = 'understandObject';
242
+ renderToolInfo();
243
+ // Provide a template; user should replace _id with a real object id.
244
+ params.value = JSON.stringify({ _id: "REPLACE_WITH_OBJECT_ID", depth: 2 }, null, 2);
245
+ };
246
+
247
+ presetFindByTag.onclick = function(){
248
+ toolSel.value = 'findObjectsByTag';
249
+ renderToolInfo();
250
+ // Example: find objects with all of these tags
251
+ params.value = JSON.stringify({ tags: ["REPLACE_WITH_TAG_ID_1", "REPLACE_WITH_TAG_ID_2"], itemtype: "task" }, null, 2);
252
+ };
253
+
254
+ presetFindByStatus.onclick = function(){
255
+ toolSel.value = 'findObjectsByStatus';
256
+ renderToolInfo();
257
+ // Example: find objects with this status
258
+ params.value = JSON.stringify({ status: "REPLACE_WITH_STATUS_ID", itemtype: "task" }, null, 2);
259
+ };
260
+
261
+ callBtn.onclick = async function(){
262
+ setStatus(callStatus, 'Calling...', null);
263
+ result.textContent='';
264
+ try{
265
+ const url = base.value.replace(/\/$/,'') + '/mcp';
266
+ let p = {};
267
+ try{ p = params.value ? JSON.parse(params.value) : {}; }catch(e){ throw new Error('Invalid JSON in params'); }
268
+ const body = {
269
+ jsonrpc: '2.0',
270
+ id: String(idCounter++),
271
+ method: toolSel.value,
272
+ params: p
273
+ };
274
+ const resp = await fetchJSON(url, { method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify(body)});
275
+ result.textContent = JSON.stringify(resp, null, 2);
276
+ setStatus(callStatus, 'OK', true);
277
+ }catch(e){
278
+ setStatus(callStatus, e.message||String(e), false);
279
+ }
280
+ };
281
+
282
+ // Auto-load manifest on open
283
+ setTimeout(()=>loadBtn.click(), 50);
284
+ })();
285
+ </script>
286
+ </body>
287
+ </html>