json-object-editor 0.10.509 → 0.10.521
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 +35 -0
- package/_www/mcp-export.html +11 -4
- package/_www/mcp-nav.js +8 -4
- package/_www/mcp-prompt.html +96 -121
- package/_www/mcp-schemas.html +294 -0
- package/_www/mcp-test.html +86 -0
- package/docs/JOE_Master_Knowledge_Export.md +135 -0
- package/docs/joe_agent_custom_gpt_instructions_v_2.md +54 -0
- package/docs/joe_agent_spec_v_2.2.md +64 -0
- package/docs/schema_summary_guidelines.md +128 -0
- package/package.json +1 -1
- package/readme.md +525 -469
- package/server/modules/MCP.js +606 -405
- package/server/modules/Schemas.js +321 -111
- package/server/modules/Server.js +26 -15
- package/server/modules/Storage.js +9 -0
- package/server/relationships.graph.json +5 -0
- package/server/schemas/block.js +37 -0
- package/server/schemas/board.js +2 -1
- package/server/schemas/budget.js +28 -1
- package/server/schemas/event.js +42 -0
- package/server/schemas/financial_account.js +35 -0
- package/server/schemas/goal.js +30 -0
- package/server/schemas/group.js +31 -0
- package/server/schemas/include.js +28 -0
- package/server/schemas/ingredient.js +28 -0
- package/server/schemas/initiative.js +32 -0
- package/server/schemas/instance.js +31 -1
- package/server/schemas/layout.js +31 -0
- package/server/schemas/ledger.js +30 -0
- package/server/schemas/list.js +33 -0
- package/server/schemas/meal.js +30 -0
- package/server/schemas/note.js +30 -0
- package/server/schemas/notification.js +33 -1
- package/server/schemas/page.js +43 -0
- package/server/schemas/post.js +32 -0
- package/server/schemas/project.js +36 -0
- package/server/schemas/recipe.js +32 -0
- package/server/schemas/report.js +32 -0
- package/server/schemas/setting.js +22 -0
- package/server/schemas/site.js +30 -0
- package/server/schemas/status.js +33 -0
- package/server/schemas/tag.js +28 -1
- package/server/schemas/task.js +778 -737
- package/server/schemas/transaction.js +43 -0
- package/server/schemas/user.js +36 -1
- package/server/schemas/workflow.js +30 -1
package/server/modules/MCP.js
CHANGED
|
@@ -1,405 +1,606 @@
|
|
|
1
|
-
// modules/MCP.js
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Model Context Protocol (MCP) core module for JOE.
|
|
5
|
-
* This module provides a JSON-RPC 2.0 compatible interface to JOE objects and schemas.
|
|
6
|
-
* Agents (like OpenAI Assistants) can discover and call structured tools via manifest + POST.
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
const MCP = {};
|
|
10
|
-
const { Storage, Schemas } = global.JOE; // Adjust as needed based on how your modules are wired
|
|
11
|
-
|
|
12
|
-
// Internal helpers
|
|
13
|
-
function loadFromStorage(collection, query) {
|
|
14
|
-
return new Promise((resolve, reject) => {
|
|
15
|
-
try {
|
|
16
|
-
Storage.load(collection, query || {}, function(err, results){
|
|
17
|
-
if (err) return reject(err);
|
|
18
|
-
resolve(results || []);
|
|
19
|
-
});
|
|
20
|
-
} catch (e) {
|
|
21
|
-
reject(e);
|
|
22
|
-
}
|
|
23
|
-
});
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
function sanitizeItems(items) {
|
|
27
|
-
try {
|
|
28
|
-
const arr = Array.isArray(items) ? items : [items];
|
|
29
|
-
return arr.map(i => {
|
|
30
|
-
if (!i || typeof i !== 'object') return i;
|
|
31
|
-
const copy = JSON.parse(JSON.stringify(i));
|
|
32
|
-
if (copy.password) copy.password = null;
|
|
33
|
-
if (copy.token) copy.token = null;
|
|
34
|
-
return copy;
|
|
35
|
-
});
|
|
36
|
-
} catch (e) {
|
|
37
|
-
return Array.isArray(items) ? items : [items];
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
//
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
if (
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
},
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
},
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
};
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
//
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
const
|
|
346
|
-
const
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
}
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
}
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
}
|
|
405
|
-
|
|
1
|
+
// modules/MCP.js
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Model Context Protocol (MCP) core module for JOE.
|
|
5
|
+
* This module provides a JSON-RPC 2.0 compatible interface to JOE objects and schemas.
|
|
6
|
+
* Agents (like OpenAI Assistants) can discover and call structured tools via manifest + POST.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const MCP = {};
|
|
10
|
+
const { Storage, Schemas } = global.JOE; // Adjust as needed based on how your modules are wired
|
|
11
|
+
|
|
12
|
+
// Internal helpers
|
|
13
|
+
function loadFromStorage(collection, query) {
|
|
14
|
+
return new Promise((resolve, reject) => {
|
|
15
|
+
try {
|
|
16
|
+
Storage.load(collection, query || {}, function(err, results){
|
|
17
|
+
if (err) return reject(err);
|
|
18
|
+
resolve(results || []);
|
|
19
|
+
});
|
|
20
|
+
} catch (e) {
|
|
21
|
+
reject(e);
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function sanitizeItems(items) {
|
|
27
|
+
try {
|
|
28
|
+
const arr = Array.isArray(items) ? items : [items];
|
|
29
|
+
return arr.map(i => {
|
|
30
|
+
if (!i || typeof i !== 'object') return i;
|
|
31
|
+
const copy = JSON.parse(JSON.stringify(i));
|
|
32
|
+
if (copy.password) copy.password = null;
|
|
33
|
+
if (copy.token) copy.token = null;
|
|
34
|
+
return copy;
|
|
35
|
+
});
|
|
36
|
+
} catch (e) {
|
|
37
|
+
return Array.isArray(items) ? items : [items];
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function getComparable(val){
|
|
42
|
+
if (val == null) return null;
|
|
43
|
+
// Date-like
|
|
44
|
+
var d = new Date(val);
|
|
45
|
+
if (!isNaN(d.getTime())) return d.getTime();
|
|
46
|
+
if (typeof val === 'number') return val;
|
|
47
|
+
return (val+'').toLowerCase();
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function sortItems(items, sortBy, sortDir){
|
|
51
|
+
if (!Array.isArray(items) || !sortBy) return items;
|
|
52
|
+
var dir = (sortDir === 'asc') ? 1 : -1;
|
|
53
|
+
return items.slice().sort(function(a,b){
|
|
54
|
+
var av = getComparable(a && a[sortBy]);
|
|
55
|
+
var bv = getComparable(b && b[sortBy]);
|
|
56
|
+
if (av == null && bv == null) return 0;
|
|
57
|
+
if (av == null) return 1; // nulls last
|
|
58
|
+
if (bv == null) return -1;
|
|
59
|
+
if (av > bv) return 1*dir;
|
|
60
|
+
if (av < bv) return -1*dir;
|
|
61
|
+
return 0;
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function toSlim(item){
|
|
66
|
+
var name = (item && (item.name || item.title || item.label || item.email || item.slug)) || (item && item._id) || '';
|
|
67
|
+
var info = (item && (item.info || item.description || item.summary)) || '';
|
|
68
|
+
return {
|
|
69
|
+
_id: item && item._id,
|
|
70
|
+
itemtype: item && item.itemtype,
|
|
71
|
+
name: name,
|
|
72
|
+
info: info,
|
|
73
|
+
joeUpdated: item && (item.joeUpdated || item.updated || item.modified),
|
|
74
|
+
created: item && item.created
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// ----------------------
|
|
79
|
+
// TOOL DEFINITIONS
|
|
80
|
+
// ----------------------
|
|
81
|
+
// This object maps tool names to actual execution functions.
|
|
82
|
+
// Each function takes a `params` object and returns a JSON-serializable result.
|
|
83
|
+
MCP.tools = {
|
|
84
|
+
|
|
85
|
+
// List all schema names in the system
|
|
86
|
+
listSchemas: async (_params, _ctx) => {
|
|
87
|
+
const list = (Schemas && (
|
|
88
|
+
(Schemas.schemaList && Schemas.schemaList.length && Schemas.schemaList) ||
|
|
89
|
+
(Schemas.schema && Object.keys(Schemas.schema))
|
|
90
|
+
)) || [];
|
|
91
|
+
return list.slice().sort();
|
|
92
|
+
},
|
|
93
|
+
|
|
94
|
+
// Get schema definition; summaryOnly to return normalized summary instead of full schema
|
|
95
|
+
getSchema: async ({ name, summaryOnly }, _ctx) => {
|
|
96
|
+
if (!name) throw new Error("Missing required param 'name'");
|
|
97
|
+
if (summaryOnly) {
|
|
98
|
+
const sum = Schemas && Schemas.summary && Schemas.summary[name];
|
|
99
|
+
if (!sum) throw new Error(`Schema summary for "${name}" not found`);
|
|
100
|
+
return sum;
|
|
101
|
+
}
|
|
102
|
+
const def = Schemas.schema && Schemas.schema[name];
|
|
103
|
+
if (!def) throw new Error(`Schema "${name}" not found`);
|
|
104
|
+
return def;
|
|
105
|
+
},
|
|
106
|
+
|
|
107
|
+
// Get multiple schemas. With summaryOnly, return summaries; without names, returns all.
|
|
108
|
+
getSchemas: async ({ names, summaryOnly } = {}, _ctx) => {
|
|
109
|
+
const all = (Schemas && Schemas.schema) || {};
|
|
110
|
+
if (summaryOnly) {
|
|
111
|
+
const allS = (Schemas && Schemas.summary) || {};
|
|
112
|
+
if (Array.isArray(names) && names.length) {
|
|
113
|
+
const outS = {};
|
|
114
|
+
names.forEach(function(n){ if (allS[n]) { outS[n] = allS[n]; } });
|
|
115
|
+
return outS;
|
|
116
|
+
}
|
|
117
|
+
return allS;
|
|
118
|
+
} else {
|
|
119
|
+
if (Array.isArray(names) && names.length) {
|
|
120
|
+
const out = {};
|
|
121
|
+
names.forEach(function(n){ if (all[n]) { out[n] = all[n]; } });
|
|
122
|
+
return out;
|
|
123
|
+
}
|
|
124
|
+
return all;
|
|
125
|
+
}
|
|
126
|
+
},
|
|
127
|
+
|
|
128
|
+
// Convenience: fetch a single object by _id (schema optional). Prefer cache; fallback to storage.
|
|
129
|
+
getObject: async ({ _id, schema, flatten = false, depth = 1 }, _ctx) => {
|
|
130
|
+
if (!_id) throw new Error("Missing required param '_id'");
|
|
131
|
+
// Fast path via global lookup
|
|
132
|
+
let obj = (JOE && JOE.Cache && JOE.Cache.findByID) ? (JOE.Cache.findByID(_id) || null) : null;
|
|
133
|
+
if (!obj && schema) {
|
|
134
|
+
const results = await loadFromStorage(schema, { _id });
|
|
135
|
+
obj = (results && results[0]) || null;
|
|
136
|
+
}
|
|
137
|
+
if (!obj && schema && JOE && JOE.Cache && JOE.Cache.findByID) {
|
|
138
|
+
obj = JOE.Cache.findByID(schema, _id) || null;
|
|
139
|
+
}
|
|
140
|
+
if (!obj) throw new Error(`Object not found${schema?(' in '+schema):''} with _id: ${_id}`);
|
|
141
|
+
if (flatten && JOE && JOE.Utils && JOE.Utils.flattenObject) {
|
|
142
|
+
try { return sanitizeItems(JOE.Utils.flattenObject(_id, { recursive: true, depth }))[0]; } catch(e) {}
|
|
143
|
+
}
|
|
144
|
+
return sanitizeItems(obj)[0];
|
|
145
|
+
},
|
|
146
|
+
|
|
147
|
+
/* Deprecated: use unified 'search' instead
|
|
148
|
+
getObjectsByIds: async () => { throw new Error('Use search with ids instead'); },
|
|
149
|
+
queryObjects: async () => { throw new Error('Use search instead'); },
|
|
150
|
+
*/
|
|
151
|
+
|
|
152
|
+
/* Deprecated: use unified 'search' instead
|
|
153
|
+
searchCache: async () => { throw new Error('Use search instead'); },
|
|
154
|
+
*/
|
|
155
|
+
|
|
156
|
+
// Unified search: defaults to cache; set source="storage" to query DB for a schema
|
|
157
|
+
search: async ({ schema, query = {}, ids, source = 'cache', limit = 50, offset = 0, flatten = false, depth = 1, countOnly = false, withCount = false, sortBy, sortDir = 'desc', slim = false }, _ctx) => {
|
|
158
|
+
const useCache = !source || source === 'cache';
|
|
159
|
+
const useStorage = source === 'storage';
|
|
160
|
+
|
|
161
|
+
if (ids && !Array.isArray(ids)) throw new Error("'ids' must be an array if provided");
|
|
162
|
+
|
|
163
|
+
// When ids are provided and a schema is known, prefer cache for safety/speed
|
|
164
|
+
if (Array.isArray(ids) && schema) {
|
|
165
|
+
let items = [];
|
|
166
|
+
if (JOE && JOE.Cache && JOE.Cache.findByID) {
|
|
167
|
+
const found = JOE.Cache.findByID(schema, ids.join(',')) || [];
|
|
168
|
+
items = Array.isArray(found) ? found : (found ? [found] : []);
|
|
169
|
+
}
|
|
170
|
+
if (useStorage && (!items || items.length === 0)) {
|
|
171
|
+
try {
|
|
172
|
+
const fromStorage = await loadFromStorage(schema, { _id: { $in: ids } });
|
|
173
|
+
items = fromStorage || [];
|
|
174
|
+
} catch (e) { /* ignore storage errors here */ }
|
|
175
|
+
}
|
|
176
|
+
if (flatten && !slim && JOE && JOE.Utils && JOE.Utils.flattenObject) {
|
|
177
|
+
try { items = ids.map(id => JOE.Utils.flattenObject(id, { recursive: true, depth })); } catch (e) {}
|
|
178
|
+
}
|
|
179
|
+
items = sortItems(items, sortBy, sortDir);
|
|
180
|
+
if (countOnly) {
|
|
181
|
+
return { count: (items || []).length };
|
|
182
|
+
}
|
|
183
|
+
const total = (items || []).length;
|
|
184
|
+
const start = Math.max(0, parseInt(offset || 0, 10) || 0);
|
|
185
|
+
const end = (typeof limit === 'number' && limit > 0) ? (start + limit) : undefined;
|
|
186
|
+
const sliced = (typeof end === 'number') ? items.slice(start, end) : items.slice(start);
|
|
187
|
+
const payload = slim ? (sliced || []).map(toSlim) : sanitizeItems(sliced);
|
|
188
|
+
return withCount ? { items: payload, count: total } : { items: payload };
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// No ids: choose source
|
|
192
|
+
if (useCache) {
|
|
193
|
+
if (!JOE || !JOE.Cache || !JOE.Cache.search) throw new Error('Cache not initialized');
|
|
194
|
+
let results = JOE.Cache.search(query || {});
|
|
195
|
+
if (schema) results = (results || []).filter(i => i && i.itemtype === schema);
|
|
196
|
+
results = sortItems(results, sortBy, sortDir);
|
|
197
|
+
if (countOnly) {
|
|
198
|
+
return { count: (results || []).length };
|
|
199
|
+
}
|
|
200
|
+
const total = (results || []).length;
|
|
201
|
+
const start = Math.max(0, parseInt(offset || 0, 10) || 0);
|
|
202
|
+
const end = (typeof limit === 'number' && limit > 0) ? (start + limit) : undefined;
|
|
203
|
+
const sliced = (typeof end === 'number') ? results.slice(start, end) : results.slice(start);
|
|
204
|
+
const payload = slim ? (sliced || []).map(toSlim) : sanitizeItems(sliced);
|
|
205
|
+
return withCount ? { items: payload, count: total } : { items: payload };
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
if (useStorage) {
|
|
209
|
+
if (!schema) throw new Error("'schema' is required when source=storage");
|
|
210
|
+
const results = await loadFromStorage(schema, query || {});
|
|
211
|
+
let sorted = sortItems(results, sortBy, sortDir);
|
|
212
|
+
if (countOnly) {
|
|
213
|
+
return { count: (sorted || []).length };
|
|
214
|
+
}
|
|
215
|
+
const total = (sorted || []).length;
|
|
216
|
+
const start = Math.max(0, parseInt(offset || 0, 10) || 0);
|
|
217
|
+
const end = (typeof limit === 'number' && limit > 0) ? (start + limit) : undefined;
|
|
218
|
+
const sliced = (typeof end === 'number') ? (sorted || []).slice(start, end) : (sorted || []).slice(start);
|
|
219
|
+
if (flatten && !slim && JOE && JOE.Utils && JOE.Utils.flattenObject) {
|
|
220
|
+
try { return withCount ? { items: sanitizeItems(sliced.map(function(it){ return JOE.Utils.flattenObject(it && it._id, { recursive: true, depth }); })), count: total } : { items: sanitizeItems(sliced.map(function(it){ return JOE.Utils.flattenObject(it && it._id, { recursive: true, depth }); })) }; } catch (e) {}
|
|
221
|
+
}
|
|
222
|
+
const payload = slim ? (sliced || []).map(toSlim) : sanitizeItems(sliced);
|
|
223
|
+
return withCount ? { items: payload, count: total } : { items: payload };
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
throw new Error("Invalid 'source'. Use 'cache' (default) or 'storage'.");
|
|
227
|
+
},
|
|
228
|
+
|
|
229
|
+
// Fuzzy, typo-tolerant search over cache with weighted fields
|
|
230
|
+
fuzzySearch: async ({ schema, q, filters = {}, fields, threshold = 0.35, limit = 50, offset = 0, highlight = false, minQueryLength = 2 }, _ctx) => {
|
|
231
|
+
if (!q || (q+'').length < (minQueryLength||2)) {
|
|
232
|
+
return { items: [] };
|
|
233
|
+
}
|
|
234
|
+
if (!JOE || !JOE.Cache || !JOE.Cache.search) throw new Error('Cache not initialized');
|
|
235
|
+
var query = Object.assign({}, filters || {});
|
|
236
|
+
query.$fuzzy = { q, fields, threshold, limit, offset, highlight, minQueryLength };
|
|
237
|
+
var results = JOE.Cache.search(query) || [];
|
|
238
|
+
if (schema) { results = results.filter(function(i){ return i && i.itemtype === schema; }); }
|
|
239
|
+
var total = (typeof results.count === 'number') ? results.count : results.length;
|
|
240
|
+
if (typeof limit === 'number' && limit > 0) { results = results.slice(0, limit); }
|
|
241
|
+
return { items: sanitizeItems(results), count: total };
|
|
242
|
+
},
|
|
243
|
+
|
|
244
|
+
// Save an object via Storage (respects events/history)
|
|
245
|
+
saveObject: async ({ object }, ctx = {}) => {
|
|
246
|
+
if (!object || !object.itemtype) throw new Error("'object' with 'itemtype' is required");
|
|
247
|
+
const user = (ctx.req && ctx.req.User) ? ctx.req.User : { name: 'anonymous' };
|
|
248
|
+
// Ensure server-side update timestamp parity with /API/save
|
|
249
|
+
if (!object.joeUpdated) { object.joeUpdated = new Date().toISOString(); }
|
|
250
|
+
// Ensure a stable _id for new objects so history and events work consistently
|
|
251
|
+
try {
|
|
252
|
+
if (!object._id) { object._id = (typeof cuid === 'function') ? cuid() : ($c && $c.guid ? $c.guid() : undefined); }
|
|
253
|
+
} catch(_e) { /* ignore id generation errors; Mongo will assign one */ }
|
|
254
|
+
const saved = await new Promise((resolve, reject) => {
|
|
255
|
+
try {
|
|
256
|
+
Storage.save(object, object.itemtype, function(err, data){
|
|
257
|
+
if (err) return reject(err);
|
|
258
|
+
resolve(data);
|
|
259
|
+
}, { user, history: true });
|
|
260
|
+
} catch (e) { reject(e); }
|
|
261
|
+
});
|
|
262
|
+
return sanitizeItems(saved)[0];
|
|
263
|
+
},
|
|
264
|
+
|
|
265
|
+
// Batch save with bounded concurrency; preserves per-item history/events
|
|
266
|
+
saveObjects: async ({ objects, stopOnError = false, concurrency = 5 } = {}, ctx = {}) => {
|
|
267
|
+
if (!Array.isArray(objects) || objects.length === 0) {
|
|
268
|
+
throw new Error("'objects' (non-empty array) is required");
|
|
269
|
+
}
|
|
270
|
+
const user = (ctx.req && ctx.req.User) ? ctx.req.User : { name: 'anonymous' };
|
|
271
|
+
const size = Math.max(1, parseInt(concurrency, 10) || 5);
|
|
272
|
+
const results = new Array(objects.length);
|
|
273
|
+
const errors = [];
|
|
274
|
+
let cancelled = false;
|
|
275
|
+
|
|
276
|
+
async function saveOne(obj, index){
|
|
277
|
+
if (cancelled) return;
|
|
278
|
+
if (!obj || !obj.itemtype) {
|
|
279
|
+
const errMsg = "Each object must include 'itemtype'";
|
|
280
|
+
errors.push({ index, error: errMsg });
|
|
281
|
+
if (stopOnError) { cancelled = true; }
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
if (!obj.joeUpdated) { obj.joeUpdated = new Date().toISOString(); }
|
|
285
|
+
try {
|
|
286
|
+
if (!obj._id) {
|
|
287
|
+
try { obj._id = (typeof cuid === 'function') ? cuid() : ($c && $c.guid ? $c.guid() : undefined); } catch(_e) {}
|
|
288
|
+
}
|
|
289
|
+
const saved = await new Promise((resolve, reject) => {
|
|
290
|
+
try {
|
|
291
|
+
Storage.save(obj, obj.itemtype, function(err, data){
|
|
292
|
+
if (err) return reject(err);
|
|
293
|
+
resolve(data);
|
|
294
|
+
}, { user, history: true });
|
|
295
|
+
} catch (e) { reject(e); }
|
|
296
|
+
});
|
|
297
|
+
results[index] = sanitizeItems(saved)[0];
|
|
298
|
+
} catch (e) {
|
|
299
|
+
errors.push({ index, error: e && e.message ? e.message : (e+'' ) });
|
|
300
|
+
if (stopOnError) { cancelled = true; }
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// Simple promise pool
|
|
305
|
+
let cursor = 0;
|
|
306
|
+
const runners = new Array(Math.min(size, objects.length)).fill(0).map(async function(){
|
|
307
|
+
while (!cancelled && cursor < objects.length) {
|
|
308
|
+
const idx = cursor++;
|
|
309
|
+
await saveOne(objects[idx], idx);
|
|
310
|
+
if (stopOnError && cancelled) break;
|
|
311
|
+
}
|
|
312
|
+
});
|
|
313
|
+
await Promise.all(runners);
|
|
314
|
+
|
|
315
|
+
const saved = results.filter(function(x){ return !!x; }).length;
|
|
316
|
+
const failed = errors.length;
|
|
317
|
+
return { results: sanitizeItems(results.filter(function(x){ return !!x; })), errors, saved, failed };
|
|
318
|
+
},
|
|
319
|
+
|
|
320
|
+
// Hydration: surface core fields (from file), all schemas, statuses, and tags (no params)
|
|
321
|
+
hydrate: async (_params, _ctx) => {
|
|
322
|
+
let coreDef = (JOE && JOE.Fields && JOE.Fields['core']) || null;
|
|
323
|
+
if (!coreDef) {
|
|
324
|
+
try { coreDef = require(__dirname + '/../fields/core.js'); } catch (_e) { coreDef = {}; }
|
|
325
|
+
}
|
|
326
|
+
const coreFields = Object.keys(coreDef || {}).map(name => ({
|
|
327
|
+
name,
|
|
328
|
+
definition: coreDef[name]
|
|
329
|
+
}));
|
|
330
|
+
|
|
331
|
+
const payload = {
|
|
332
|
+
coreFields,
|
|
333
|
+
schemas: Object.keys(Schemas?.schema || {}),
|
|
334
|
+
schemaSummary: (Schemas && Schemas.summary) || {},
|
|
335
|
+
schemaSummaryGeneratedAt: (Schemas && Schemas.summaryGeneratedAt) || null
|
|
336
|
+
};
|
|
337
|
+
payload.statuses = sanitizeItems(JOE.Data?.status || []);
|
|
338
|
+
payload.tags = sanitizeItems(JOE.Data?.tag || [])
|
|
339
|
+
|
|
340
|
+
return payload;
|
|
341
|
+
},
|
|
342
|
+
|
|
343
|
+
// List app definitions in a sanitized/summarized form
|
|
344
|
+
listApps: async (_params, _ctx) => {
|
|
345
|
+
const apps = (JOE && JOE.Apps && JOE.Apps.cache) || {};
|
|
346
|
+
const names = Object.keys(apps || {});
|
|
347
|
+
const summarized = {};
|
|
348
|
+
names.forEach(function(name){
|
|
349
|
+
try {
|
|
350
|
+
const app = apps[name] || {};
|
|
351
|
+
summarized[name] = {
|
|
352
|
+
title: app.title || name,
|
|
353
|
+
description: app.description || '',
|
|
354
|
+
collections: Array.isArray(app.collections) ? app.collections : [],
|
|
355
|
+
plugins: Array.isArray(app.plugins) ? app.plugins : []
|
|
356
|
+
};
|
|
357
|
+
} catch(_e) {
|
|
358
|
+
summarized[name] = { title: name, description: '' };
|
|
359
|
+
}
|
|
360
|
+
});
|
|
361
|
+
return { names, apps: summarized };
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// 🔧 Add more tools here as needed
|
|
365
|
+
};
|
|
366
|
+
|
|
367
|
+
// ----------------------
|
|
368
|
+
// METADATA FOR TOOLS
|
|
369
|
+
// ----------------------
|
|
370
|
+
// These are used to auto-generate the MCP manifest from the function registry
|
|
371
|
+
MCP.descriptions = {
|
|
372
|
+
listSchemas: "List all available JOE schema names.",
|
|
373
|
+
getSchema: "Retrieve schema by name. Set summaryOnly=true for normalized summary instead of full schema.",
|
|
374
|
+
getSchemas: "Retrieve multiple schemas. With summaryOnly=true, returns summaries; if names omitted, returns all.",
|
|
375
|
+
getObject: "Fetch a single object by _id (schema optional). Supports optional flatten.",
|
|
376
|
+
// getObjectsByIds: "Deprecated - use 'search' with ids.",
|
|
377
|
+
// queryObjects: "Deprecated - use 'search'.",
|
|
378
|
+
// searchCache: "Deprecated - use 'search'.",
|
|
379
|
+
search: "Exact search. Defaults to cache; set source=storage to query DB. Supports sortBy/sortDir, offset/limit paging, withCount, and slim response for {_id,itemtype,name,info,joeUpdated,created}. Use fuzzySearch for typo-tolerant free text.",
|
|
380
|
+
fuzzySearch: "Fuzzy free‑text search. Candidates are prefiltered by 'filters' (e.g., { itemtype: 'user' }) and then scored. If 'fields' are omitted, the schema's searchables (strings) are used with best-field scoring; provide 'fields' (strings or {path,weight}) to use weighted sums. Examples: { q: 'corey hadden', filters: { itemtype: 'user' } } or { q: 'backyard', filters: { itemtype: 'house' } }.",
|
|
381
|
+
saveObject: "Create/update an object; triggers events/history.",
|
|
382
|
+
saveObjects: "Batch save objects with bounded concurrency; per-item history/events preserved.",
|
|
383
|
+
hydrate: "Loads and merges the full JOE context, including core and organization-specific schemas, relationships, universal fields (tags and statuses), and datasets. Returns a single unified object describing the active environment for use by agents, UIs, and plugins.",
|
|
384
|
+
listApps: "List app definitions (title, description, collections, plugins)."
|
|
385
|
+
};
|
|
386
|
+
|
|
387
|
+
MCP.params = {
|
|
388
|
+
listSchemas: {},
|
|
389
|
+
getSchema: {
|
|
390
|
+
type: "object",
|
|
391
|
+
properties: {
|
|
392
|
+
name: { type: "string" },
|
|
393
|
+
summaryOnly: { type: "boolean" }
|
|
394
|
+
},
|
|
395
|
+
required: ["name"]
|
|
396
|
+
},
|
|
397
|
+
getSchemas: {
|
|
398
|
+
type: "object",
|
|
399
|
+
properties: {
|
|
400
|
+
names: { type: "array", items: { type: "string" } },
|
|
401
|
+
summaryOnly: { type: "boolean" }
|
|
402
|
+
},
|
|
403
|
+
required: []
|
|
404
|
+
},
|
|
405
|
+
getObject: {
|
|
406
|
+
type: "object",
|
|
407
|
+
properties: {
|
|
408
|
+
_id: { type: "string" },
|
|
409
|
+
schema: { type: "string" },
|
|
410
|
+
flatten: { type: "boolean" },
|
|
411
|
+
depth: { type: "integer" }
|
|
412
|
+
},
|
|
413
|
+
required: ["_id"]
|
|
414
|
+
},
|
|
415
|
+
// getObjectsByIds: { ...deprecated },
|
|
416
|
+
// queryObjects: { ...deprecated },
|
|
417
|
+
// searchCache: { ...deprecated },
|
|
418
|
+
search: {
|
|
419
|
+
type: "object",
|
|
420
|
+
properties: {
|
|
421
|
+
schema: { type: "string" },
|
|
422
|
+
query: { type: "object" },
|
|
423
|
+
ids: { type: "array", items: { type: "string" } },
|
|
424
|
+
source: { type: "string", enum: ["cache","storage"] },
|
|
425
|
+
limit: { type: "integer" },
|
|
426
|
+
offset: { type: "integer" },
|
|
427
|
+
flatten: { type: "boolean" },
|
|
428
|
+
depth: { type: "integer" },
|
|
429
|
+
countOnly: { type: "boolean" },
|
|
430
|
+
withCount: { type: "boolean" },
|
|
431
|
+
sortBy: { type: "string" },
|
|
432
|
+
sortDir: { type: "string", enum: ["asc","desc"] },
|
|
433
|
+
slim: { type: "boolean" }
|
|
434
|
+
},
|
|
435
|
+
required: []
|
|
436
|
+
},
|
|
437
|
+
fuzzySearch: {
|
|
438
|
+
type: "object",
|
|
439
|
+
properties: {
|
|
440
|
+
schema: { type: "string" },
|
|
441
|
+
q: { type: "string" },
|
|
442
|
+
filters: { type: "object" },
|
|
443
|
+
fields: {
|
|
444
|
+
type: "array",
|
|
445
|
+
items: {
|
|
446
|
+
anyOf: [
|
|
447
|
+
{ type: "string" },
|
|
448
|
+
{ type: "object", properties: { path: { type: "string" }, weight: { type: "number" } }, required: ["path"] }
|
|
449
|
+
]
|
|
450
|
+
}
|
|
451
|
+
},
|
|
452
|
+
threshold: { type: "number" },
|
|
453
|
+
limit: { type: "integer" },
|
|
454
|
+
offset: { type: "integer" },
|
|
455
|
+
highlight: { type: "boolean" },
|
|
456
|
+
minQueryLength: { type: "integer" }
|
|
457
|
+
},
|
|
458
|
+
required: ["q"]
|
|
459
|
+
},
|
|
460
|
+
saveObject: {
|
|
461
|
+
type: "object",
|
|
462
|
+
properties: {
|
|
463
|
+
object: { type: "object" }
|
|
464
|
+
},
|
|
465
|
+
required: ["object"]
|
|
466
|
+
},
|
|
467
|
+
saveObjects: {
|
|
468
|
+
type: "object",
|
|
469
|
+
properties: {
|
|
470
|
+
objects: { type: "array", items: { type: "object" } },
|
|
471
|
+
stopOnError: { type: "boolean" },
|
|
472
|
+
concurrency: { type: "integer" }
|
|
473
|
+
},
|
|
474
|
+
required: ["objects"]
|
|
475
|
+
},
|
|
476
|
+
hydrate: { type: "object", properties: {} }
|
|
477
|
+
,
|
|
478
|
+
listApps: { type: "object", properties: {} }
|
|
479
|
+
};
|
|
480
|
+
|
|
481
|
+
MCP.returns = {
|
|
482
|
+
listSchemas: {
|
|
483
|
+
type: "array",
|
|
484
|
+
items: { type: "string" }
|
|
485
|
+
},
|
|
486
|
+
getSchema: { type: "object" },
|
|
487
|
+
getSchemas: { type: "object" },
|
|
488
|
+
getObject: { type: "object" },
|
|
489
|
+
// getObjectsByIds: { ...deprecated },
|
|
490
|
+
// queryObjects: { ...deprecated },
|
|
491
|
+
// searchCache: { ...deprecated },
|
|
492
|
+
search: {
|
|
493
|
+
type: "object",
|
|
494
|
+
properties: {
|
|
495
|
+
items: { type: "array", items: { type: "object" } }
|
|
496
|
+
}
|
|
497
|
+
},
|
|
498
|
+
fuzzySearch: {
|
|
499
|
+
type: "object",
|
|
500
|
+
properties: {
|
|
501
|
+
items: { type: "array", items: { type: "object" } },
|
|
502
|
+
count: { type: "integer" }
|
|
503
|
+
}
|
|
504
|
+
},
|
|
505
|
+
// When countOnly is true, search returns { count }
|
|
506
|
+
saveObject: { type: "object" },
|
|
507
|
+
saveObjects: {
|
|
508
|
+
type: "object",
|
|
509
|
+
properties: {
|
|
510
|
+
results: { type: "array", items: { type: "object" } },
|
|
511
|
+
errors: { type: "array", items: { type: "object" } },
|
|
512
|
+
saved: { type: "integer" },
|
|
513
|
+
failed: { type: "integer" }
|
|
514
|
+
}
|
|
515
|
+
},
|
|
516
|
+
hydrate: { type: "object" },
|
|
517
|
+
listApps: {
|
|
518
|
+
type: "object",
|
|
519
|
+
properties: {
|
|
520
|
+
names: { type: "array", items: { type: "string" } },
|
|
521
|
+
apps: { type: "object" }
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
};
|
|
525
|
+
|
|
526
|
+
// ----------------------
|
|
527
|
+
// MANIFEST HANDLER
|
|
528
|
+
// ----------------------
|
|
529
|
+
// Responds to GET /.well-known/mcp/manifest.json
|
|
530
|
+
// Returns tool descriptions for agent discovery
|
|
531
|
+
MCP.manifest = async function (req, res) {
|
|
532
|
+
try {
|
|
533
|
+
const toolNames = Object.keys(MCP.tools);
|
|
534
|
+
const tools = toolNames.map(name => ({
|
|
535
|
+
name,
|
|
536
|
+
description: MCP.descriptions[name],
|
|
537
|
+
params: MCP.params[name],
|
|
538
|
+
returns: MCP.returns[name]
|
|
539
|
+
}));
|
|
540
|
+
const joe = {
|
|
541
|
+
name: (JOE && JOE.webconfig && JOE.webconfig.name) || 'JOE',
|
|
542
|
+
version: (JOE && JOE.VERSION) || '',
|
|
543
|
+
hostname: (JOE && JOE.webconfig && JOE.webconfig.hostname) || ''
|
|
544
|
+
};
|
|
545
|
+
const base = req.protocol+':'+(req.get('host')||'');
|
|
546
|
+
const privacyUrl = base+'/'+'privacy';
|
|
547
|
+
const termsUrl = base+'/'+'terms';
|
|
548
|
+
return res.json({ version: "1.0", joe, privacy_policy_url: privacyUrl, terms_of_service_url: termsUrl, tools });
|
|
549
|
+
} catch (e) {
|
|
550
|
+
console.log('[MCP] manifest error:', e);
|
|
551
|
+
return res.status(500).json({ error: e.message || 'manifest error' });
|
|
552
|
+
}
|
|
553
|
+
};
|
|
554
|
+
|
|
555
|
+
// ----------------------
|
|
556
|
+
// JSON-RPC HANDLER
|
|
557
|
+
// ----------------------
|
|
558
|
+
// Responds to POST /mcp with JSON-RPC 2.0 calls
|
|
559
|
+
MCP.rpcHandler = async function (req, res) {
|
|
560
|
+
const { id, method, params } = req.body;
|
|
561
|
+
|
|
562
|
+
// Validate method
|
|
563
|
+
if (!MCP.tools[method]) {
|
|
564
|
+
return res.status(400).json({
|
|
565
|
+
jsonrpc: "2.0",
|
|
566
|
+
id,
|
|
567
|
+
error: { code: -32601, message: "Method not found" }
|
|
568
|
+
});
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
try {
|
|
572
|
+
const result = await MCP.tools[method](params, { req, res });
|
|
573
|
+
return res.json({ jsonrpc: "2.0", id, result });
|
|
574
|
+
} catch (err) {
|
|
575
|
+
console.error(`[MCP] Error in method ${method}:`, err);
|
|
576
|
+
return res.status(500).json({
|
|
577
|
+
jsonrpc: "2.0",
|
|
578
|
+
id,
|
|
579
|
+
error: { code: -32000, message: err.message || "Internal error" }
|
|
580
|
+
});
|
|
581
|
+
}
|
|
582
|
+
};
|
|
583
|
+
|
|
584
|
+
module.exports = MCP;
|
|
585
|
+
|
|
586
|
+
// Optional initializer to attach routes without modifying Server.js
|
|
587
|
+
MCP.init = function initMcpRoutes(){
|
|
588
|
+
try {
|
|
589
|
+
if (!global.JOE || !JOE.Server) return;
|
|
590
|
+
if (JOE._mcpInitialized) return;
|
|
591
|
+
const server = JOE.Server;
|
|
592
|
+
const auth = JOE.auth; // may be undefined for manifest
|
|
593
|
+
server.get('/.well-known/mcp/manifest.json', function(req, res){
|
|
594
|
+
return MCP.manifest(req, res);
|
|
595
|
+
});
|
|
596
|
+
if (auth) {
|
|
597
|
+
server.post('/mcp', auth, function(req, res){ return MCP.rpcHandler(req, res); });
|
|
598
|
+
} else {
|
|
599
|
+
server.post('/mcp', function(req, res){ return MCP.rpcHandler(req, res); });
|
|
600
|
+
}
|
|
601
|
+
JOE._mcpInitialized = true;
|
|
602
|
+
console.log('[MCP] routes attached');
|
|
603
|
+
} catch (e) {
|
|
604
|
+
console.log('[MCP] init error:', e);
|
|
605
|
+
}
|
|
606
|
+
};
|