Claude / scripts /agentweb-mcp.js
Cnass's picture
Upload 7 files
397ffab verified
Raw History Blame Contribute Delete
25.7 kB
#!/usr/bin/env node
// Agent Web's own MCP server (stdio, newline-delimited JSON-RPC 2.0).
// - share_file: hands files to the user; the web server sees the result and
// shows them in the chat as download cards (see server/chat.js).
// - screen tools: look at and drive the virtual X display (a browser, ...)
// that the user watches in the "Ekran" tab. Only listed when the display is
// on (the web server sets DISPLAY only then, see server/display.js): their
// schemas and screenshots would only cost tokens otherwise.
// No dependencies on purpose: it starts fast and needs nothing from npm.
'use strict';
const fs = require('fs');
const path = require('path');
const os = require('os');
const readline = require('readline');
const { spawn } = require('child_process');
const SERVER_INFO = { name: 'agentweb', version: '1.0.0' };
const SCREEN = !!process.env.DISPLAY;
const DISPLAY = process.env.DISPLAY || ':99';
const MAX_SHARE_FILES = 10;
const MAX_SHARE_BYTES = 50 * 1024 * 1024;
const MAX_SHARE_TOTAL = 100 * 1024 * 1024;
const SYSTEM_PATH_RE = /^\/(proc|sys|dev)(\/|$)/;
// Screenshots are scaled to fit WXGA; coordinates in every tool are in the
// pixels of that (scaled) image and are mapped back to the real screen.
const SHOT_MAX_W = 1280;
const SHOT_MAX_H = 800;
const MAX_PNG_BYTES = 3 * 1024 * 1024;
const MAX_TYPE_CHARS = 5000;
const LOG_DIR = path.join(os.tmpdir(), 'agentweb-logs');
const APPS = {
browser: { title: 'Tarayıcı', command: 'agentweb-browser', args: [] },
};
const COORD = { type: 'integer', minimum: 0, description: 'Pixel in the screenshot image.' };
const TOOLS = [
{
name: 'share_file',
title: 'Dosya paylaş',
description:
'Send files from this machine to the user: they appear in the Agent Web chat as download cards ' +
'(images also get a preview). Use it whenever the user asks you to send, share or give them a file, ' +
'or when a result is better delivered as a file than as pasted text (long code, logs, CSV, documents, ' +
'archives, images). Write the file first, then call this with its path. Folders cannot be shared; ' +
'archive them first (zip/tar). At most 10 files, 50 MB each.',
inputSchema: {
type: 'object',
properties: {
paths: {
type: 'array',
items: { type: 'string' },
minItems: 1,
maxItems: MAX_SHARE_FILES,
description: 'File paths, absolute or relative to the current working directory.',
},
},
required: ['paths'],
additionalProperties: false,
},
annotations: { title: 'Dosya paylaş', readOnlyHint: true, openWorldHint: false },
},
{
name: 'screenshot',
title: 'Ekran görüntüsü',
description:
'Screenshot of the virtual screen (X display) that the user sees in the "Ekran" tab, where GUI apps ' +
'such as the browser run. Every screen tool takes coordinates in the pixels of this image.',
inputSchema: { type: 'object', properties: {}, additionalProperties: false },
annotations: { title: 'Ekran görüntüsü', readOnlyHint: true, openWorldHint: false },
},
{
name: 'click',
title: 'Tıkla',
description: 'Move the mouse to (x, y) on the virtual screen and click.',
inputSchema: {
type: 'object',
properties: {
x: COORD,
y: COORD,
button: { type: 'string', enum: ['left', 'right', 'middle'], default: 'left' },
double: { type: 'boolean', default: false, description: 'Double-click.' },
},
required: ['x', 'y'],
additionalProperties: false,
},
annotations: { title: 'Tıkla', readOnlyHint: false, destructiveHint: false, openWorldHint: false },
},
{
name: 'drag',
title: 'Sürükle',
description: 'Press a mouse button at (from_x, from_y), move to (to_x, to_y) and release.',
inputSchema: {
type: 'object',
properties: {
from_x: COORD,
from_y: COORD,
to_x: COORD,
to_y: COORD,
button: { type: 'string', enum: ['left', 'right', 'middle'], default: 'left' },
},
required: ['from_x', 'from_y', 'to_x', 'to_y'],
additionalProperties: false,
},
annotations: { title: 'Sürükle', readOnlyHint: false, destructiveHint: false, openWorldHint: false },
},
{
name: 'type_text',
title: 'Yaz',
description:
'Enter text into the focused window of the virtual screen. ASCII text is typed key by key ("\\n" presses ' +
'Enter). Text with other characters (Turkish letters, emoji...) is pasted through the clipboard with Ctrl+V, ' +
'because typing those key by key is unreliable; method "type" or "paste" forces one way.',
inputSchema: {
type: 'object',
properties: {
text: { type: 'string', minLength: 1, maxLength: MAX_TYPE_CHARS },
method: { type: 'string', enum: ['auto', 'type', 'paste'], default: 'auto' },
},
required: ['text'],
additionalProperties: false,
},
annotations: { title: 'Yaz', readOnlyHint: false, destructiveHint: false, openWorldHint: false },
},
{
name: 'press_keys',
title: 'Tuşa bas',
description:
'Press keys on the virtual screen, xdotool style: "Return", "Escape", "Tab", "BackSpace", "Delete", ' +
'"F5", "ctrl+s", "ctrl+shift+p", "alt+F4". Several space-separated combos are pressed in order.',
inputSchema: {
type: 'object',
properties: { keys: { type: 'string', minLength: 1, maxLength: 200 } },
required: ['keys'],
additionalProperties: false,
},
annotations: { title: 'Tuşa bas', readOnlyHint: false, destructiveHint: false, openWorldHint: false },
},
{
name: 'scroll',
title: 'Kaydır',
description: 'Scroll the mouse wheel at (x, y) on the virtual screen.',
inputSchema: {
type: 'object',
properties: {
x: COORD,
y: COORD,
direction: { type: 'string', enum: ['up', 'down', 'left', 'right'] },
amount: { type: 'integer', minimum: 1, maximum: 20, default: 3 },
},
required: ['x', 'y', 'direction'],
additionalProperties: false,
},
annotations: { title: 'Kaydır', readOnlyHint: false, destructiveHint: false, openWorldHint: false },
},
{
name: 'list_windows',
title: 'Pencereler',
description: 'List the visible windows on the virtual screen: title, position and size (in screenshot pixels).',
inputSchema: { type: 'object', properties: {}, additionalProperties: false },
annotations: { title: 'Pencereler', readOnlyHint: true, openWorldHint: false },
},
{
name: 'focus_window',
title: 'Pencereyi öne al',
description: 'Bring the first visible window whose title contains the given text to the front and focus it.',
inputSchema: {
type: 'object',
properties: { title: { type: 'string', minLength: 1, maxLength: 200 } },
required: ['title'],
additionalProperties: false,
},
annotations: { title: 'Pencereyi öne al', readOnlyHint: false, destructiveHint: false, openWorldHint: false },
},
{
name: 'launch_app',
title: 'Uygulama başlat',
description:
'Start a GUI app on the virtual screen: "browser" (Chrome, optionally opening url). ' +
'Check progress with screenshot.',
inputSchema: {
type: 'object',
properties: {
app: { type: 'string', enum: Object.keys(APPS) },
url: { type: 'string', maxLength: 2000, description: 'Only for "browser": an http(s) address to open.' },
},
required: ['app'],
additionalProperties: false,
},
annotations: { title: 'Uygulama başlat', readOnlyHint: false, destructiveHint: false, openWorldHint: true },
},
];
class ToolError extends Error {}
function formatBytes(n) {
if (n < 1024) return `${n} B`;
if (n < 1024 * 1024) return `${Math.round(n / 1024)} KB`;
return `${(n / (1024 * 1024)).toFixed(1)} MB`;
}
function text(value) {
return { type: 'text', text: value };
}
function run(cmd, args, { input, timeout = 20000, maxBytes = 64 * 1024 * 1024 } = {}) {
return new Promise((resolve, reject) => {
let child;
try {
// xdotool decodes typed text with the locale; under C/POSIX it rejects
// anything that is not ASCII (Turkish letters, emoji...).
const env = { ...process.env, DISPLAY, LC_ALL: 'C.UTF-8' };
child = spawn(cmd, args, { env, stdio: ['pipe', 'pipe', 'pipe'] });
} catch (err) {
reject(err);
return;
}
const out = [];
const errOut = [];
let size = 0;
let settled = false;
const finish = (fn, value) => {
if (settled) return;
settled = true;
clearTimeout(timer);
fn(value);
};
const timer = setTimeout(() => {
child.kill('SIGKILL');
finish(reject, new ToolError(`${cmd} zaman aşımına uğradı.`));
}, timeout);
child.stdout.on('data', (chunk) => {
size += chunk.length;
if (size > maxBytes) {
child.kill('SIGKILL');
finish(reject, new ToolError(`${cmd} çıktısı çok büyük.`));
return;
}
out.push(chunk);
});
child.stderr.on('data', (chunk) => {
if (errOut.length < 50) errOut.push(chunk);
});
child.on('error', (err) => {
finish(reject, err.code === 'ENOENT' ? new ToolError(`${cmd} kurulu değil.`) : err);
});
child.on('close', (code) => {
const stderr = Buffer.concat(errOut).toString('utf8').trim();
if (code === 0) finish(resolve, { stdout: Buffer.concat(out), stderr });
else finish(reject, new ToolError(`${cmd} başarısız oldu (kod ${code})${stderr ? `: ${stderr.slice(0, 400)}` : ''}`));
});
child.stdin.on('error', () => {});
child.stdin.end(input || undefined);
});
}
function requireDisplay() {
const match = /^:(\d+)/.exec(DISPLAY);
if (match && !fs.existsSync(`/tmp/.X11-unix/X${match[1]}`)) {
throw new ToolError(
`Sanal ekran (${DISPLAY}) çalışmıyor. Kullanıcıdan Agent Web'de "Ekran" sekmesini açmasını isteyin; ekran orada başlatılır.`
);
}
}
let geometryCache = null;
async function screenGeometry() {
if (geometryCache && Date.now() - geometryCache.at < 30000) return geometryCache;
const { stdout } = await run('xdotool', ['getdisplaygeometry']);
const [width, height] = stdout.toString().trim().split(/\s+/).map(Number);
if (!(width > 0 && height > 0)) throw new ToolError('Ekran boyutu okunamadı.');
const scale = Math.min(1, SHOT_MAX_W / width, SHOT_MAX_H / height);
geometryCache = {
at: Date.now(),
width,
height,
scale,
shotWidth: Math.round(width * scale),
shotHeight: Math.round(height * scale),
};
return geometryCache;
}
async function toScreen(x, y) {
const g = await screenGeometry();
for (const [name, v] of [['x', x], ['y', y]]) {
if (!Number.isInteger(v) || v < 0) throw new ToolError(`${name} sıfır ya da pozitif bir tam sayı olmalı.`);
}
if (x >= g.shotWidth || y >= g.shotHeight) {
throw new ToolError(`(${x}, ${y}) ekranın dışında; görüntü ${g.shotWidth}x${g.shotHeight} piksel.`);
}
return [Math.min(g.width - 1, Math.round(x / g.scale)), Math.min(g.height - 1, Math.round(y / g.scale))];
}
function toShot(g, value) {
return Math.round(value * g.scale);
}
const BUTTONS = { left: '1', middle: '2', right: '3' };
async function screenshot() {
requireDisplay();
const g = await screenGeometry();
const resize = g.scale < 1 ? ['-resize', `${g.shotWidth}x${g.shotHeight}!`] : [];
let image;
try {
image = (await run('import', ['-silent', '-window', 'root', ...resize, 'png:-'])).stdout;
} catch (err) {
if (!/kurulu değil/.test(err.message)) throw err;
image = (await run('magick', ['import', '-silent', '-window', 'root', ...resize, 'png:-'])).stdout;
}
let mimeType = 'image/png';
if (image.length > MAX_PNG_BYTES) {
try {
image = (await run('convert', ['png:-', '-quality', '85', 'jpg:-'], { input: image })).stdout;
mimeType = 'image/jpeg';
} catch {
// PNG olarak kalsin
}
}
return [
{ type: 'image', data: image.toString('base64'), mimeType },
text(
`Ekran ${g.width}x${g.height}` +
(g.scale < 1 ? `, görüntü ${g.shotWidth}x${g.shotHeight} olarak küçültüldü` : '') +
'. Araçlardaki koordinatlar bu görüntünün pikselleridir.'
),
];
}
async function click(args) {
requireDisplay();
const [x, y] = await toScreen(args.x, args.y);
const button = BUTTONS[args.button || 'left'];
if (!button) throw new ToolError('button left, right ya da middle olmalı.');
const repeat = args.double ? ['--repeat', '2', '--delay', '120'] : [];
await run('xdotool', ['mousemove', '--sync', String(x), String(y), 'click', ...repeat, button]);
return [text(`${args.double ? 'Çift tıklandı' : 'Tıklandı'}: (${args.x}, ${args.y}) ${args.button || 'left'}`)];
}
async function drag(args) {
requireDisplay();
const [x1, y1] = await toScreen(args.from_x, args.from_y);
const [x2, y2] = await toScreen(args.to_x, args.to_y);
const button = BUTTONS[args.button || 'left'];
if (!button) throw new ToolError('button left, right ya da middle olmalı.');
const steps = [];
for (let i = 1; i <= 10; i += 1) {
steps.push('mousemove', String(Math.round(x1 + ((x2 - x1) * i) / 10)), String(Math.round(y1 + ((y2 - y1) * i) / 10)), 'sleep', '0.02');
}
await run('xdotool', ['mousemove', '--sync', String(x1), String(y1), 'mousedown', button, ...steps, 'mouseup', button]);
return [text(`Sürüklendi: (${args.from_x}, ${args.from_y}) → (${args.to_x}, ${args.to_y})`)];
}
// xclip keeps serving the selection from a background copy of itself, so only
// the foreground process is waited for (its pipes stay open in the copy).
function setClipboard(value) {
return new Promise((resolve, reject) => {
let child;
try {
child = spawn('xclip', ['-selection', 'clipboard', '-i'], {
env: { ...process.env, DISPLAY, LC_ALL: 'C.UTF-8' },
stdio: ['pipe', 'ignore', 'ignore'],
});
} catch (err) {
reject(err);
return;
}
const timer = setTimeout(() => {
child.kill('SIGKILL');
reject(new ToolError('xclip zaman aşımına uğradı.'));
}, 5000);
child.on('error', (err) => {
clearTimeout(timer);
reject(err.code === 'ENOENT' ? new ToolError('xclip kurulu değil.') : err);
});
child.on('exit', (code) => {
clearTimeout(timer);
if (code === 0) resolve();
else reject(new ToolError(`xclip başarısız oldu (kod ${code}).`));
});
child.stdin.on('error', () => {});
child.stdin.end(value);
});
}
async function typeText(args) {
requireDisplay();
const value = args.text;
if (typeof value !== 'string' || !value) throw new ToolError('text boş olamaz.');
if (value.length > MAX_TYPE_CHARS) throw new ToolError(`En fazla ${MAX_TYPE_CHARS} karakter yazılabilir.`);
const method = args.method || 'auto';
if (!['auto', 'type', 'paste'].includes(method)) throw new ToolError('method auto, type ya da paste olmalı.');
// xdotool can only type what the keyboard map has: for anything else it maps
// a spare key for a moment and undoes it before the app reads the key.
const plain = /^[\x20-\x7e\n\t]*$/.test(value);
if (method === 'paste' || (method === 'auto' && !plain)) {
try {
await setClipboard(value);
await new Promise((resolve) => setTimeout(resolve, 120));
await run('xdotool', ['key', '--clearmodifiers', 'ctrl+v']);
return [text(`${value.length} karakter pano ile yapıştırıldı (Ctrl+V).`)];
} catch (err) {
if (method === 'paste' || !/xclip kurulu değil/.test(err.message)) throw err;
}
}
await run('xdotool', ['type', '--delay', '12', '--', value], { timeout: 120000 });
return [text(`${value.length} karakter yazıldı.`)];
}
async function pressKeys(args) {
requireDisplay();
const combos = String(args.keys || '').trim().split(/\s+/).filter(Boolean);
if (!combos.length || combos.length > 20) throw new ToolError('keys 1-20 tuş kombinasyonu içermeli.');
for (const combo of combos) {
if (!/^[A-Za-z0-9_]+(\+[A-Za-z0-9_]+)*$/.test(combo)) throw new ToolError(`Geçersiz tuş: ${combo}`);
}
await run('xdotool', ['key', '--delay', '60', '--', ...combos]);
return [text(`Basıldı: ${combos.join(' ')}`)];
}
async function scroll(args) {
requireDisplay();
const [x, y] = await toScreen(args.x, args.y);
const button = { up: '4', down: '5', left: '6', right: '7' }[args.direction];
if (!button) throw new ToolError('direction up, down, left ya da right olmalı.');
const amount = Number.isInteger(args.amount) ? Math.min(20, Math.max(1, args.amount)) : 3;
await run('xdotool', ['mousemove', '--sync', String(x), String(y), 'click', '--repeat', String(amount), '--delay', '40', button]);
return [text(`Kaydırıldı: ${args.direction} × ${amount}`)];
}
async function visibleWindows() {
let ids;
try {
ids = (await run('xdotool', ['search', '--onlyvisible', '--name', '.'])).stdout.toString().split(/\s+/).filter(Boolean);
} catch (err) {
// xdotool exits 1 when nothing matches.
if (/kod 1\)/.test(err.message)) return [];
throw err;
}
const g = await screenGeometry();
const windows = [];
for (const id of ids.slice(0, 50)) {
try {
const name = (await run('xdotool', ['getwindowname', id])).stdout.toString().trim();
const geo = (await run('xdotool', ['getwindowgeometry', '--shell', id])).stdout.toString();
const v = (key) => Number((new RegExp(`^${key}=(-?\\d+)`, 'm').exec(geo) || [])[1] || 0);
if (!name || v('WIDTH') < 2 || v('HEIGHT') < 2) continue;
windows.push({ id, title: name, x: toShot(g, v('X')), y: toShot(g, v('Y')), width: toShot(g, v('WIDTH')), height: toShot(g, v('HEIGHT')) });
} catch {
// pencere bu arada kapanmis olabilir
}
}
return windows;
}
async function listWindows() {
requireDisplay();
const windows = await visibleWindows();
if (!windows.length) return [text('Ekranda görünür pencere yok.')];
const lines = windows.map((w) => `- "${w.title}" konum (${w.x}, ${w.y}) boyut ${w.width}x${w.height}`);
return [text(`Görünür pencereler:\n${lines.join('\n')}`)];
}
async function focusWindow(args) {
requireDisplay();
const wanted = String(args.title || '').toLowerCase();
if (!wanted) throw new ToolError('title boş olamaz.');
const match = (await visibleWindows()).find((w) => w.title.toLowerCase().includes(wanted));
if (!match) throw new ToolError(`Başlığında "${args.title}" geçen görünür pencere yok.`);
await run('xdotool', ['windowactivate', '--sync', match.id]).catch(() => run('xdotool', ['windowfocus', match.id]));
return [text(`Öne alındı: "${match.title}"`)];
}
function findOnPath(command) {
for (const dir of String(process.env.PATH || '').split(':')) {
if (!dir) continue;
const candidate = path.join(dir, command);
try {
fs.accessSync(candidate, fs.constants.X_OK);
return candidate;
} catch {
// yok
}
}
return null;
}
async function launchApp(args) {
requireDisplay();
const app = Object.prototype.hasOwnProperty.call(APPS, args.app) ? APPS[args.app] : null;
if (!app) throw new ToolError(`app şunlardan biri olmalı: ${Object.keys(APPS).join(', ')}`);
const bin = findOnPath(app.command);
if (!bin) throw new ToolError(`${app.title} bu kurulumda yok (${app.command} bulunamadı).`);
const appArgs = [...app.args];
if (args.url !== undefined) {
if (args.app !== 'browser') throw new ToolError('url sadece browser için kullanılabilir.');
if (!/^https?:\/\/\S+$/i.test(String(args.url))) throw new ToolError('url http:// ya da https:// ile başlamalı.');
appArgs.push(String(args.url));
}
fs.mkdirSync(LOG_DIR, { recursive: true });
const logFile = path.join(LOG_DIR, `${args.app}.log`);
const log = fs.openSync(logFile, 'a');
try {
const child = spawn(bin, appArgs, { env: { ...process.env, DISPLAY }, stdio: ['ignore', log, log], detached: true });
child.on('error', () => {});
child.unref();
} finally {
fs.closeSync(log);
}
return [text(`${app.title} başlatıldı. Durumu screenshot ile kontrol edin. Günlük: ${logFile}`)];
}
function shareFile(args) {
let raw = args.paths;
if (typeof raw === 'string') raw = [raw];
if (!Array.isArray(raw) || !raw.length) throw new ToolError('paths en az bir dosya yolu içermeli.');
if (raw.length > MAX_SHARE_FILES) throw new ToolError(`Tek seferde en fazla ${MAX_SHARE_FILES} dosya paylaşılabilir.`);
const files = [];
let total = 0;
for (const value of raw) {
if (typeof value !== 'string' || !value.trim() || value.includes('\0')) throw new ToolError('Geçersiz dosya yolu.');
const expanded = value.startsWith('~/') ? path.join(os.homedir(), value.slice(2)) : value;
const abs = path.resolve(process.cwd(), expanded);
let stat;
try {
stat = fs.statSync(abs);
} catch {
throw new ToolError(`Dosya bulunamadı: ${value}`);
}
if (stat.isDirectory()) throw new ToolError(`${value} bir klasör; önce zip ya da tar ile arşivleyip arşivi paylaşın.`);
if (!stat.isFile()) throw new ToolError(`${value} normal bir dosya değil.`);
// /proc and friends look like regular files but hold live process state
// (environment variables, memory maps) that should never leave the box.
if (SYSTEM_PATH_RE.test(fs.realpathSync(abs))) throw new ToolError(`${value} bir sistem dosyası; paylaşılamaz.`);
if (stat.size > MAX_SHARE_BYTES) throw new ToolError(`${value} çok büyük (${formatBytes(stat.size)}; en fazla 50 MB).`);
try {
fs.accessSync(abs, fs.constants.R_OK);
} catch {
throw new ToolError(`${value} okunamıyor (izin yok).`);
}
total += stat.size;
if (total > MAX_SHARE_TOTAL) throw new ToolError('Dosyaların toplamı 100 MB\'ı aşıyor; daha az dosya paylaşın.');
files.push({ path: fs.realpathSync(abs), name: path.basename(abs), size: stat.size });
}
const summary = files.map((f) => `${f.name} (${formatBytes(f.size)})`).join(', ');
return [
text(`Paylaşıldı: ${summary}. Agent Web sohbetinde indirme kartı olarak görünür.`),
text(JSON.stringify({ agentwebShare: files })),
];
}
const HANDLERS = {
share_file: shareFile,
screenshot,
click,
drag,
type_text: typeText,
press_keys: pressKeys,
scroll,
list_windows: listWindows,
focus_window: focusWindow,
launch_app: launchApp,
};
function send(message) {
process.stdout.write(`${JSON.stringify(message)}\n`);
}
function listedTools() {
return SCREEN ? TOOLS : TOOLS.filter((tool) => tool.name === 'share_file');
}
async function handle(message) {
const { id, method, params } = message;
const isRequest = id !== undefined && id !== null;
if (!method) return;
if (!isRequest) return; // notifications (initialized, cancelled, ...)
try {
switch (method) {
case 'initialize': {
const requested = params && params.protocolVersion;
send({
jsonrpc: '2.0',
id,
result: {
protocolVersion: /^\d{4}-\d{2}-\d{2}$/.test(requested || '') ? requested : '2025-06-18',
capabilities: { tools: { listChanged: false } },
serverInfo: SERVER_INFO,
instructions: SCREEN
? 'Agent Web tools. share_file sends files to the user in the chat. The screen tools drive the ' +
'virtual display the user watches in the "Ekran" tab.'
: 'Agent Web tools. share_file sends files to the user in the chat.',
},
});
return;
}
case 'ping':
send({ jsonrpc: '2.0', id, result: {} });
return;
case 'tools/list':
send({ jsonrpc: '2.0', id, result: { tools: listedTools() } });
return;
case 'tools/call': {
const name = params && params.name;
const listed = listedTools().some((tool) => tool.name === name);
const handler = listed && Object.prototype.hasOwnProperty.call(HANDLERS, name) ? HANDLERS[name] : null;
if (!handler) {
send({ jsonrpc: '2.0', id, error: { code: -32602, message: `Unknown tool: ${name}` } });
return;
}
try {
const content = await handler((params && params.arguments) || {});
send({ jsonrpc: '2.0', id, result: { content } });
} catch (err) {
const messageText = err instanceof ToolError ? err.message : `Hata: ${err && err.message ? err.message : String(err)}`;
send({ jsonrpc: '2.0', id, result: { content: [text(messageText)], isError: true } });
}
return;
}
default:
send({ jsonrpc: '2.0', id, error: { code: -32601, message: `Method not found: ${method}` } });
}
} catch (err) {
send({ jsonrpc: '2.0', id, error: { code: -32603, message: err && err.message ? err.message : String(err) } });
}
}
if (require.main === module) {
const rl = readline.createInterface({ input: process.stdin, crlfDelay: Infinity });
rl.on('line', (line) => {
if (!line.trim()) return;
let message;
try {
message = JSON.parse(line);
} catch {
send({ jsonrpc: '2.0', id: null, error: { code: -32700, message: 'Parse error' } });
return;
}
for (const item of Array.isArray(message) ? message : [message]) {
if (item && typeof item === 'object') handle(item);
}
});
rl.on('close', () => process.exit(0));
}
module.exports = { TOOLS, HANDLERS };