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