PluginProbe ʕ •ᴥ•ʔ
AI Engine – The Chatbot, AI Framework & MCP for WordPress / 2.8.3
AI Engine – The Chatbot, AI Framework & MCP for WordPress v2.8.3
3.6.1 3.6.0 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
548 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 /* colors for terminal output */
34 const colors = {
35 reset: '\x1b[0m',
36 bright: '\x1b[1m',
37 green: '\x1b[32m', // server data
38 blue: '\x1b[34m', // info
39 lightblue: '\x1b[94m', // test commands
40 white: '\x1b[37m' // script messages
41 };
42 const c = (color, text) => `${colors[color]}${text}${colors.reset}`;
43
44 /* ASCII cat welcome */
45 const showWelcome = () => {
46 console.error(c('white', ''));
47 console.error(c('white', ' /\\_/\\'));
48 console.error(c('white', ' ( o.o )'));
49 console.error(c('white', ' > ^ < Welcome to MCP by AI Engine'));
50 console.error(c('white', ''));
51 };
52
53 ////////////////////////////////////////////////////////////////////////////////
54 // paths & persistent state
55 ////////////////////////////////////////////////////////////////////////////////
56 const HOME = os.homedir();
57 const MCP_DIR = path.join(HOME, '.mcp'); fs.mkdirSync(MCP_DIR, { recursive: true });
58 const SITE_CFG = path.join(MCP_DIR, 'sites.json');
59 const LOG_HDR = path.join(MCP_DIR, 'mcp.log');
60 const LOG_BODY = path.join(MCP_DIR, 'mcp-results.log');
61 const ERR_LOG = path.join(MCP_DIR, 'error.log');
62 const CLAUDE_CFG = path.join(HOME, 'Library', 'Application Support', 'Claude', 'claude_desktop_config.json');
63 const SELF = path.resolve(__filename);
64
65 /* load sites config (upgrade legacy string → object) */
66 let sites = readJSON(SITE_CFG);
67 for (const [d, v] of Object.entries(sites))
68 if (typeof v === 'string') sites[d] = { url: v, token: '' };
69 const saveSites = () => writeJSON(SITE_CFG, sites);
70
71 /* micro JSON-lines logger */
72 function logError(kind, err, extra = {}) {
73 const entry = { ts: new Date().toISOString(), kind,
74 msg: err?.message || err, stack: err?.stack, ...extra };
75 fs.appendFileSync(ERR_LOG, JSON.stringify(entry) + '\n');
76 }
77 process.on('uncaughtException', e => logError('uncaught', e));
78 process.on('unhandledRejection', e => logError('unhandled', e));
79
80 ////////////////////////////////////////////////////////////////////////////////
81 // Claude Desktop integration (updates claude_desktop_config.json)
82 ////////////////////////////////////////////////////////////////////////////////
83 function setClaudeTarget(domain) {
84 const cfg = readJSON(CLAUDE_CFG);
85 cfg.mcpServers ??= {};
86 cfg.mcpServers['AI Engine'] = { command: SELF, args: ['relay', domain] };
87 writeJSON(CLAUDE_CFG, cfg);
88 }
89 const activeDomain = () => readJSON(CLAUDE_CFG)?.mcpServers?.['AI Engine']?.args?.[1] || null;
90
91 ////////////////////////////////////////////////////////////////////////////////
92 // CLI
93 ////////////////////////////////////////////////////////////////////////////////
94 const [ , , cmd = 'help', ...args] = process.argv;
95
96 const HELP = `
97 add <site-url> [token] Register / update site (and set Claude target)
98 remove <domain|url> Unregister site
99 list Show sites
100 claude [domain|url] Show / change Claude target
101 select Interactively select a site for Claude
102 start [domain|url] [--raw] Verbose relay (add --raw for JSON responses)
103 relay <domain|url> Silent relay (for Claude Desktop)
104 post <domain> <json> <sid> Fire raw JSON-RPC (debug)
105 help This help
106 `.trim();
107
108 switch (cmd) {
109 case 'add': addSite(...args); break;
110 case 'remove': removeSite(args[0]); break;
111 case 'list': listSites(); break;
112 case 'claude': claudeCmd(args[0]); break;
113 case 'select': selectSite(); break;
114 case 'start':
115 case 'relay': launchRelay(cmd, args[0]); break;
116 case 'post': firePost(args); break;
117 default: console.log(HELP);
118 }
119
120 /* ---------- CLI actions ---------- */
121 function addSite(url, token = '') {
122 if (!url) die('add <site-url> [token]');
123 const norm = url.replace(/\/+$/, '');
124 const dom = toDomain(norm);
125 const existed = !!sites[dom];
126 sites[dom] = { url: norm, token };
127 saveSites(); setClaudeTarget(dom);
128 console.log(`${existed ? 'updated' : 'added'} ${norm}`);
129 }
130 function removeSite(ref) {
131 if (!ref) die('remove <domain|url>');
132 const dom = toDomain(ref);
133 if (!sites[dom]) die('unknown site');
134 delete sites[dom]; saveSites();
135 if (activeDomain() === dom) setClaudeTarget(Object.keys(sites)[0] || 'missing');
136 console.log('✓ removed', ref);
137 }
138 function listSites() {
139 if (!Object.keys(sites).length) return console.log('(no sites)');
140
141 const active = activeDomain();
142 if (!active) {
143 console.log('Claude is not configured to use any of your sites.');
144 }
145
146 for (const [domain, site] of Object.entries(sites)) {
147 const marker = active === domain ? '' : '';
148 console.log(marker, site.url);
149 }
150 }
151 function claudeCmd(ref) {
152 if (!ref) return console.log(activeDomain()
153 ? `Claude: ${sites[activeDomain()].url}` : '(no site)');
154 const full = /^https?:/.test(ref) ? ref : `https://${ref}`;
155 const dom = toDomain(full);
156 sites[dom] = sites[dom] || { url: full, token: '' };
157 saveSites(); setClaudeTarget(dom);
158 console.log('✓ Claude →', sites[dom].url);
159 }
160 function selectSite() {
161 const siteList = Object.entries(sites);
162 if (!siteList.length) return console.log('No sites registered. Use "add" to register a site first.');
163
164 if (siteList.length === 1) {
165 const [domain, site] = siteList[0];
166 setClaudeTarget(domain);
167 return console.log('✓ Claude →', site.url);
168 }
169
170 console.log('Select a site for Claude:');
171 siteList.forEach(([domain, site], i) => {
172 const current = activeDomain() === domain ? ' (current)' : '';
173 console.log(` ${i + 1}. ${site.url}${current}`);
174 });
175
176 const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
177 rl.question('\nEnter selection (1-' + siteList.length + '): ', (answer) => {
178 const choice = parseInt(answer) - 1;
179 if (choice >= 0 && choice < siteList.length) {
180 const [domain, site] = siteList[choice];
181 setClaudeTarget(domain);
182 console.log('✓ Claude →', site.url);
183 } else {
184 console.log('Invalid selection');
185 }
186 rl.close();
187 });
188 }
189
190 ////////////////////////////////////////////////////////////////////////////////
191 // manual POST (debug)
192 ////////////////////////////////////////////////////////////////////////////////
193 async function firePost([dom, json, sid]) {
194 if (!dom || !json || !sid) die('post <domain> <json> <sid>');
195 const site = sites[toDomain(dom)];
196 if (!site) die('unknown site');
197
198 const fetchFn = global.fetch || (await import('node-fetch')).default;
199 const url = `${site.url.replace(/\/+$/, '')}/wp-json/mcp/v1/messages?session_id=${sid}`;
200 const headers = { 'content-type': 'application/json' };
201 if (site.token) headers.authorization = `Bearer ${site.token}`;
202
203 const res = await fetchFn(url, { method: 'POST', headers, body: json });
204 console.log('HTTP', res.status);
205 console.log(await res.text());
206 }
207
208 ////////////////////////////////////////////////////////////////////////////////
209 // launch relay
210 ////////////////////////////////////////////////////////////////////////////////
211 function launchRelay(mode, ref) {
212 const dom = pickSite(ref);
213 const isVerbose = mode === 'start';
214 const showRaw = process.argv.includes('--raw');
215 runRelay(sites[dom], isVerbose, showRaw)
216 .catch(e => { logError('fatal', e); process.exit(1); });
217 }
218 function pickSite(ref) {
219 if (ref) return toDomain(ref);
220
221 const active = activeDomain();
222 if (active && sites[active]) return active;
223
224 const keys = Object.keys(sites);
225 if (!keys.length) die('no sites registered');
226 if (keys.length === 1) return keys[0];
227 die('multiple sites: ' + keys.join(', ') + ' (use "select" to choose)');
228 }
229
230 ////////////////////////////////////////////////////////////////////////////////
231 // relay core
232 ////////////////////////////////////////////////////////////////////////////////
233 async function runRelay(site, verbose, showRaw = false) {
234 const fetchFn = global.fetch || (await import('node-fetch')).default;
235
236 /* ---- tiny disk logs ---- */
237 fs.writeFileSync(LOG_HDR, ''); fs.writeFileSync(LOG_BODY, '');
238 const hdr = fs.createWriteStream(LOG_HDR, { flags: 'a' });
239 const bod = fs.createWriteStream(LOG_BODY, { flags: 'a' });
240 const logH = (dir, id, msg='') => hdr.write(`${new Date().toISOString()} ${dir} id=${id ?? '-'} ${msg}\n`);
241 const logB = (dir, id, msg, obj) => { logH(dir, id, msg); bod.write(JSON.stringify(obj, null, 2) + '\n\n'); };
242
243 /* ---- runtime state ---- */
244 let messagesURL = null; // set after “endpoint” event
245 const backlog = []; // queued before endpoint known
246 const pending = new Set(); // ids waiting reply
247 const id2method = new Map(); // for nicer logs
248 let authFail = 0; // 0 = OK, 401 / 403 when auth failed
249 let closing = false;
250 let sseAbort = null;
251
252 /* ---- stdin from Claude ---- */
253 const rl = readline.createInterface({ input: process.stdin });
254 rl.on('line', onStdin).on('close', gracefulExit);
255 process.stdin.on('end', gracefulExit);
256
257 function onStdin(line) {
258 let msg; try { msg = JSON.parse(line); } catch { return; }
259 for (const rpc of (Array.isArray(msg) ? msg : [msg]))
260 handleRpc(rpc, line);
261 }
262
263 function handleRpc(rpc, rawLine) {
264 const { id, method, params } = rpc;
265
266 /* Claude handshake */
267 if (method === 'initialize') {
268 const res = { protocolVersion: params?.protocolVersion || '2024-11-05',
269 capabilities: {}, serverInfo: { name: 'AI Relay', version: '1.5' } };
270 console.log(JSON.stringify({ jsonrpc: '2.0', id, result: res }));
271 logB('server', id, method, res);
272 return;
273 }
274
275 /* auth already failed → instant error */
276 if (authFail && id !== undefined) return authError(id, authFail);
277
278 id2method.set(id, method);
279 messagesURL ? forward(rawLine, id) // endpoint known → send now
280 : backlog.push({ rawLine, id });
281 }
282
283 /* ---- helpers to emit JSON-RPC errors ---- */
284 function sendError(id, code, message) {
285 if (id === null || id === undefined) return; // never reply to notifications
286 const err = { code, message };
287 console.log(JSON.stringify({ jsonrpc: '2.0', id, error: err }));
288 logB('server', id, '', err);
289 }
290 const authError = (id, s) => sendError(id, s === 401 ? -32001 : -32003,
291 s === 401 ? 'Authentication required (401)'
292 : 'Invalid or insufficient token (403)');
293 const transportError = (id, m) => sendError(id, -32000, m);
294
295 /* ---- POST /messages ---- */
296 async function forward(rawLine, id) {
297 const headers = { 'content-type': 'application/json' };
298 if (site.token) headers.authorization = `Bearer ${site.token}`;
299
300 logB('client', id, id2method.get(id), {});
301 try {
302 pending.add(id);
303 const res = await fetchFn(messagesURL, { method: 'POST', headers, body: rawLine });
304
305 if (res.status === 401 || res.status === 403) return authError(id, res.status);
306 if (!res.ok) throw new Error(`HTTP ${res.status}`);
307 } catch (e) {
308 logError('post', e, { url: messagesURL });
309 transportError(id, '/messages unreachable');
310 } finally {
311 pending.delete(id);
312 }
313 }
314
315 /* ---- connect to SSE ---- */
316 const endpoint = sseURL(site.url);
317 if (verbose) {
318 showWelcome();
319 console.error(c('white', '▶ Connecting to MCP server'));
320 console.error(c('blue', endpoint));
321 console.error('');
322 } else {
323 process.stderr.write('AI Engine relay started\n');
324 }
325
326 while (!closing) {
327 messagesURL = null;
328 try {
329 sseAbort = new AbortController();
330 const headers = {
331 accept: 'text/event-stream',
332 'cache-control': 'no-cache',
333 connection: 'keep-alive',
334 'user-agent': 'Mozilla/5.0'
335 };
336 if (site.token) headers.authorization = `Bearer ${site.token}`;
337
338 const res = await fetchFn(endpoint, { headers, signal: sseAbort.signal });
339
340 /* --- auth failure --- */
341 if (res.status === 401 || res.status === 403) {
342 authFail = res.status;
343 if (verbose) console.error('✗ Unauthorized', res.status);
344 logError('sse-auth', 'unauthorized', { status: res.status });
345 backlog.forEach(b => authError(b.id, authFail));
346 backlog.length = 0;
347 pending.forEach(id => authError(id, authFail));
348 pending.clear();
349 await delay(1000);
350 continue; // stay alive → later RPCs short-circuit
351 }
352
353 /* --- wrong content-type --- */
354 const ctype = res.headers.get('content-type') || '';
355 if (!ctype.startsWith('text/event-stream')) {
356 if (verbose) console.error('✗ unexpected content-type', ctype || 'none');
357 logError('sse-ctype', ctype, {});
358 backlog.forEach(b => transportError(b.id, 'SSE route inactive'));
359 backlog.length = 0;
360 pending.forEach(id => transportError(id, 'SSE route inactive'));
361 pending.clear();
362 return;
363 }
364
365 verbose && console.error(c('white', '✓ SSE connection established'));
366
367 const dec = new TextDecoder();
368 let buf = '';
369 for await (const chunk of res.body) {
370 buf += dec.decode(chunk, { stream: true });
371 let i; while ((i = buf.indexOf('\n\n')) !== -1) {
372 handleSseFrame(buf.slice(0, i));
373 buf = buf.slice(i + 2);
374 }
375 }
376 } catch (e) {
377 if (!closing) {
378 verbose && console.error('SSE', e.message);
379 logError('sse', e, { endpoint });
380 backlog.forEach(b => transportError(b.id, 'SSE unreachable'));
381 backlog.length = 0;
382 pending.forEach(id => transportError(id, 'Server disconnected'));
383 pending.clear();
384 }
385 }
386 if (!closing) await delay(2000); // retry
387 }
388
389 /* ---- SSE frame handler ---- */
390 function handleSseFrame(frame) {
391 const evt = frame.match(/^event:(.*)/m)?.[1].trim() || 'message';
392 const data = frame.match(/(?:^data:|\ndata:)([\s\S]*)/m)?. [1]?.replace(/\ndata:/g, '').trim() || '';
393
394 if (evt === 'endpoint') {
395 messagesURL = data;
396 if (verbose) {
397 console.error(c('white', '✓ MCP server connected'));
398 console.error(c('green', data));
399 console.error('');
400
401 // Extract session_id from URL
402 const sessionMatch = data.match(/session_id=([^&]+)/);
403 if (sessionMatch) {
404 const sessionId = sessionMatch[1];
405 const domain = toDomain(site.url);
406 const toolsCmd = `${SELF} post ${domain} '{"jsonrpc":"2.0","method":"tools/list","id":1}' ${sessionId}`;
407 const pingCmd = `${SELF} post ${domain} '{"jsonrpc":"2.0","method":"tools/call","params":{"name":"mcp_ping","arguments":{}},"id":2}' ${sessionId}`;
408
409 console.error(c('white', 'Test the connection in another terminal:'));
410 console.error(c('white', 'Simple ping test:'));
411 console.error(c('lightblue', pingCmd));
412 console.error('');
413 console.error(c('white', 'List all available tools:'));
414 console.error(c('lightblue', toolsCmd));
415 console.error('');
416 console.error(c('white', 'For raw JSON responses, add'), c('lightblue', '--raw'), c('white', 'to any command'));
417 console.error(c('blue', 'Results will appear in this terminal.'));
418 console.error('');
419 }
420 }
421 backlog.splice(0).forEach(b => forward(b.rawLine, b.id));
422 return;
423 }
424
425 if (evt === 'message' && !data) return; // heartbeat
426
427 // Show received data message in verbose mode
428 if (verbose) {
429 try {
430 const obj = JSON.parse(data);
431 if ('id' in obj) {
432 console.error(c('white', `▼ Response for ID ${obj.id}:`));
433
434 // Show raw JSON if requested
435 if (showRaw) {
436 console.error(c('green', JSON.stringify(obj, null, 2)));
437 }
438 // Format MCP tool results nicely
439 else if (obj.result) {
440 console.error(c('green', '✓ Success'));
441
442 // Special formatting for mcp_ping
443 if (obj.result.data && obj.result.data.time && obj.result.data.name) {
444 console.error(c('white', `Time: ${obj.result.data.time}`));
445 console.error(c('white', `Site: ${obj.result.data.name}`));
446 if (obj.result.data.tools_count !== undefined) {
447 console.error(c('white', `Tools: ${obj.result.data.tools_count} available`));
448 }
449 }
450 // Special formatting for tools/list
451 else if (obj.result.tools && Array.isArray(obj.result.tools)) {
452 console.error(c('white', `Found ${obj.result.tools.length} tools:\n`));
453
454 obj.result.tools.forEach((tool, index) => {
455 // Tool name and description
456 console.error(c('bright', `${index + 1}. ${tool.name || 'unnamed'}`));
457 if (tool.description) {
458 console.error(c('white', ` ${tool.description}`));
459 }
460
461 // Input parameters
462 if (tool.inputSchema && tool.inputSchema.properties) {
463 const props = tool.inputSchema.properties;
464 const required = tool.inputSchema.required || [];
465 const propKeys = Object.keys(props);
466
467 if (propKeys.length > 0) {
468 console.error(c('blue', ' Parameters:'));
469 propKeys.forEach(key => {
470 const prop = props[key];
471 const isRequired = required.includes(key);
472 const reqIcon = isRequired ? '*' : '-';
473 const typeInfo = prop.type ? ` (${prop.type})` : '';
474 const desc = prop.description ? ` - ${prop.description}` : '';
475 console.error(c('white', ` ${reqIcon} ${key}${typeInfo}${desc}`));
476 });
477 } else {
478 console.error(c('blue', ' No parameters required'));
479 }
480 } else {
481 console.error(c('blue', ' No parameters required'));
482 }
483 console.error(''); // Empty line between tools
484 });
485 }
486 // Fallback for other structured data
487 else if (obj.result.data) {
488 console.error(c('white', 'Data:'));
489 console.error(c('green', JSON.stringify(obj.result.data, null, 2)));
490 }
491 // Generic result display
492 else {
493 console.error(c('green', JSON.stringify(obj.result, null, 2)));
494 }
495 }
496 // Handle errors
497 else if (obj.error) {
498 console.error(c('white', '✗ Error:'));
499 console.error(c('green', `${obj.error.code}: ${obj.error.message}`));
500 }
501 // Fallback to raw JSON for other responses
502 else {
503 console.error(c('green', JSON.stringify(obj, null, 2)));
504 }
505
506 console.error('');
507 // Don't forward to console.log in verbose mode for responses with IDs
508 return;
509 }
510 } catch (e) {
511 console.error(c('white', 'Received Data:'));
512 console.error(c('green', data));
513 console.error('');
514 }
515 }
516
517 console.log(data); // forward as-is
518
519 try {
520 const obj = JSON.parse(data);
521 if ('id' in obj) pending.delete(obj.id);
522 logB('server', obj.id, '', obj.result ? { result: obj.result }
523 : { error: obj.error });
524 } catch (e) {
525 logError('sse-json', e, { raw: data });
526 }
527 }
528
529 /* ---- graceful exit ---- */
530 async function gracefulExit() {
531 if (closing) return; closing = true;
532
533 if (messagesURL) {
534 try {
535 const headers = { 'content-type': 'application/json' };
536 if (site.token) headers.authorization = `Bearer ${site.token}`;
537 await fetchFn(messagesURL, {
538 method: 'POST',
539 headers,
540 body: JSON.stringify({ jsonrpc: '2.0', method: 'mwai/kill' })
541 });
542 } catch {/* ignore */}
543 }
544 sseAbort?.abort();
545 process.exit(0);
546 }
547 }
548