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