json-object-editor 0.10.444 → 0.10.503
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 +32 -0
- package/_www/mcp-export.html +186 -0
- package/_www/mcp-test.html +146 -0
- package/package.json +2 -3
- package/readme.md +113 -0
- package/server/init.js +7 -0
- package/server/modules/MCP.js +339 -0
- package/server/modules/Server.js +2 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,37 @@
|
|
|
1
1
|
## CHANGELOG
|
|
2
2
|
|
|
3
|
+
### 0.10.500
|
|
4
|
+
500 - MCP integration (initial)
|
|
5
|
+
- MCP core module with JSON-RPC 2.0 endpoint (/mcp) protected by auth
|
|
6
|
+
- Manifest available at /.well-known/mcp/manifest.json for tool discovery
|
|
7
|
+
- Tools: listSchemas, getSchema, getObject, getObjectsByIds, queryObjects, searchCache, saveObject
|
|
8
|
+
- Tools mapped to real JOE APIs (Schemas, Storage, Cache) with sensitive-field sanitization
|
|
9
|
+
- Read-path prefers Cache for speed; saveObject uses Storage.save (events/history/socket emit)
|
|
10
|
+
- Adopted module init pattern: MCP.init() attaches routes via init.js; removed direct Server.js wiring
|
|
11
|
+
|
|
12
|
+
502 - MCP refinements + tooling
|
|
13
|
+
- Unified search tool; deprecated older object/query tools in manifest
|
|
14
|
+
- getObject flattens by default; optional depth param
|
|
15
|
+
- New hydrate tool returns core fields (from server/fields/core.js), all schemas, statuses, and tags
|
|
16
|
+
- Test pages: mcp-test updated; mcp-export generates Actions/Assistants configs
|
|
17
|
+
- Server serves JOE _www as root fallback so test pages are easy to reach
|
|
18
|
+
- Assistants plugin: support {type:"mcp", url} in tools → imports MCP manifest; dev TLS bypass option; improved error logs
|
|
19
|
+
|
|
20
|
+
503 - Hydrate simplification & manifest instance info
|
|
21
|
+
- Hydrate takes no params; always returns core fields, schemas, statuses, tags
|
|
22
|
+
- Hydrate includes full core field definitions from server/fields/core.js (not just names)
|
|
23
|
+
- Manifest now includes { joe: { name, version, hostname } }; mcp-test shows this
|
|
24
|
+
|
|
25
|
+
### 0.10.500
|
|
26
|
+
500 - MCP integration (initial)
|
|
27
|
+
- MCP core module with JSON-RPC 2.0 endpoint (/mcp) protected by auth
|
|
28
|
+
- Manifest available at /.well-known/mcp/manifest.json for tool discovery
|
|
29
|
+
- Tools: listSchemas, getSchema, getObject, getObjectsByIds, queryObjects, searchCache, saveObject
|
|
30
|
+
- Tools mapped to real JOE APIs (Schemas, Storage, Cache) with sensitive-field sanitization
|
|
31
|
+
- Read-path prefers Cache for speed; saveObject uses Storage.save (events/history/socket emit)
|
|
32
|
+
- Adopted module init pattern: MCP.init() attaches routes via init.js; removed direct Server.js wiring
|
|
33
|
+
- Basics for local testing (auth + example payloads) to be added to README
|
|
34
|
+
|
|
3
35
|
### 0.10.400
|
|
4
36
|
443 - added npm build support
|
|
5
37
|
442 - engagements (alpha)
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html>
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<title>JOE MCP → Assistant Config Export</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
|
+
input,button,textarea{font-size:14px}
|
|
11
|
+
input[type=text]{min-width:320px}
|
|
12
|
+
textarea{width:100%;height:200px;font-family:ui-monospace,Menlo,Consolas,monospace}
|
|
13
|
+
pre{background:#f6f8fa;border:1px solid #e1e4e8;padding:10px;overflow:auto}
|
|
14
|
+
.grid{display:grid;grid-template-columns:1fr;gap:18px}
|
|
15
|
+
.row{display:flex;gap:10px;align-items:center;flex-wrap:wrap}
|
|
16
|
+
.small{font-size:12px;color:#666}
|
|
17
|
+
.good{color:#0a7d00}.bad{color:#b00020}
|
|
18
|
+
code{background:#f6f8fa;padding:2px 4px;border-radius:3px}
|
|
19
|
+
</style>
|
|
20
|
+
</head>
|
|
21
|
+
<body>
|
|
22
|
+
<h1>JOE MCP → Assistant Config Export</h1>
|
|
23
|
+
<div class="small">Generate copy/paste config for Custom GPT Actions (OpenAPI) and Assistants (tools array).</div>
|
|
24
|
+
|
|
25
|
+
<div class="grid">
|
|
26
|
+
<section>
|
|
27
|
+
<h3>1) Server</h3>
|
|
28
|
+
<div class="row">
|
|
29
|
+
<label for="base">Base URL</label>
|
|
30
|
+
<input id="base" type="text" placeholder="https://example.com" />
|
|
31
|
+
</div>
|
|
32
|
+
<div class="row">
|
|
33
|
+
<label for="manifestPath">Manifest path</label>
|
|
34
|
+
<input id="manifestPath" type="text" value="/.well-known/mcp/manifest.json" />
|
|
35
|
+
</div>
|
|
36
|
+
<div class="row">
|
|
37
|
+
<label for="auth">Authorization header (optional)</label>
|
|
38
|
+
<input id="auth" type="text" placeholder="Basic BASE64(user:pass)" />
|
|
39
|
+
</div>
|
|
40
|
+
<div class="row">
|
|
41
|
+
<button id="load">Load manifest</button>
|
|
42
|
+
<span id="status" class="small"></span>
|
|
43
|
+
</div>
|
|
44
|
+
<pre id="manifestOut" style="display:none"></pre>
|
|
45
|
+
</section>
|
|
46
|
+
|
|
47
|
+
<section>
|
|
48
|
+
<h3>2) Custom GPT Actions (OpenAPI 3.1)</h3>
|
|
49
|
+
<div class="small">Paste this schema into GPT Builder → Actions → Import from text.</div>
|
|
50
|
+
<textarea id="openapi" readonly></textarea>
|
|
51
|
+
</section>
|
|
52
|
+
|
|
53
|
+
<section>
|
|
54
|
+
<h3>3) Assistants API tools (functions array)</h3>
|
|
55
|
+
<div class="small">Use this tools JSON in code when creating/updating an Assistant.</div>
|
|
56
|
+
<textarea id="tools" readonly></textarea>
|
|
57
|
+
</section>
|
|
58
|
+
|
|
59
|
+
<section>
|
|
60
|
+
<h3>4) JSON-RPC request template</h3>
|
|
61
|
+
<div class="small">When the Assistant calls a tool, POST this body to <code>/mcp</code>.</div>
|
|
62
|
+
<pre>{
|
|
63
|
+
"jsonrpc": "2.0",
|
|
64
|
+
"id": "<opaque-id>",
|
|
65
|
+
"method": "<toolName>",
|
|
66
|
+
"params": { /* per-tool params */ }
|
|
67
|
+
}</pre>
|
|
68
|
+
</section>
|
|
69
|
+
</div>
|
|
70
|
+
|
|
71
|
+
<script>
|
|
72
|
+
(function(){
|
|
73
|
+
const $ = (id)=>document.getElementById(id);
|
|
74
|
+
const base = $('base');
|
|
75
|
+
const manifestPath = $('manifestPath');
|
|
76
|
+
const auth = $('auth');
|
|
77
|
+
const loadBtn = $('load');
|
|
78
|
+
const status = $('status');
|
|
79
|
+
const openapiEl = $('openapi');
|
|
80
|
+
const toolsEl = $('tools');
|
|
81
|
+
const manifestOut = $('manifestOut');
|
|
82
|
+
|
|
83
|
+
base.value = base.value || (location.origin);
|
|
84
|
+
|
|
85
|
+
function setStatus(msg, ok){
|
|
86
|
+
status.textContent = msg||'';
|
|
87
|
+
status.className = 'small ' + (ok===true?'good': ok===false?'bad':'');
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function buildAssistantsTools(manifest){
|
|
91
|
+
const tools = (manifest.tools||[]).map(t=>({
|
|
92
|
+
type: 'function',
|
|
93
|
+
function: {
|
|
94
|
+
name: t.name,
|
|
95
|
+
description: t.description||'',
|
|
96
|
+
parameters: t.params || { type:'object' }
|
|
97
|
+
}
|
|
98
|
+
}));
|
|
99
|
+
return tools;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function buildOpenAPI(manifest, serverUrl){
|
|
103
|
+
const methodEnum = (manifest.tools||[]).map(t=>t.name);
|
|
104
|
+
const oneOf = (manifest.tools||[]).map(t=>({
|
|
105
|
+
type:'object',
|
|
106
|
+
description: t.description || t.name,
|
|
107
|
+
required: [],
|
|
108
|
+
properties: t.params && t.params.properties ? t.params.properties : {},
|
|
109
|
+
}));
|
|
110
|
+
|
|
111
|
+
const schema = {
|
|
112
|
+
openapi: '3.1.0',
|
|
113
|
+
info: { title: 'JOE MCP Bridge', version: '1.0.0' },
|
|
114
|
+
servers: [{ url: serverUrl.replace(/\/$/,'') }],
|
|
115
|
+
paths: {
|
|
116
|
+
'/mcp': {
|
|
117
|
+
post: {
|
|
118
|
+
operationId: 'mcpCall',
|
|
119
|
+
summary: 'Call a JOE MCP tool',
|
|
120
|
+
description: 'Use one of: ' + methodEnum.join(', '),
|
|
121
|
+
requestBody: {
|
|
122
|
+
required: true,
|
|
123
|
+
content: {
|
|
124
|
+
'application/json': {
|
|
125
|
+
schema: {
|
|
126
|
+
type: 'object',
|
|
127
|
+
required: ['jsonrpc','id','method','params'],
|
|
128
|
+
properties: {
|
|
129
|
+
jsonrpc: { type:'string', const:'2.0' },
|
|
130
|
+
id: { type:'string' },
|
|
131
|
+
method: { type:'string', enum: methodEnum },
|
|
132
|
+
params: { oneOf: oneOf.length? oneOf : [{ type:'object' }] }
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
},
|
|
138
|
+
responses: { '200': { description:'OK' } }
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
};
|
|
143
|
+
return schema;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
async function fetchJSON(url){
|
|
147
|
+
const headers = {};
|
|
148
|
+
if(auth.value){ headers['Authorization'] = auth.value; }
|
|
149
|
+
const res = await fetch(url, { headers });
|
|
150
|
+
if(!res.ok){
|
|
151
|
+
throw new Error('HTTP '+res.status+' '+(await res.text()));
|
|
152
|
+
}
|
|
153
|
+
return res.json();
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
loadBtn.onclick = async function(){
|
|
157
|
+
try{
|
|
158
|
+
setStatus('Loading...', null);
|
|
159
|
+
openapiEl.value = '';
|
|
160
|
+
toolsEl.value = '';
|
|
161
|
+
manifestOut.style.display='none';
|
|
162
|
+
|
|
163
|
+
const url = base.value.replace(/\/$/,'') + manifestPath.value;
|
|
164
|
+
const manifest = await fetchJSON(url);
|
|
165
|
+
manifestOut.style.display='block';
|
|
166
|
+
manifestOut.textContent = JSON.stringify(manifest, null, 2);
|
|
167
|
+
|
|
168
|
+
const tools = buildAssistantsTools(manifest);
|
|
169
|
+
toolsEl.value = JSON.stringify(tools, null, 2);
|
|
170
|
+
|
|
171
|
+
const openapi = buildOpenAPI(manifest, base.value);
|
|
172
|
+
openapiEl.value = JSON.stringify(openapi, null, 2);
|
|
173
|
+
|
|
174
|
+
setStatus('Manifest loaded. Config generated.', true);
|
|
175
|
+
}catch(e){
|
|
176
|
+
setStatus(e.message||String(e), false);
|
|
177
|
+
}
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
// Auto-load
|
|
181
|
+
setTimeout(()=>loadBtn.click(), 50);
|
|
182
|
+
})();
|
|
183
|
+
</script>
|
|
184
|
+
</body>
|
|
185
|
+
</html>
|
|
186
|
+
|
|
@@ -0,0 +1,146 @@
|
|
|
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
|
+
</style>
|
|
18
|
+
</head>
|
|
19
|
+
<body>
|
|
20
|
+
<h1>JOE MCP Test</h1>
|
|
21
|
+
<div class="small">Use this page to discover tools and call the JSON-RPC endpoint.</div>
|
|
22
|
+
<pre id="instanceInfo" class="small">Loading instance info…</pre>
|
|
23
|
+
|
|
24
|
+
<h3>Manifest</h3>
|
|
25
|
+
<div class="row">
|
|
26
|
+
<label for="base">Base URL</label>
|
|
27
|
+
<input id="base" value="" placeholder="http://localhost:{{PORT}}" style="min-width:280px"/>
|
|
28
|
+
<button id="loadManifest">Load manifest</button>
|
|
29
|
+
<span id="status" class="small"></span>
|
|
30
|
+
</div>
|
|
31
|
+
<label for="tool">Tools</label>
|
|
32
|
+
<select id="tool"></select>
|
|
33
|
+
<pre id="toolInfo"></pre>
|
|
34
|
+
|
|
35
|
+
<h3>Call JSON-RPC</h3>
|
|
36
|
+
<label for="params">Params (JSON)</label>
|
|
37
|
+
<textarea id="params">{}</textarea>
|
|
38
|
+
<div class="row">
|
|
39
|
+
<button id="call">POST /mcp</button>
|
|
40
|
+
<span id="callStatus" class="small"></span>
|
|
41
|
+
</div>
|
|
42
|
+
<pre id="result"></pre>
|
|
43
|
+
|
|
44
|
+
<script>
|
|
45
|
+
(function(){
|
|
46
|
+
const $ = (id)=>document.getElementById(id);
|
|
47
|
+
const base = $('base');
|
|
48
|
+
const loadBtn = $('loadManifest');
|
|
49
|
+
const status = $('status');
|
|
50
|
+
const toolSel = $('tool');
|
|
51
|
+
const toolInfo = $('toolInfo');
|
|
52
|
+
const params = $('params');
|
|
53
|
+
const callBtn = $('call');
|
|
54
|
+
const callStatus = $('callStatus');
|
|
55
|
+
const result = $('result');
|
|
56
|
+
|
|
57
|
+
// Try to infer base from window location
|
|
58
|
+
base.value = base.value || (location.origin);
|
|
59
|
+
|
|
60
|
+
let manifest = null;
|
|
61
|
+
let idCounter = 1;
|
|
62
|
+
|
|
63
|
+
function setStatus(el, msg, ok){
|
|
64
|
+
el.textContent = msg || '';
|
|
65
|
+
el.className = 'small ' + (ok===true?'good': ok===false?'bad':'');
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async function fetchJSON(url, opts){
|
|
69
|
+
const res = await fetch(url, opts);
|
|
70
|
+
const ct = res.headers.get('content-type')||'';
|
|
71
|
+
const isJSON = ct.includes('application/json');
|
|
72
|
+
if(!res.ok){
|
|
73
|
+
let detail = isJSON ? await res.json().catch(()=>({})) : await res.text();
|
|
74
|
+
throw new Error('HTTP '+res.status+': '+(isJSON?JSON.stringify(detail):detail));
|
|
75
|
+
}
|
|
76
|
+
return isJSON ? res.json() : res.text();
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
loadBtn.onclick = async function(){
|
|
80
|
+
setStatus(status, 'Loading...', null);
|
|
81
|
+
toolSel.innerHTML = '';
|
|
82
|
+
toolInfo.textContent='';
|
|
83
|
+
try{
|
|
84
|
+
const url = base.value.replace(/\/$/,'') + '/.well-known/mcp/manifest.json';
|
|
85
|
+
manifest = await fetchJSON(url);
|
|
86
|
+
// Instance info
|
|
87
|
+
if (manifest.joe){
|
|
88
|
+
$('instanceInfo').textContent = `Name: ${manifest.joe.name} | Version: ${manifest.joe.version} | Host: ${manifest.joe.hostname}`;
|
|
89
|
+
}
|
|
90
|
+
(manifest.tools||[]).forEach(t=>{
|
|
91
|
+
const opt=document.createElement('option');
|
|
92
|
+
opt.value=t.name; opt.textContent=t.name;
|
|
93
|
+
toolSel.appendChild(opt);
|
|
94
|
+
});
|
|
95
|
+
if((manifest.tools||[]).length){
|
|
96
|
+
toolSel.selectedIndex=0; renderToolInfo();
|
|
97
|
+
}
|
|
98
|
+
setStatus(status, 'Manifest loaded', true);
|
|
99
|
+
}catch(e){
|
|
100
|
+
setStatus(status, e.message||String(e), false);
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
function renderToolInfo(){
|
|
105
|
+
const name = toolSel.value;
|
|
106
|
+
const tool = (manifest.tools||[]).find(t=>t.name===name);
|
|
107
|
+
toolInfo.textContent = tool ? JSON.stringify(tool, null, 2) : '';
|
|
108
|
+
// Prefill common params for convenience
|
|
109
|
+
if(tool && tool.params){
|
|
110
|
+
params.value = JSON.stringify(Object.fromEntries(Object.keys(tool.params.properties||{}).map(k=>[k, null])), null, 2);
|
|
111
|
+
}
|
|
112
|
+
// Hydrate presets UI
|
|
113
|
+
if (name === 'hydrate') {
|
|
114
|
+
params.value = JSON.stringify({}, null, 2);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
toolSel.onchange = renderToolInfo;
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
callBtn.onclick = async function(){
|
|
121
|
+
setStatus(callStatus, 'Calling...', null);
|
|
122
|
+
result.textContent='';
|
|
123
|
+
try{
|
|
124
|
+
const url = base.value.replace(/\/$/,'') + '/mcp';
|
|
125
|
+
let p = {};
|
|
126
|
+
try{ p = params.value ? JSON.parse(params.value) : {}; }catch(e){ throw new Error('Invalid JSON in params'); }
|
|
127
|
+
const body = {
|
|
128
|
+
jsonrpc: '2.0',
|
|
129
|
+
id: String(idCounter++),
|
|
130
|
+
method: toolSel.value,
|
|
131
|
+
params: p
|
|
132
|
+
};
|
|
133
|
+
const resp = await fetchJSON(url, { method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify(body)});
|
|
134
|
+
result.textContent = JSON.stringify(resp, null, 2);
|
|
135
|
+
setStatus(callStatus, 'OK', true);
|
|
136
|
+
}catch(e){
|
|
137
|
+
setStatus(callStatus, e.message||String(e), false);
|
|
138
|
+
}
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
// Auto-load manifest on open
|
|
142
|
+
setTimeout(()=>loadBtn.click(), 50);
|
|
143
|
+
})();
|
|
144
|
+
</script>
|
|
145
|
+
</body>
|
|
146
|
+
</html>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "json-object-editor",
|
|
3
|
-
"version": "0.10.
|
|
3
|
+
"version": "0.10.503",
|
|
4
4
|
"description": "JOE the Json Object Editor | Platform Edition",
|
|
5
5
|
"main": "app.js",
|
|
6
6
|
"scripts": {
|
|
@@ -38,14 +38,13 @@
|
|
|
38
38
|
"craydent": "^0.8.9",
|
|
39
39
|
"express": "^4.16.4",
|
|
40
40
|
"googleapis": "^149.0.0",
|
|
41
|
-
"got": "^12.6.0",
|
|
42
41
|
"jwt-decode": "^2.2.0",
|
|
43
42
|
"mailgun": "^0.5.0",
|
|
44
43
|
"mongojs": "^2.3.0",
|
|
45
44
|
"mysql": "^2.16.0",
|
|
46
45
|
"nodemailer": "^2.7.2",
|
|
47
46
|
"nodemailer-ses-transport": "^1.4.0",
|
|
48
|
-
"openai": "^5.
|
|
47
|
+
"openai": "^5.23.2",
|
|
49
48
|
"opener": "^1.4.3",
|
|
50
49
|
"pem": "^1.13.2",
|
|
51
50
|
"plaid": "^13.1.0",
|
package/readme.md
CHANGED
|
@@ -5,6 +5,119 @@ JOE is software that allows you to manage data models via JSON objects. There ar
|
|
|
5
5
|
|
|
6
6
|
|
|
7
7
|
|
|
8
|
+
## Architecture & Mental Model (Server)
|
|
9
|
+
|
|
10
|
+
- Global JOE
|
|
11
|
+
- Created in `server/init.js`; exposes subsystems like `JOE.Utils`, `JOE.Schemas`, `JOE.Storage`, `JOE.Mongo`/`JOE.MySQL`, `JOE.Cache`, `JOE.Apps`, `JOE.Server` (Express), `JOE.Sites`, `JOE.io` (Socket), and `JOE.auth`.
|
|
12
|
+
- Init pattern
|
|
13
|
+
- Modules can optionally export `init()`; `init.js` loads/watches modules and calls `init()` after `JOE.Server` is ready. This enables hot-reload and keeps `Server.js` thin.
|
|
14
|
+
- Schemas & events
|
|
15
|
+
- `JOE.Schemas` loads from `server/schemas/` and app schema dir into `JOE.Schemas.schema` with names in `JOE.Schemas.schemaList`. Raw copies live in `JOE.Schemas.raw_schemas` for event hooks.
|
|
16
|
+
- `JOE.Storage.save()` triggers schema events (`create`, `save`, `status`, `delete`) via `JOE.Schemas.events()` and writes history documents to `_history`.
|
|
17
|
+
- Storage & cache
|
|
18
|
+
- `JOE.Storage.load(collection, query, cb)` chooses backend per schema `storage.type` (`mongo`, `mysql`, `file`, `api`), with Mongo/file fallback.
|
|
19
|
+
- `JOE.Storage.save(item, collection, cb, { user, history })` emits `item_updated` to sockets, records history, and fires events.
|
|
20
|
+
- `JOE.Cache.update(cb, collections)` populates `JOE.Data`, flattens a `list`, and builds `lookup`. Use `JOE.Cache.findByID(collection, idOrCsv)` for fast lookups; `JOE.Cache.search(query)` for in-memory filtering.
|
|
21
|
+
- Auth
|
|
22
|
+
- `JOE.auth` middleware checks cookie token or Basic Auth; many API routes (and `/mcp`) are protected.
|
|
23
|
+
- Shorthand `$J`
|
|
24
|
+
- Server and client convenience: `$J.get(_id)`, `$J.search(query)`, `$J.schema(name)`.
|
|
25
|
+
- On server, provided by `server/modules/UniversalShorthand.js` and assigned to `global.$J` in `init.js`.
|
|
26
|
+
- MCP overview
|
|
27
|
+
- Manifest: `/.well-known/mcp/manifest.json`; JSON-RPC: `POST /mcp` (auth-protected). Tools map to real JOE APIs (`Schemas`, `Storage`, `Cache`) and sanitize sensitive fields.
|
|
28
|
+
|
|
29
|
+
## MCP routes and local testing
|
|
30
|
+
|
|
31
|
+
- Endpoints
|
|
32
|
+
- Manifest (public): `GET /.well-known/mcp/manifest.json`
|
|
33
|
+
- JSON-RPC (auth): `POST /mcp`
|
|
34
|
+
|
|
35
|
+
- Auth
|
|
36
|
+
- If users exist, `POST /mcp` requires cookie or Basic Auth (same as other APIs). If no users configured, it is effectively open.
|
|
37
|
+
|
|
38
|
+
- Test page
|
|
39
|
+
- JOE ships a simple tester at `/_www/mcp-test.html` inside the package.
|
|
40
|
+
- Access via JOE path: `http://localhost:<PORT>/JsonObjectEditor/_www/mcp-test.html`
|
|
41
|
+
- If your host app serves its own `_www`, the tester can also be available at the root (fallback) if running with the updated server that mounts JOE’s `_www` as a secondary static directory. Then: `http://localhost:<PORT>/mcp-test.html`
|
|
42
|
+
|
|
43
|
+
- Tools
|
|
44
|
+
- `listSchemas(name?)`, `getSchema(name)`
|
|
45
|
+
- `getObject(_id, schema?)` (flattens by default; supports `depth` override)
|
|
46
|
+
- `search` (preferred): unified tool for cache and storage
|
|
47
|
+
- Params: `{ schema?, query?, ids?, source?: 'cache'|'storage', limit?, flatten?, depth? }`
|
|
48
|
+
- Defaults to cache across all collections; add `schema` to filter; set `source:"storage"` to query a specific schema in the DB.
|
|
49
|
+
- `saveObject({ object })`
|
|
50
|
+
|
|
51
|
+
- Quick tests (PowerShell)
|
|
52
|
+
- Prepare headers if using Basic Auth:
|
|
53
|
+
```powershell
|
|
54
|
+
$pair = "user:pass"; $b64 = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes($pair))
|
|
55
|
+
$h = @{ Authorization = "Basic $b64"; "Content-Type" = "application/json" }
|
|
56
|
+
$base = "http://localhost:<PORT>"
|
|
57
|
+
```
|
|
58
|
+
- Manifest:
|
|
59
|
+
```powershell
|
|
60
|
+
Invoke-RestMethod "$base/.well-known/mcp/manifest.json"
|
|
61
|
+
```
|
|
62
|
+
- listSchemas:
|
|
63
|
+
```powershell
|
|
64
|
+
$body = @{ jsonrpc="2.0"; id="1"; method="listSchemas"; params=@{} } | ConvertTo-Json -Depth 6
|
|
65
|
+
Invoke-RestMethod -Method Post -Uri "$base/mcp" -Headers $h -Body $body
|
|
66
|
+
```
|
|
67
|
+
- getSchema:
|
|
68
|
+
```powershell
|
|
69
|
+
$body = @{ jsonrpc="2.0"; id="2"; method="getSchema"; params=@{ name="<schemaName>" } } | ConvertTo-Json -Depth 6
|
|
70
|
+
Invoke-RestMethod -Method Post -Uri "$base/mcp" -Headers $h -Body $body
|
|
71
|
+
```
|
|
72
|
+
- search (cache default):
|
|
73
|
+
```powershell
|
|
74
|
+
$body = @{ jsonrpc="2.0"; id="3"; method="search"; params=@{ query=@{ itemtype="<schemaName>" }; limit=10 } } | ConvertTo-Json -Depth 10
|
|
75
|
+
Invoke-RestMethod -Method Post -Uri "$base/mcp" -Headers $h -Body $body
|
|
76
|
+
```
|
|
77
|
+
- search (storage):
|
|
78
|
+
```powershell
|
|
79
|
+
$body = @{ jsonrpc="2.0"; id="4"; method="search"; params=@{ schema="<schemaName>"; source="storage"; query=@{ }; limit=10 } } | ConvertTo-Json -Depth 10
|
|
80
|
+
Invoke-RestMethod -Method Post -Uri "$base/mcp" -Headers $h -Body $body
|
|
81
|
+
```
|
|
82
|
+
- search (ids + flatten):
|
|
83
|
+
```powershell
|
|
84
|
+
$body = @{ jsonrpc="2.0"; id="5"; method="search"; params=@{ schema="<schemaName>"; ids=@("<id1>","<id2>"); flatten=$true; depth=2 } } | ConvertTo-Json -Depth 10
|
|
85
|
+
Invoke-RestMethod -Method Post -Uri "$base/mcp" -Headers $h -Body $body
|
|
86
|
+
```
|
|
87
|
+
- saveObject:
|
|
88
|
+
```powershell
|
|
89
|
+
$object = @{ itemtype="<schemaName>"; name="Test via MCP" }
|
|
90
|
+
$body = @{ jsonrpc="2.0"; id="4"; method="saveObject"; params=@{ object=$object } } | ConvertTo-Json -Depth 10
|
|
91
|
+
Invoke-RestMethod -Method Post -Uri "$base/mcp" -Headers $h -Body $body
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
- Quick tests (curl)
|
|
95
|
+
- Manifest:
|
|
96
|
+
```bash
|
|
97
|
+
curl -s http://localhost:<PORT>/.well-known/mcp/manifest.json | jq
|
|
98
|
+
```
|
|
99
|
+
- listSchemas:
|
|
100
|
+
- search (cache):
|
|
101
|
+
```bash
|
|
102
|
+
curl -s -X POST http://localhost:<PORT>/mcp \
|
|
103
|
+
-H 'Content-Type: application/json' \
|
|
104
|
+
-d '{"jsonrpc":"2.0","id":"3","method":"search","params":{"query":{"itemtype":"<schemaName>"},"limit":10}}' | jq
|
|
105
|
+
```
|
|
106
|
+
- search (storage):
|
|
107
|
+
```bash
|
|
108
|
+
curl -s -X POST http://localhost:<PORT>/mcp \
|
|
109
|
+
-H 'Content-Type: application/json' \
|
|
110
|
+
-d '{"jsonrpc":"2.0","id":"4","method":"search","params":{"schema":"<schemaName>","source":"storage","query":{},"limit":10}}' | jq
|
|
111
|
+
```
|
|
112
|
+
```bash
|
|
113
|
+
curl -s -X POST http://localhost:<PORT>/mcp \
|
|
114
|
+
-H 'Content-Type: application/json' \
|
|
115
|
+
-d '{"jsonrpc":"2.0","id":"1","method":"listSchemas","params":{}}' | jq
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
- Troubleshooting
|
|
119
|
+
- If you see a payload like `{ originalURL: "/...", site: "no site found" }`, the request hit the Sites catch-all. Ensure MCP routes are initialized before Sites (handled by default in `server/init.js` via `MCP.init()`), and use the correct URL: `/.well-known/mcp/manifest.json` or `/mcp`.
|
|
120
|
+
|
|
8
121
|
## SERVER/PLATFORM mode
|
|
9
122
|
check port 2099
|
|
10
123
|
/JOE/
|
package/server/init.js
CHANGED
|
@@ -97,6 +97,13 @@ var app = function(config){
|
|
|
97
97
|
global.$J = require('./modules/UniversalShorthand.js');
|
|
98
98
|
function initServers(){
|
|
99
99
|
JOE.Server = require('./modules/Server.js');
|
|
100
|
+
// Attach MCP routes before Sites catch-all
|
|
101
|
+
try {
|
|
102
|
+
const MCP = require('./modules/MCP.js');
|
|
103
|
+
if (MCP && MCP.init) MCP.init();
|
|
104
|
+
} catch (e) {
|
|
105
|
+
console.log('[MCP] attach error:', e && e.message);
|
|
106
|
+
}
|
|
100
107
|
|
|
101
108
|
JOE.Sites = require('./modules/Sites.js');
|
|
102
109
|
JOE.io = require('./modules/Socket.js');
|
|
@@ -0,0 +1,339 @@
|
|
|
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
|
+
// TOOL DEFINITIONS
|
|
43
|
+
// ----------------------
|
|
44
|
+
// This object maps tool names to actual execution functions.
|
|
45
|
+
// Each function takes a `params` object and returns a JSON-serializable result.
|
|
46
|
+
MCP.tools = {
|
|
47
|
+
|
|
48
|
+
// List all schema names in the system
|
|
49
|
+
listSchemas: async (_params, _ctx) => {
|
|
50
|
+
const list = (Schemas && (
|
|
51
|
+
(Schemas.schemaList && Schemas.schemaList.length && Schemas.schemaList) ||
|
|
52
|
+
(Schemas.schema && Object.keys(Schemas.schema))
|
|
53
|
+
)) || [];
|
|
54
|
+
return list.slice().sort();
|
|
55
|
+
},
|
|
56
|
+
|
|
57
|
+
// Get full schema definition by name
|
|
58
|
+
getSchema: async ({ name }, _ctx) => {
|
|
59
|
+
if (!name) throw new Error("Missing required param 'name'");
|
|
60
|
+
const def = Schemas.schema && Schemas.schema[name];
|
|
61
|
+
if (!def) throw new Error(`Schema "${name}" not found`);
|
|
62
|
+
return def;
|
|
63
|
+
},
|
|
64
|
+
|
|
65
|
+
// Convenience: fetch a single object by _id (schema optional). Prefer cache; fallback to storage.
|
|
66
|
+
getObject: async ({ _id, schema, flatten = true, depth = 1 }, _ctx) => {
|
|
67
|
+
if (!_id) throw new Error("Missing required param '_id'");
|
|
68
|
+
// Fast path via global lookup
|
|
69
|
+
let obj = (JOE && JOE.Cache && JOE.Cache.findByID) ? (JOE.Cache.findByID(_id) || null) : null;
|
|
70
|
+
if (!obj && schema) {
|
|
71
|
+
const results = await loadFromStorage(schema, { _id });
|
|
72
|
+
obj = (results && results[0]) || null;
|
|
73
|
+
}
|
|
74
|
+
if (!obj && schema && JOE && JOE.Cache && JOE.Cache.findByID) {
|
|
75
|
+
obj = JOE.Cache.findByID(schema, _id) || null;
|
|
76
|
+
}
|
|
77
|
+
if (!obj) throw new Error(`Object not found${schema?(' in '+schema):''} with _id: ${_id}`);
|
|
78
|
+
if (flatten && JOE && JOE.Utils && JOE.Utils.flattenObject) {
|
|
79
|
+
try { return sanitizeItems(JOE.Utils.flattenObject(_id, { recursive: true, depth }))[0]; } catch(e) {}
|
|
80
|
+
}
|
|
81
|
+
return sanitizeItems(obj)[0];
|
|
82
|
+
},
|
|
83
|
+
|
|
84
|
+
/* Deprecated: use unified 'search' instead
|
|
85
|
+
getObjectsByIds: async () => { throw new Error('Use search with ids instead'); },
|
|
86
|
+
queryObjects: async () => { throw new Error('Use search instead'); },
|
|
87
|
+
*/
|
|
88
|
+
|
|
89
|
+
/* Deprecated: use unified 'search' instead
|
|
90
|
+
searchCache: async () => { throw new Error('Use search instead'); },
|
|
91
|
+
*/
|
|
92
|
+
|
|
93
|
+
// Unified search: defaults to cache; set source="storage" to query DB for a schema
|
|
94
|
+
search: async ({ schema, query = {}, ids, source = 'cache', limit = 50, flatten = false, depth = 1 }, _ctx) => {
|
|
95
|
+
const useCache = !source || source === 'cache';
|
|
96
|
+
const useStorage = source === 'storage';
|
|
97
|
+
|
|
98
|
+
if (ids && !Array.isArray(ids)) throw new Error("'ids' must be an array if provided");
|
|
99
|
+
|
|
100
|
+
// When ids are provided and a schema is known, prefer cache for safety/speed
|
|
101
|
+
if (Array.isArray(ids) && schema) {
|
|
102
|
+
let items = [];
|
|
103
|
+
if (JOE && JOE.Cache && JOE.Cache.findByID) {
|
|
104
|
+
const found = JOE.Cache.findByID(schema, ids.join(',')) || [];
|
|
105
|
+
items = Array.isArray(found) ? found : (found ? [found] : []);
|
|
106
|
+
}
|
|
107
|
+
if (useStorage && (!items || items.length === 0)) {
|
|
108
|
+
try {
|
|
109
|
+
const fromStorage = await loadFromStorage(schema, { _id: { $in: ids } });
|
|
110
|
+
items = fromStorage || [];
|
|
111
|
+
} catch (e) { /* ignore storage errors here */ }
|
|
112
|
+
}
|
|
113
|
+
if (flatten && JOE && JOE.Utils && JOE.Utils.flattenObject) {
|
|
114
|
+
try { items = ids.map(id => JOE.Utils.flattenObject(id, { recursive: true, depth })); } catch (e) {}
|
|
115
|
+
}
|
|
116
|
+
const sliced = (typeof limit === 'number' && limit > 0) ? items.slice(0, limit) : items;
|
|
117
|
+
return { items: sanitizeItems(sliced) };
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// No ids: choose source
|
|
121
|
+
if (useCache) {
|
|
122
|
+
if (!JOE || !JOE.Cache || !JOE.Cache.search) throw new Error('Cache not initialized');
|
|
123
|
+
let results = JOE.Cache.search(query || {});
|
|
124
|
+
if (schema) results = (results || []).filter(i => i && i.itemtype === schema);
|
|
125
|
+
const sliced = (typeof limit === 'number' && limit > 0) ? results.slice(0, limit) : results;
|
|
126
|
+
return { items: sanitizeItems(sliced) };
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (useStorage) {
|
|
130
|
+
if (!schema) throw new Error("'schema' is required when source=storage");
|
|
131
|
+
const results = await loadFromStorage(schema, query || {});
|
|
132
|
+
const sliced = (typeof limit === 'number' && limit > 0) ? (results || []).slice(0, limit) : (results || []);
|
|
133
|
+
if (flatten && JOE && JOE.Utils && JOE.Utils.flattenObject) {
|
|
134
|
+
try { return { items: sanitizeItems(sliced.map(it => JOE.Utils.flattenObject(it && it._id, { recursive: true, depth }))) }; } catch (e) {}
|
|
135
|
+
}
|
|
136
|
+
return { items: sanitizeItems(sliced) };
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
throw new Error("Invalid 'source'. Use 'cache' (default) or 'storage'.");
|
|
140
|
+
},
|
|
141
|
+
|
|
142
|
+
// Save an object via Storage (respects events/history)
|
|
143
|
+
saveObject: async ({ object }, ctx = {}) => {
|
|
144
|
+
if (!object || !object.itemtype) throw new Error("'object' with 'itemtype' is required");
|
|
145
|
+
const user = (ctx.req && ctx.req.User) ? ctx.req.User : { name: 'anonymous' };
|
|
146
|
+
const saved = await new Promise((resolve, reject) => {
|
|
147
|
+
try {
|
|
148
|
+
Storage.save(object, object.itemtype, function(err, data){
|
|
149
|
+
if (err) return reject(err);
|
|
150
|
+
resolve(data);
|
|
151
|
+
}, { user });
|
|
152
|
+
} catch (e) { reject(e); }
|
|
153
|
+
});
|
|
154
|
+
return sanitizeItems(saved)[0];
|
|
155
|
+
},
|
|
156
|
+
|
|
157
|
+
// Hydration: surface core fields (from file), all schemas, statuses, and tags (no params)
|
|
158
|
+
hydrate: async (_params, _ctx) => {
|
|
159
|
+
let coreDef = (JOE && JOE.Fields && JOE.Fields['core']) || null;
|
|
160
|
+
if (!coreDef) {
|
|
161
|
+
try { coreDef = require(__dirname + '/../fields/core.js'); } catch (_e) { coreDef = {}; }
|
|
162
|
+
}
|
|
163
|
+
const coreFields = Object.keys(coreDef || {}).map(name => ({
|
|
164
|
+
name,
|
|
165
|
+
definition: coreDef[name]
|
|
166
|
+
}));
|
|
167
|
+
|
|
168
|
+
const payload = {
|
|
169
|
+
coreFields,
|
|
170
|
+
schemas: Object.keys(Schemas?.schema || {})
|
|
171
|
+
};
|
|
172
|
+
payload.statuses = sanitizeItems(JOE.Data?.status || []);
|
|
173
|
+
payload.tags = sanitizeItems(JOE.Data?.tag || [])
|
|
174
|
+
|
|
175
|
+
return payload;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// 🔧 Add more tools here as needed
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
// ----------------------
|
|
182
|
+
// METADATA FOR TOOLS
|
|
183
|
+
// ----------------------
|
|
184
|
+
// These are used to auto-generate the MCP manifest from the function registry
|
|
185
|
+
MCP.descriptions = {
|
|
186
|
+
listSchemas: "List all available JOE schema names.",
|
|
187
|
+
getSchema: "Retrieve a full schema definition by name.",
|
|
188
|
+
getObject: "Fetch a single object by _id (schema optional). Flattens by default.",
|
|
189
|
+
// getObjectsByIds: "Deprecated - use 'search' with ids.",
|
|
190
|
+
// queryObjects: "Deprecated - use 'search'.",
|
|
191
|
+
// searchCache: "Deprecated - use 'search'.",
|
|
192
|
+
search: "Unified search. Defaults to cache; set source=storage to query DB.",
|
|
193
|
+
saveObject: "Create/update an object; triggers events/history.",
|
|
194
|
+
hydrate: "Describe core fields, statuses, tags, and inferred field shapes for an optional schema."
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
MCP.params = {
|
|
198
|
+
listSchemas: {},
|
|
199
|
+
getSchema: {
|
|
200
|
+
type: "object",
|
|
201
|
+
properties: {
|
|
202
|
+
name: { type: "string" }
|
|
203
|
+
},
|
|
204
|
+
required: ["name"]
|
|
205
|
+
},
|
|
206
|
+
getObject: {
|
|
207
|
+
type: "object",
|
|
208
|
+
properties: {
|
|
209
|
+
_id: { type: "string" },
|
|
210
|
+
schema: { type: "string" },
|
|
211
|
+
flatten: { type: "boolean" },
|
|
212
|
+
depth: { type: "integer" }
|
|
213
|
+
},
|
|
214
|
+
required: ["_id"]
|
|
215
|
+
},
|
|
216
|
+
// getObjectsByIds: { ...deprecated },
|
|
217
|
+
// queryObjects: { ...deprecated },
|
|
218
|
+
// searchCache: { ...deprecated },
|
|
219
|
+
search: {
|
|
220
|
+
type: "object",
|
|
221
|
+
properties: {
|
|
222
|
+
schema: { type: "string" },
|
|
223
|
+
query: { type: "object" },
|
|
224
|
+
ids: { type: "array", items: { type: "string" } },
|
|
225
|
+
source: { type: "string", enum: ["cache","storage"] },
|
|
226
|
+
limit: { type: "integer" },
|
|
227
|
+
flatten: { type: "boolean" },
|
|
228
|
+
depth: { type: "integer" }
|
|
229
|
+
},
|
|
230
|
+
required: []
|
|
231
|
+
},
|
|
232
|
+
saveObject: {
|
|
233
|
+
type: "object",
|
|
234
|
+
properties: {
|
|
235
|
+
object: { type: "object" }
|
|
236
|
+
},
|
|
237
|
+
required: ["object"]
|
|
238
|
+
},
|
|
239
|
+
hydrate: { type: "object", properties: {} }
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
MCP.returns = {
|
|
243
|
+
listSchemas: {
|
|
244
|
+
type: "array",
|
|
245
|
+
items: { type: "string" }
|
|
246
|
+
},
|
|
247
|
+
getSchema: { type: "object" },
|
|
248
|
+
getObject: { type: "object" },
|
|
249
|
+
// getObjectsByIds: { ...deprecated },
|
|
250
|
+
// queryObjects: { ...deprecated },
|
|
251
|
+
// searchCache: { ...deprecated },
|
|
252
|
+
search: {
|
|
253
|
+
type: "object",
|
|
254
|
+
properties: {
|
|
255
|
+
items: { type: "array", items: { type: "object" } }
|
|
256
|
+
}
|
|
257
|
+
},
|
|
258
|
+
saveObject: { type: "object" },
|
|
259
|
+
hydrate: { type: "object" }
|
|
260
|
+
};
|
|
261
|
+
|
|
262
|
+
// ----------------------
|
|
263
|
+
// MANIFEST HANDLER
|
|
264
|
+
// ----------------------
|
|
265
|
+
// Responds to GET /.well-known/mcp/manifest.json
|
|
266
|
+
// Returns tool descriptions for agent discovery
|
|
267
|
+
MCP.manifest = async function (req, res) {
|
|
268
|
+
try {
|
|
269
|
+
const toolNames = Object.keys(MCP.tools);
|
|
270
|
+
const tools = toolNames.map(name => ({
|
|
271
|
+
name,
|
|
272
|
+
description: MCP.descriptions[name],
|
|
273
|
+
params: MCP.params[name],
|
|
274
|
+
returns: MCP.returns[name]
|
|
275
|
+
}));
|
|
276
|
+
const joe = {
|
|
277
|
+
name: (JOE && JOE.webconfig && JOE.webconfig.name) || 'JOE',
|
|
278
|
+
version: (JOE && JOE.VERSION) || '',
|
|
279
|
+
hostname: (JOE && JOE.webconfig && JOE.webconfig.hostname) || ''
|
|
280
|
+
};
|
|
281
|
+
return res.json({ version: "1.0", joe, tools });
|
|
282
|
+
} catch (e) {
|
|
283
|
+
console.log('[MCP] manifest error:', e);
|
|
284
|
+
return res.status(500).json({ error: e.message || 'manifest error' });
|
|
285
|
+
}
|
|
286
|
+
};
|
|
287
|
+
|
|
288
|
+
// ----------------------
|
|
289
|
+
// JSON-RPC HANDLER
|
|
290
|
+
// ----------------------
|
|
291
|
+
// Responds to POST /mcp with JSON-RPC 2.0 calls
|
|
292
|
+
MCP.rpcHandler = async function (req, res) {
|
|
293
|
+
const { id, method, params } = req.body;
|
|
294
|
+
|
|
295
|
+
// Validate method
|
|
296
|
+
if (!MCP.tools[method]) {
|
|
297
|
+
return res.status(400).json({
|
|
298
|
+
jsonrpc: "2.0",
|
|
299
|
+
id,
|
|
300
|
+
error: { code: -32601, message: "Method not found" }
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
try {
|
|
305
|
+
const result = await MCP.tools[method](params, { req, res });
|
|
306
|
+
return res.json({ jsonrpc: "2.0", id, result });
|
|
307
|
+
} catch (err) {
|
|
308
|
+
console.error(`[MCP] Error in method ${method}:`, err);
|
|
309
|
+
return res.status(500).json({
|
|
310
|
+
jsonrpc: "2.0",
|
|
311
|
+
id,
|
|
312
|
+
error: { code: -32000, message: err.message || "Internal error" }
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
};
|
|
316
|
+
|
|
317
|
+
module.exports = MCP;
|
|
318
|
+
|
|
319
|
+
// Optional initializer to attach routes without modifying Server.js
|
|
320
|
+
MCP.init = function initMcpRoutes(){
|
|
321
|
+
try {
|
|
322
|
+
if (!global.JOE || !JOE.Server) return;
|
|
323
|
+
if (JOE._mcpInitialized) return;
|
|
324
|
+
const server = JOE.Server;
|
|
325
|
+
const auth = JOE.auth; // may be undefined for manifest
|
|
326
|
+
server.get('/.well-known/mcp/manifest.json', function(req, res){
|
|
327
|
+
return MCP.manifest(req, res);
|
|
328
|
+
});
|
|
329
|
+
if (auth) {
|
|
330
|
+
server.post('/mcp', auth, function(req, res){ return MCP.rpcHandler(req, res); });
|
|
331
|
+
} else {
|
|
332
|
+
server.post('/mcp', function(req, res){ return MCP.rpcHandler(req, res); });
|
|
333
|
+
}
|
|
334
|
+
JOE._mcpInitialized = true;
|
|
335
|
+
console.log('[MCP] routes attached');
|
|
336
|
+
} catch (e) {
|
|
337
|
+
console.log('[MCP] init error:', e);
|
|
338
|
+
}
|
|
339
|
+
};
|
package/server/modules/Server.js
CHANGED
|
@@ -106,7 +106,6 @@ server.use(function(req, res, next) {
|
|
|
106
106
|
});
|
|
107
107
|
server.use(JOE.webconfig.joepath,express.static(JOE.joedir));
|
|
108
108
|
|
|
109
|
-
|
|
110
109
|
//USER
|
|
111
110
|
server.get(['/API/user/:method'],auth,function(req,res,next){
|
|
112
111
|
var users = (JOE.Data && JOE.Data.user) || [];
|
|
@@ -809,6 +808,8 @@ JOE.Utils.setupFileFolder(webDir,'web');
|
|
|
809
808
|
|
|
810
809
|
|
|
811
810
|
server.use('/',express.static(JOE.webDir));
|
|
811
|
+
// Also serve JOE's own _www as a fallback under root (parent app takes priority)
|
|
812
|
+
server.use('/',express.static(path.join(JOE.joedir, JOE.webconfig.webDir)));
|
|
812
813
|
|
|
813
814
|
http.listen(JOE.webconfig.port,function(){
|
|
814
815
|
//console.log('joe listening on '+JOE.webconfig.port);
|