PluginProbe ʕ •ᴥ•ʔ
AI Engine – The Chatbot, AI Framework & MCP for WordPress / 2.8.2
AI Engine – The Chatbot, AI Framework & MCP for WordPress v2.8.2
3.5.9 3.5.8 3.5.7 3.5.6 3.5.5 3.5.4 3.5.3 3.5.2 3.5.1 3.5.0 3.4.9 3.4.8 3.4.7 0.2.1 1.6.91 0.2.2 1.6.92 0.2.3 1.6.93 0.2.4 1.6.94 0.2.5 1.6.95 0.2.6 1.6.96 0.2.7 1.6.97 0.2.8 1.6.98 0.2.9 1.6.99 0.3.0 1.7.0 0.3.1 1.7.1 0.3.2 1.7.2 0.3.3 1.7.3 0.3.4 1.7.4 0.3.5 1.7.5 0.3.6 1.7.6 0.4.0 1.7.7 0.4.1 1.7.8 0.4.2 1.7.9 0.4.3 1.8.0 0.4.4 1.8.1 0.4.5 1.8.2 0.4.6 1.8.3 0.4.7 1.8.4 0.4.8 1.8.5 0.4.9 1.8.6 0.5.0 1.8.7 0.5.1 1.8.8 0.5.2 1.8.9 0.5.3 1.9.0 0.5.4 1.9.1 0.5.5 1.9.2 0.5.6 1.9.3 0.5.7 1.9.4 0.5.8 1.9.5 0.5.9 1.9.6 0.6.0 1.9.7 0.6.1 1.9.8 0.6.2 1.9.81 0.6.3 1.9.82 0.6.4 1.9.83 0.6.5 1.9.84 0.6.6 1.9.85 0.6.7 1.9.86 0.6.8 1.9.87 0.6.9 1.9.88 0.7.0 1.9.89 0.7.1 1.9.90 0.7.2 1.9.91 0.7.3 1.9.92 0.7.4 1.9.93 0.7.5 1.9.94 0.7.6 1.9.95 0.7.7 1.9.96 0.7.8 1.9.97 0.7.9 1.9.98 0.8.0 1.9.99 0.8.1 2.0.0 0.8.2 2.0.1 0.8.3 2.0.2 0.8.4 2.0.3 0.8.5 2.0.4 0.8.6 2.0.5 0.8.7 2.0.6 0.8.8 2.0.7 0.8.9 2.0.8 0.9.0 2.0.9 0.9.2 2.1.0 0.9.3 2.1.1 0.9.4 2.1.2 0.9.5 2.1.3 0.9.6 2.1.4 0.9.7 2.1.5 0.9.8 2.1.6 0.9.81 2.1.7 0.9.82 2.1.8 0.9.83 2.1.9 0.9.84 2.2.0 0.9.85 2.2.1 0.9.86 2.2.2 0.9.87 2.2.3 0.9.88 2.2.4 0.9.89 2.2.5 0.9.9 2.2.51 0.9.91 2.2.52 0.9.92 2.2.53 0.9.93 2.2.54 0.9.94 2.2.56 0.9.95 2.2.57 0.9.96 2.2.6 0.9.97 2.2.60 0.9.98 2.2.61 0.9.99 2.2.62 1.0.0 2.2.63 1.0.01 2.2.70 1.0.1 2.2.80 1.0.2 2.2.81 1.0.3 2.2.90 1.0.4 2.2.91 1.0.5 2.2.92 1.0.6 2.2.93 1.0.7 2.2.94 1.0.8 2.2.95 1.0.9 2.3.0 1.1.0 2.3.1 1.1.1 2.3.2 1.1.2 2.3.3 1.1.3 2.3.4 1.1.4 2.3.5 1.1.5 2.3.6 1.1.6 2.3.7 1.1.7 2.3.8 1.1.8 2.3.9 1.1.9 2.4.0 1.2.0 2.4.1 1.2.1 2.4.2 1.2.2 2.4.3 1.2.21 2.4.4 1.2.3 2.4.5 1.2.30 2.4.6 1.3.0 2.4.7 1.3.1 2.4.8 1.3.2 2.4.9 1.3.3 2.5.0 1.3.31 2.5.1 1.3.32 2.5.2 1.3.33 2.5.3 1.3.34 2.5.4 1.3.35 2.5.5 1.3.36 2.5.6 1.3.37 2.5.7 1.3.38 2.5.8 1.3.39 2.5.9 1.3.40 2.6.0 1.3.41 2.6.1 1.3.42 2.6.2 1.3.43 2.6.3 1.3.44 2.6.5 1.3.45 2.6.6 1.3.46 2.6.7 1.3.47 2.6.8 1.3.48 2.6.9 1.3.49 2.7.0 1.3.50 2.7.1 1.3.51 2.7.2 1.3.52 2.7.3 1.3.53 2.7.4 1.3.54 2.7.5 1.3.56 2.7.6 1.3.57 2.7.7 1.3.58 2.7.8 1.3.59 2.7.9 1.3.60 2.8.0 1.3.61 2.8.1 1.3.62 2.8.2 1.3.63 2.8.3 1.3.64 2.8.4 1.3.65 2.8.5 1.3.66 2.8.6 1.3.67 2.8.7 1.3.68 2.8.8 1.3.69 2.8.9 1.3.70 2.9.0 1.3.71 2.9.1 1.3.72 2.9.2 1.3.73 2.9.3 1.3.74 2.9.4 1.3.75 2.9.5 1.3.76 2.9.6 1.3.77 2.9.7 1.3.78 2.9.8 1.3.79 2.9.9 1.3.80 3.0.0 1.3.81 3.0.1 1.3.82 3.0.2 1.3.83 3.0.3 1.3.84 3.0.4 1.3.85 3.0.5 1.3.86 3.0.6 1.3.87 3.0.7 1.3.88 3.0.8 1.3.89 3.0.9 1.3.90 3.1.0 1.3.91 3.1.1 1.3.92 3.1.2 1.3.93 3.1.3 1.3.94 3.1.4 1.3.95 3.1.5 1.3.96 3.1.6 1.3.97 3.1.7 1.3.98 3.1.8 1.3.99 3.1.9 1.4.0 3.2.0 1.4.1 3.2.1 1.4.2 3.2.2 1.4.3 3.2.3 1.4.4 3.2.4 1.4.5 3.2.5 1.4.6 3.2.6 1.4.7 3.2.7 1.4.8 3.2.8 1.4.9 3.2.9 1.5.0 3.3.0 1.5.1 3.3.1 1.5.2 3.3.2 1.5.3 3.3.3 1.5.4 3.3.4 1.5.5 3.3.5 1.5.6 3.3.6 1.5.7 3.3.7 1.5.8 3.3.8 1.5.9 3.3.9 1.6.0 3.4.0 1.6.1 3.4.1 1.6.2 3.4.2 1.6.3 3.4.3 1.6.5 3.4.4 1.6.51 3.4.5 1.6.52 3.4.6 1.6.53 1.6.54 1.6.55 1.6.56 1.6.57 1.6.58 1.6.59 1.6.60 1.6.61 1.6.62 1.6.63 1.6.64 1.6.65 1.6.66 1.6.67 1.6.68 trunk 1.6.69 0.0.1 1.6.70 0.0.2 1.6.71 0.0.3 1.6.72 0.0.4 1.6.73 0.0.5 1.6.74 0.0.6 1.6.75 0.0.7 1.6.76 0.0.8 1.6.77 0.0.9 1.6.78 0.1.0 1.6.79 0.1.1 1.6.81 0.1.2 1.6.82 0.1.3 1.6.83 0.1.4 1.6.84 0.1.5 1.6.85 0.1.6 1.6.86 0.1.7 1.6.87 0.1.8 1.6.88 0.1.9 1.6.89 0.2.0 1.6.90
ai-engine / labs / mcp.js
ai-engine / labs Last commit date
mcp.conf 1 year ago mcp.js 1 year ago mcp.md 1 year ago mcp.php 1 year ago mcp_core.php 1 year ago mcp_rest.php 1 year ago realtime.php 1 year ago
mcp.js
361 lines
1 #!/usr/bin/env node
2 /**
3 * Claude ↔ AI-Engine MCP relay
4 * --------------------------------
5 * Connects Claude Desktop (JSON-RPC on stdin/stdout) to a WordPress site that
6 * exposes:
7 * • GET /wp-json/mcp/v1/sse (Server-Sent Events stream)
8 * • POST /wp-json/mcp/v1/messages (JSON-RPC ingress)
9 *
10 * If the site is protected by a Bearer token:
11 * • Store the token per-site in ~/.mcp/sites.json
12 * • Relay adds Authorization: Bearer <token>
13 * • 401 / 403 responses are converted to JSON-RPC errors −32001 / −32003
14 * so Claude shows an immediate, clear message instead of timing out.
15 */
16
17 ////////////////////////////////////////////////////////////////////////////////
18 // imports & tiny helpers
19 ////////////////////////////////////////////////////////////////////////////////
20 const fs = require('fs');
21 const os = require('os');
22 const path = require('path');
23 const readline = require('readline');
24 const { setTimeout: delay } = require('timers/promises');
25
26 const readJSON = f => { try { return JSON.parse(fs.readFileSync(f, 'utf8')); } catch { return {}; } };
27 const writeJSON = (f, o) => { fs.mkdirSync(path.dirname(f), { recursive: true }); fs.writeFileSync(f, JSON.stringify(o, null, 2)); };
28
29 const toDomain = s => new URL(/^https?:/.test(s) ? s : `https://${s}`).hostname.toLowerCase();
30 const sseURL = u => u.replace(/\/+$/, '') + '/wp-json/mcp/v1/sse/';
31 const die = m => { console.error(m); process.exit(1); };
32
33 ////////////////////////////////////////////////////////////////////////////////
34 // paths & persistent state
35 ////////////////////////////////////////////////////////////////////////////////
36 const HOME = os.homedir();
37 const MCP_DIR = path.join(HOME, '.mcp'); fs.mkdirSync(MCP_DIR, { recursive: true });
38 const SITE_CFG = path.join(MCP_DIR, 'sites.json');
39 const LOG_HDR = path.join(MCP_DIR, 'mcp.log');
40 const LOG_BODY = path.join(MCP_DIR, 'mcp-results.log');
41 const ERR_LOG = path.join(MCP_DIR, 'error.log');
42 const CLAUDE_CFG = path.join(HOME, 'Library', 'Application Support', 'Claude', 'claude_desktop_config.json');
43 const SELF = path.resolve(__filename);
44
45 /* load sites config (upgrade legacy string → object) */
46 let sites = readJSON(SITE_CFG);
47 for (const [d, v] of Object.entries(sites))
48 if (typeof v === 'string') sites[d] = { url: v, token: '' };
49 const saveSites = () => writeJSON(SITE_CFG, sites);
50
51 /* micro JSON-lines logger */
52 function logError(kind, err, extra = {}) {
53 const entry = { ts: new Date().toISOString(), kind,
54 msg: err?.message || err, stack: err?.stack, ...extra };
55 fs.appendFileSync(ERR_LOG, JSON.stringify(entry) + '\n');
56 }
57 process.on('uncaughtException', e => logError('uncaught', e));
58 process.on('unhandledRejection', e => logError('unhandled', e));
59
60 ////////////////////////////////////////////////////////////////////////////////
61 // Claude Desktop integration (updates claude_desktop_config.json)
62 ////////////////////////////////////////////////////////////////////////////////
63 function setClaudeTarget(domain) {
64 const cfg = readJSON(CLAUDE_CFG);
65 cfg.mcpServers ??= {};
66 cfg.mcpServers['AI Engine'] = { command: SELF, args: ['relay', domain] };
67 writeJSON(CLAUDE_CFG, cfg);
68 }
69 const activeDomain = () => readJSON(CLAUDE_CFG)?.mcpServers?.['AI Engine']?.args?.[1] || null;
70
71 ////////////////////////////////////////////////////////////////////////////////
72 // CLI
73 ////////////////////////////////////////////////////////////////////////////////
74 const [ , , cmd = 'help', ...args] = process.argv;
75
76 const HELP = `
77 add <site-url> [token] Register / update site (and set Claude target)
78 remove <domain|url> Unregister site
79 list Show sites
80 claude [domain|url] Show / change Claude target
81 start [domain|url] Verbose relay
82 relay <domain|url> Silent relay (for Claude Desktop)
83 post <domain> <json> <sid> Fire raw JSON-RPC (debug)
84 help This help
85 `.trim();
86
87 switch (cmd) {
88 case 'add': addSite(...args); break;
89 case 'remove': removeSite(args[0]); break;
90 case 'list': listSites(); break;
91 case 'claude': claudeCmd(args[0]); break;
92 case 'start':
93 case 'relay': launchRelay(cmd, args[0]); break;
94 case 'post': firePost(args); break;
95 default: console.log(HELP);
96 }
97
98 /* ---------- CLI actions ---------- */
99 function addSite(url, token = '') {
100 if (!url) die('add <site-url> [token]');
101 const norm = url.replace(/\/+$/, '');
102 const dom = toDomain(norm);
103 const existed = !!sites[dom];
104 sites[dom] = { url: norm, token };
105 saveSites(); setClaudeTarget(dom);
106 console.log(`${existed ? 'updated' : 'added'} ${norm}`);
107 }
108 function removeSite(ref) {
109 if (!ref) die('remove <domain|url>');
110 const dom = toDomain(ref);
111 if (!sites[dom]) die('unknown site');
112 delete sites[dom]; saveSites();
113 if (activeDomain() === dom) setClaudeTarget(Object.keys(sites)[0] || 'missing');
114 console.log('✓ removed', ref);
115 }
116 function listSites() {
117 if (!Object.keys(sites).length) return console.log('(no sites)');
118 for (const s of Object.values(sites))
119 console.log('', s.url, s.token ? '(token set)' : '');
120 }
121 function claudeCmd(ref) {
122 if (!ref) return console.log(activeDomain()
123 ? `Claude: ${sites[activeDomain()].url}` : '(no site)');
124 const full = /^https?:/.test(ref) ? ref : `https://${ref}`;
125 const dom = toDomain(full);
126 sites[dom] = sites[dom] || { url: full, token: '' };
127 saveSites(); setClaudeTarget(dom);
128 console.log('✓ Claude →', sites[dom].url);
129 }
130
131 ////////////////////////////////////////////////////////////////////////////////
132 // manual POST (debug)
133 ////////////////////////////////////////////////////////////////////////////////
134 async function firePost([dom, json, sid]) {
135 if (!dom || !json || !sid) die('post <domain> <json> <sid>');
136 const site = sites[toDomain(dom)];
137 if (!site) die('unknown site');
138
139 const fetchFn = global.fetch || (await import('node-fetch')).default;
140 const url = `${site.url.replace(/\/+$/, '')}/wp-json/mcp/v1/messages?session_id=${sid}`;
141 const headers = { 'content-type': 'application/json' };
142 if (site.token) headers.authorization = `Bearer ${site.token}`;
143
144 const res = await fetchFn(url, { method: 'POST', headers, body: json });
145 console.log('HTTP', res.status);
146 console.log(await res.text());
147 }
148
149 ////////////////////////////////////////////////////////////////////////////////
150 // launch relay
151 ////////////////////////////////////////////////////////////////////////////////
152 function launchRelay(mode, ref) {
153 const dom = pickSite(ref);
154 runRelay(sites[dom], mode === 'start')
155 .catch(e => { logError('fatal', e); process.exit(1); });
156 }
157 function pickSite(ref) {
158 if (ref) return toDomain(ref);
159 const keys = Object.keys(sites);
160 if (!keys.length) die('no sites registered');
161 if (keys.length === 1) return keys[0];
162 die('multiple sites: ' + keys.join(', '));
163 }
164
165 ////////////////////////////////////////////////////////////////////////////////
166 // relay core
167 ////////////////////////////////////////////////////////////////////////////////
168 async function runRelay(site, verbose) {
169 const fetchFn = global.fetch || (await import('node-fetch')).default;
170
171 /* ---- tiny disk logs ---- */
172 fs.writeFileSync(LOG_HDR, ''); fs.writeFileSync(LOG_BODY, '');
173 const hdr = fs.createWriteStream(LOG_HDR, { flags: 'a' });
174 const bod = fs.createWriteStream(LOG_BODY, { flags: 'a' });
175 const logH = (dir, id, msg='') => hdr.write(`${new Date().toISOString()} ${dir} id=${id ?? '-'} ${msg}\n`);
176 const logB = (dir, id, msg, obj) => { logH(dir, id, msg); bod.write(JSON.stringify(obj, null, 2) + '\n\n'); };
177
178 /* ---- runtime state ---- */
179 let messagesURL = null; // set after “endpoint” event
180 const backlog = []; // queued before endpoint known
181 const pending = new Set(); // ids waiting reply
182 const id2method = new Map(); // for nicer logs
183 let authFail = 0; // 0 = OK, 401 / 403 when auth failed
184 let closing = false;
185 let sseAbort = null;
186
187 /* ---- stdin from Claude ---- */
188 const rl = readline.createInterface({ input: process.stdin });
189 rl.on('line', onStdin).on('close', gracefulExit);
190 process.stdin.on('end', gracefulExit);
191
192 function onStdin(line) {
193 let msg; try { msg = JSON.parse(line); } catch { return; }
194 for (const rpc of (Array.isArray(msg) ? msg : [msg]))
195 handleRpc(rpc, line);
196 }
197
198 function handleRpc(rpc, rawLine) {
199 const { id, method, params } = rpc;
200
201 /* Claude handshake */
202 if (method === 'initialize') {
203 const res = { protocolVersion: params?.protocolVersion || '2024-11-05',
204 capabilities: {}, serverInfo: { name: 'AI Relay', version: '1.5' } };
205 console.log(JSON.stringify({ jsonrpc: '2.0', id, result: res }));
206 logB('server', id, method, res);
207 return;
208 }
209
210 /* auth already failed → instant error */
211 if (authFail && id !== undefined) return authError(id, authFail);
212
213 id2method.set(id, method);
214 messagesURL ? forward(rawLine, id) // endpoint known → send now
215 : backlog.push({ rawLine, id });
216 }
217
218 /* ---- helpers to emit JSON-RPC errors ---- */
219 function sendError(id, code, message) {
220 if (id === null || id === undefined) return; // never reply to notifications
221 const err = { code, message };
222 console.log(JSON.stringify({ jsonrpc: '2.0', id, error: err }));
223 logB('server', id, '', err);
224 }
225 const authError = (id, s) => sendError(id, s === 401 ? -32001 : -32003,
226 s === 401 ? 'Authentication required (401)'
227 : 'Invalid or insufficient token (403)');
228 const transportError = (id, m) => sendError(id, -32000, m);
229
230 /* ---- POST /messages ---- */
231 async function forward(rawLine, id) {
232 const headers = { 'content-type': 'application/json' };
233 if (site.token) headers.authorization = `Bearer ${site.token}`;
234
235 logB('client', id, id2method.get(id), {});
236 try {
237 pending.add(id);
238 const res = await fetchFn(messagesURL, { method: 'POST', headers, body: rawLine });
239
240 if (res.status === 401 || res.status === 403) return authError(id, res.status);
241 if (!res.ok) throw new Error(`HTTP ${res.status}`);
242 } catch (e) {
243 logError('post', e, { url: messagesURL });
244 transportError(id, '/messages unreachable');
245 } finally {
246 pending.delete(id);
247 }
248 }
249
250 /* ---- connect to SSE ---- */
251 const endpoint = sseURL(site.url);
252 verbose ? console.error('▶ connect', endpoint) : process.stderr.write('AI Engine relay started\n');
253
254 while (!closing) {
255 messagesURL = null;
256 try {
257 sseAbort = new AbortController();
258 const headers = {
259 accept: 'text/event-stream',
260 'cache-control': 'no-cache',
261 connection: 'keep-alive',
262 'user-agent': 'Mozilla/5.0'
263 };
264 if (site.token) headers.authorization = `Bearer ${site.token}`;
265
266 const res = await fetchFn(endpoint, { headers, signal: sseAbort.signal });
267
268 /* --- auth failure --- */
269 if (res.status === 401 || res.status === 403) {
270 authFail = res.status;
271 if (verbose) console.error('✗ Unauthorized', res.status);
272 logError('sse-auth', 'unauthorized', { status: res.status });
273 backlog.forEach(b => authError(b.id, authFail));
274 backlog.length = 0;
275 pending.forEach(id => authError(id, authFail));
276 pending.clear();
277 await delay(1000);
278 continue; // stay alive → later RPCs short-circuit
279 }
280
281 /* --- wrong content-type --- */
282 const ctype = res.headers.get('content-type') || '';
283 if (!ctype.startsWith('text/event-stream')) {
284 if (verbose) console.error('✗ unexpected content-type', ctype || 'none');
285 logError('sse-ctype', ctype, {});
286 backlog.forEach(b => transportError(b.id, 'SSE route inactive'));
287 backlog.length = 0;
288 pending.forEach(id => transportError(id, 'SSE route inactive'));
289 pending.clear();
290 return;
291 }
292
293 verbose && console.error('SSE connected');
294
295 const dec = new TextDecoder();
296 let buf = '';
297 for await (const chunk of res.body) {
298 buf += dec.decode(chunk, { stream: true });
299 let i; while ((i = buf.indexOf('\n\n')) !== -1) {
300 handleSseFrame(buf.slice(0, i));
301 buf = buf.slice(i + 2);
302 }
303 }
304 } catch (e) {
305 if (!closing) {
306 verbose && console.error('SSE', e.message);
307 logError('sse', e, { endpoint });
308 backlog.forEach(b => transportError(b.id, 'SSE unreachable'));
309 backlog.length = 0;
310 pending.forEach(id => transportError(id, 'Server disconnected'));
311 pending.clear();
312 }
313 }
314 if (!closing) await delay(2000); // retry
315 }
316
317 /* ---- SSE frame handler ---- */
318 function handleSseFrame(frame) {
319 const evt = frame.match(/^event:(.*)/m)?.[1].trim() || 'message';
320 const data = frame.match(/(?:^data:|\ndata:)([\s\S]*)/m)?. [1]?.replace(/\ndata:/g, '').trim() || '';
321
322 if (evt === 'endpoint') {
323 messagesURL = data;
324 verbose && console.error('↪ messages', data);
325 backlog.splice(0).forEach(b => forward(b.rawLine, b.id));
326 return;
327 }
328
329 if (evt === 'message' && !data) return; // heartbeat
330 console.log(data); // forward as-is
331
332 try {
333 const obj = JSON.parse(data);
334 if ('id' in obj) pending.delete(obj.id);
335 logB('server', obj.id, '', obj.result ? { result: obj.result }
336 : { error: obj.error });
337 } catch (e) {
338 logError('sse-json', e, { raw: data });
339 }
340 }
341
342 /* ---- graceful exit ---- */
343 async function gracefulExit() {
344 if (closing) return; closing = true;
345
346 if (messagesURL) {
347 try {
348 const headers = { 'content-type': 'application/json' };
349 if (site.token) headers.authorization = `Bearer ${site.token}`;
350 await fetchFn(messagesURL, {
351 method: 'POST',
352 headers,
353 body: JSON.stringify({ jsonrpc: '2.0', method: 'mwai/kill' })
354 });
355 } catch {/* ignore */}
356 }
357 sseAbort?.abort();
358 process.exit(0);
359 }
360 }
361