import { readFileSync } from 'node:fs'; import { mcpToolError, contractFor } from './errors.js'; import { toolCommand, toolDefinition } from './commands.js'; import { guided } from './field-hints.js'; import { Weave, WeaveError } from './engine.js'; import '../public/starter-core.js'; import { submitForm, listForms, createForm, updateForm, getFormDefinition } from './forms.js'; const PROTOCOL_VERSION = '2024-11-05'; export { QUERY_PAGE, QUERY_CAP } from './commands.js'; const VERSION = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8')).version; const seal = (text) => text.replace(/<(?=\/?weave_data)/gi, '\\u003c'); const attr = (v) => String(v ?? '*').replace(/[\s"<>&]+/g, ' ').trim() || '*'; function textResult(data, source = null) { const text = typeof data === 'string' ? data : JSON.stringify(data); if (!source) return { content: [{ type: 'text', text }] }; const head = ``; return { content: [{ type: 'text', text: `${head}\n${seal(text)}\n` }] }; } function storedSource(weave, name, args) { if (name === 'weave_call' && args.name !== 'help') { let inner = args.args ?? {}; try { if (typeof inner === 'string') inner = JSON.parse(inner); } catch { inner = {}; } return storedSource(weave, args.name, inner ?? {}); } if (!READ_ONLY.has(name)) return null; const tableOf = (fn) => { try { const db = fn(); return db ? weave.qualifiedName(db) : null; } catch { return null; } }; const ref = args.db ?? args.table ?? null; const table = (ref != null ? tableOf(() => weave.findTable(ref)) ?? ref : null) ?? (args.entity != null ? tableOf(() => weave.state.tables[weave.getEntity(args.entity).dbId]) : null); return { table, entity: args.entity ?? null, field: args.doc ?? args.field ?? null }; } export const COMPACT_TOOLS = new Set([ 'weave_create_entity', 'weave_update_entity', 'weave_delete_entity', 'weave_restore_entity', 'weave_set_state', 'weave_link', 'weave_unlink', 'weave_create_space', 'weave_update_space', 'weave_restore_space', 'weave_create_table', 'weave_update_table', 'weave_move_table', 'weave_duplicate_table', 'weave_restore_table', 'weave_add_field', 'weave_update_field', 'weave_add_relation', ]); const BRIEF_KEYS = ['id', 'publicId', 'name', 'type', 'config', 'deletedAt', 'purged', 'next', 'hints', 'invite', 'token']; function brief(obj) { const out = {}; for (const k of BRIEF_KEYS) if (obj?.[k] != null) out[k] = obj[k]; return out; } function compactResult(name, result) { if (!COMPACT_TOOLS.has(name) || !result || typeof result !== 'object') return result; if (name === 'weave_add_relation') return { field: brief(result.field), ...(result.inverse ? { inverse: brief(result.inverse) } : {}) }; return brief(result); } const VERBOSE = { type: 'boolean', description: 'Return the whole object' }; export const TOOLS = [ toolDefinition('weave_ontology'), toolDefinition('weave_schema'), toolDefinition('weave_query'), toolDefinition('weave_get_entity'), toolDefinition('weave_create_entity'), toolDefinition('weave_update_entity'), toolDefinition('weave_delete_entity'), toolDefinition('weave_restore_entity'), toolDefinition('weave_trash'), toolDefinition('weave_stats'), toolDefinition('weave_undo'), toolDefinition('weave_bulk'), toolDefinition('weave_set_state'), toolDefinition('weave_link'), toolDefinition('weave_unlink'), toolDefinition('weave_get_doc'), toolDefinition('weave_set_doc'), toolDefinition('weave_doc_revisions'), toolDefinition('weave_doc_restore'), toolDefinition('weave_add_comment'), toolDefinition('weave_delete_comment'), toolDefinition('weave_search'), toolDefinition('weave_create_space'), toolDefinition('weave_create_table'), toolDefinition('weave_add_field'), toolDefinition('weave_check_formula'), toolDefinition('weave_add_relation'), toolDefinition('weave_create_automation'), toolDefinition('weave_export_csv'), toolDefinition('weave_import_csv'), toolDefinition('weave_attach_file'), toolDefinition('weave_vocabulary'), toolDefinition('weave_update_space'), toolDefinition('weave_template_list'), toolDefinition('weave_template_use'), toolDefinition('weave_delete_space'), toolDefinition('weave_restore_space'), toolDefinition('weave_update_table'), toolDefinition('weave_table_view'), toolDefinition('weave_move_table'), toolDefinition('weave_duplicate_table'), toolDefinition('weave_delete_table'), toolDefinition('weave_restore_table'), toolDefinition('weave_update_field'), toolDefinition('weave_rollback_field'), toolDefinition('weave_delete_field'), toolDefinition('weave_restore_field'), toolDefinition('weave_apply_schema'), toolDefinition('weave_build'), toolDefinition('weave_views'), toolDefinition('weave_shares'), toolDefinition('weave_automations'), toolDefinition('weave_activity'), toolDefinition('weave_audit'), toolDefinition('weave_workspace'), toolDefinition('weave_accounts'), toolDefinition('weave_keys'), toolDefinition('weave_files'), toolDefinition('weave_registry'), toolDefinition('weave_relation_map'), toolDefinition('weave_export_json'), { name: 'weave_forms', description: 'Manage native table forms. Architect access is required on the workspace and its hub registry. list shows forms here; get reads an editable definition; create takes a definition with name, table and fields; update changes a form without changing its permalink. Fields accept field, key, label, required, default and aliases. Omitted settings survive edits. Public submissions use weave_form_submit.', inputSchema: { type: 'object', properties: { action: { type: 'string', enum: ['list', 'get', 'create', 'update'] }, form: { type: 'string', description: 'The form ID or name for get or update.' }, definition: { type: 'object', description: 'name, description, table, fields, hidden, enabled and floor (Observer or Anonymous). Create also accepts immutable shareOnly true for submission-link-only forms. Update accepts only the settings to change.' }, }, required: ['action'], }, }, { name: 'weave_form_submit', description: 'Submit one record through a System/Forms form without write access to its table. form is its id or name; values accepts stable input keys, field IDs, aliases, names or labels. Reuse idempotencyKey when retrying the same request: it returns the original receipt; changed input with the same key is refused. Server-filled and unlisted fields are refused. Bug forms take note and categories (slow, broken-ui, wrong-data, error).', inputSchema: { type: 'object', properties: { form: { type: 'string', description: 'The form id or name, as GET /api/forms lists them.' }, values: { type: 'object', description: 'Stable input key, field ID, alias, name or label to value, for the fields the form shows.' }, idempotencyKey: { type: 'string', description: 'A unique key for this attempt. Reuse it with the same input to safely retry for 24 hours.' }, note: { type: 'string', description: 'Bug report forms: what happened.' }, categories: { type: 'array', items: { type: 'string' }, description: 'Bug report forms: the symptoms.' }, }, required: ['form'], }, }, toolDefinition('weave_import_json'), ]; for (const t of TOOLS) if (COMPACT_TOOLS.has(t.name)) t.inputSchema.properties.verbose = VERBOSE; export const CORE_TOOLS = new Set([ 'weave_ontology', 'weave_schema', 'weave_query', 'weave_get_entity', 'weave_create_entity', 'weave_update_entity', 'weave_create_space', 'weave_create_table', 'weave_add_field', 'weave_update_field', 'weave_add_relation', 'weave_import_csv', 'weave_vocabulary', 'weave_workspace', 'weave_search', 'weave_build', 'weave_call', ]); export const SUMMARY = { weave_build: 'spaces, tables, fields, relations and records in one call (dryRun checks)', weave_ontology: 'short schema: outline, one table, changes', weave_schema: 'the full schema as JSON', weave_query: 'records of one table, where and sort, 200 a page', weave_get_entity: 'one record in full: values, document, comments, activity', weave_search: 'find records, tables and spaces by text; each hit has a permalink', weave_create_entity: 'create one record', weave_update_entity: 'change one record\'s values', weave_import_csv: 'many records from CSV text', weave_create_space: 'create a space', weave_create_table: 'create a table in a space', weave_add_field: 'add a field to a table', weave_update_field: 'rename, retype or reconfigure a field (options are a full replacement)', weave_add_relation: 'add a relation and its inverse between two tables', weave_workspace: 'read or rename the workspace, set its description or logo', weave_vocabulary: 'allowed values; several sections and icon queries in one call', weave_call: 'run any other tool by name; help describes one', weave_delete_entity: 'trash a record (hard: true purges)', weave_restore_entity: 'restore a trashed record', weave_trash: 'list trashed records', weave_stats: 'summarise a table\'s columns', weave_undo: 'revert the last record edits (list: true previews)', weave_bulk: 'set, link, move or roll up many records in one write', weave_set_state: 'move a record to a workflow state', weave_link: 'link records through a relation field', weave_unlink: 'unlink records from a relation field', weave_get_doc: 'a record\'s document as markdown', weave_set_doc: 'replace or append a record\'s document', weave_doc_revisions: 'a document\'s revisions', weave_doc_restore: 'write a past revision back', weave_add_comment: 'comment on a record', weave_delete_comment: 'delete a comment', weave_check_formula: 'validate and preview a formula', weave_create_automation: 'create an automation rule', weave_automations: 'list, describe, update or delete rules', weave_export_csv: 'a table as CSV', weave_attach_file: 'attach or prepare raw upload', weave_files: 'read or delete an attached file', weave_update_space: 'rename a space; description, icon, template mark', weave_template_list: 'list template spaces', weave_template_use: 'copy a template space to a workspace', weave_delete_space: 'trash a space (hard: true purges)', weave_restore_space: 'restore a trashed space', weave_update_table: 'rename, icon, noun, field order, system fields, Σ row', weave_table_view: 'read and write a table\'s views', weave_move_table: 'move a table to another space', weave_duplicate_table: 'copy a table\'s schema, no records', weave_delete_table: 'trash a table (hard: true purges)', weave_restore_table: 'restore a trashed table', weave_rollback_field: 'restore a field\'s earlier config', weave_delete_field: 'trash a field (hard: true purges)', weave_restore_field: 'restore a trashed field', weave_apply_schema: 'apply a whole schema document (dryRun plans)', weave_views: 'saved multi-table pages', weave_shares: 'share links', weave_activity: 'the change feed', weave_audit: 'schema and account audit log', weave_accounts: 'accounts, tokens, sessions, require-auth', weave_keys: 'the credential keystore', weave_registry: 'registry drift report and rebuild', weave_relation_map: 'relation map as mermaid', weave_export_json: 'the workspace as JSON', weave_import_json: 'replace the workspace from a JSON export', weave_forms: 'create, configure and pause table forms', weave_form_submit: 'one record through a form', }; TOOLS.push({ name: 'weave_call', description: [ 'Call any weave tool by name: {name, args}, where args is that tool\'s input. {name: "help", args: {tool}} returns its description and input schema; read it before the first call. The tools not listed on their own:', ...TOOLS.filter((t) => !CORE_TOOLS.has(t.name)).map((t) => `${t.name}: ${SUMMARY[t.name] ?? t.description.split(/[.:]\s/)[0].slice(0, 60)}`), ].join('\n'), inputSchema: { type: 'object', properties: { name: { type: 'string', description: 'A tool name, or "help"' }, args: { type: 'object', description: 'That tool\'s arguments' } }, required: ['name'], }, }); const TITLES = { weave_ontology: 'Read schema outline', weave_schema: 'Read full schema', weave_query: 'Query records', weave_get_entity: 'Read record', weave_create_entity: 'Create record', weave_update_entity: 'Update record', weave_delete_entity: 'Delete record', weave_restore_entity: 'Restore record', weave_trash: 'List trash', weave_stats: 'Summarize table', weave_undo: 'Undo edits', weave_bulk: 'Edit records in bulk', weave_set_state: 'Set workflow state', weave_link: 'Link records', weave_unlink: 'Unlink records', weave_get_doc: 'Read document', weave_set_doc: 'Write document', weave_doc_revisions: 'List document revisions', weave_doc_restore: 'Restore document revision', weave_add_comment: 'Add comment', weave_delete_comment: 'Delete comment', weave_search: 'Search workspace', weave_create_space: 'Create space', weave_create_table: 'Create table', weave_add_field: 'Add field', weave_check_formula: 'Check formula', weave_add_relation: 'Add relation', weave_create_automation: 'Create automation', weave_export_csv: 'Export table as CSV', weave_import_csv: 'Import CSV records', weave_attach_file: 'Attach file', weave_vocabulary: 'Look up allowed values', weave_update_space: 'Update space', weave_template_list: 'List templates', weave_template_use: 'Use template', weave_delete_space: 'Delete space', weave_restore_space: 'Restore space', weave_update_table: 'Update table', weave_table_view: 'Read or write table views', weave_move_table: 'Move table', weave_duplicate_table: 'Duplicate table', weave_delete_table: 'Delete table', weave_restore_table: 'Restore table', weave_update_field: 'Update field', weave_rollback_field: 'Roll back field config', weave_delete_field: 'Delete field', weave_restore_field: 'Restore field', weave_apply_schema: 'Apply schema document', weave_build: 'Build workspace', weave_views: 'Manage saved views', weave_shares: 'Manage share links', weave_automations: 'Manage automations', weave_activity: 'Read activity feed', weave_audit: 'Read audit log', weave_workspace: 'Read or update workspace', weave_accounts: 'Manage accounts', weave_keys: 'Manage credential keys', weave_files: 'Read or delete files', weave_registry: 'Check workspace registry', weave_relation_map: 'Draw relation map', weave_export_json: 'Export workspace as JSON', weave_forms: 'Manage forms', weave_form_submit: 'Submit form', weave_import_json: 'Replace workspace from JSON', weave_call: 'Call any weave tool', }; const READ_ONLY = new Set([ 'weave_ontology', 'weave_schema', 'weave_query', 'weave_get_entity', 'weave_search', 'weave_stats', 'weave_trash', 'weave_get_doc', 'weave_doc_revisions', 'weave_check_formula', 'weave_export_csv', 'weave_export_json', 'weave_vocabulary', 'weave_template_list', 'weave_activity', 'weave_audit', 'weave_relation_map', ]); const DESTRUCTIVE = new Set([ 'weave_delete_entity', 'weave_delete_comment', 'weave_delete_space', 'weave_delete_table', 'weave_delete_field', 'weave_bulk', 'weave_undo', 'weave_set_doc', 'weave_doc_restore', 'weave_update_field', 'weave_rollback_field', 'weave_apply_schema', 'weave_import_json', 'weave_automations', 'weave_accounts', 'weave_keys', 'weave_files', 'weave_views', 'weave_shares', 'weave_workspace', 'weave_call', ]); const OPEN_WORLD = new Set(['weave_create_automation', 'weave_automations', 'weave_call']); const IDEMPOTENT = new Set([ ...READ_ONLY, 'weave_update_entity', 'weave_delete_entity', 'weave_restore_entity', 'weave_set_state', 'weave_link', 'weave_unlink', 'weave_doc_restore', 'weave_delete_comment', 'weave_update_space', 'weave_delete_space', 'weave_restore_space', 'weave_update_table', 'weave_table_view', 'weave_move_table', 'weave_delete_table', 'weave_restore_table', 'weave_update_field', 'weave_rollback_field', 'weave_delete_field', 'weave_restore_field', 'weave_apply_schema', 'weave_automations', 'weave_workspace', 'weave_keys', 'weave_files', 'weave_registry', 'weave_import_json', ]); for (const t of TOOLS) { t.annotations = { title: TITLES[t.name], readOnlyHint: READ_ONLY.has(t.name), destructiveHint: DESTRUCTIVE.has(t.name), idempotentHint: IDEMPOTENT.has(t.name), openWorldHint: OPEN_WORLD.has(t.name) }; } export function toolTable() { const effect = (a) => [a.readOnlyHint ? 'Read-only' : a.destructiveHint ? 'Destructive' : 'Adds or edits', ...(a.openWorldHint ? ['can call a webhook'] : [])].join(', '); return ['| Tool | What it does | Effect |', '| --- | --- | --- |', ...TOOLS.map((t) => `| \`${t.name}\` | ${t.annotations.title} | ${effect(t.annotations)} |`)].join('\n'); } export function toolProfile(env = process.env) { return env.WEAVE_MCP_TOOLS === 'all' ? 'all' : 'core'; } export function listTools(profile = toolProfile()) { return profile === 'all' ? TOOLS : TOOLS.filter((t) => CORE_TOOLS.has(t.name)); } const envOrigin = () => process.env.WEAVE_ORIGIN?.trim().replace(/\/+$/, '') ?? ''; function callTool(weave, name, rawArgs, caller) { if (name === 'weave_call') { checkArgs('weave_call', rawArgs); let inner = rawArgs.args ?? {}; if (typeof inner === 'string') inner = JSON.parse(inner); if (rawArgs.name === 'help') { const t = TOOLS.find((x) => x.name === inner.tool && mayCall(caller, x.name)); if (!t) throw new Error(`help needs args.tool, one of the names weave_call lists (got ${JSON.stringify(inner.tool ?? null)})`); return { name: t.name, description: t.description, inputSchema: t.inputSchema }; } if (!rawArgs.name || rawArgs.name === 'weave_call') throw new Error('weave_call needs name: a weave tool name, or "help"'); return callTool(weave, rawArgs.name, inner, caller); } const { verbose, ...args } = rawArgs; const result = dispatchTool(weave, name, COMPACT_TOOLS.has(name) ? args : rawArgs, { caller }); return verbose === true ? result : compactResult(name, result); } const systemWrite = (system) => new Error(system === 'users' ? 'Writing a Users record needs an architect token' : 'Writing a record of a system table needs an architect token'); function refuseUsersWrite(weave, ref, caller) { if (!caller || mayAdminister(weave, caller.role)) return; let system = null; try { system = weave.state.tables[weave.getEntity(ref).dbId]?.system; } catch {} if (system) throw systemWrite(system); } export function mayAdminister(on, role) { return Weave.roleName(role) === 'architect' || !on.listAccounts().length; } const ADMIN_TOOLS = { weave_accounts: 'workspace', weave_import_json: 'workspace', weave_keys: 'workspace', weave_shares: 'workspace' }; export const SEATLESS_TOOLS = new Set(['weave_ontology', 'weave_schema', 'weave_query', 'weave_get_entity', 'weave_search', 'weave_trash', 'weave_stats', 'weave_get_doc', 'weave_doc_revisions', 'weave_check_formula', 'weave_export_csv', 'weave_vocabulary', 'weave_template_list', 'weave_activity', 'weave_relation_map', 'weave_add_comment']); export const ROLE_TOOLS = { observer: new Set([...SEATLESS_TOOLS, 'weave_call']), editor: new Set([...SEATLESS_TOOLS, 'weave_call', 'weave_create_entity', 'weave_update_entity', 'weave_delete_entity', 'weave_attach_file']), }; const WORKSPACE_TOOLS = new Set(['weave_create_space', 'weave_build', 'weave_apply_schema', 'weave_template_use', 'weave_create_automation', 'weave_automations', 'weave_registry', 'weave_views', 'weave_workspace', 'weave_forms', 'weave_import_json', 'weave_export_json', 'weave_keys', 'weave_shares', 'weave_accounts', 'weave_undo']); const mayCall = (caller, name) => { const tier = WORKSPACE_TOOLS.has(name) ? caller?.workspaceTier ?? caller?.tier : caller?.tier; return !ROLE_TOOLS[tier] || ROLE_TOOLS[tier].has(name); }; const unknownTool = (name) => Object.assign(new Error(`Unknown tool: ${name}`), { code: -32601 }); const isMap = (v) => v != null && typeof v === 'object' && !Array.isArray(v); const toolProps = (name) => TOOLS.find((t) => t.name === name)?.inputSchema.properties; const unknownArgs = (props, args) => (isMap(args) ? Object.keys(args) : []).filter((k) => !Object.hasOwn(props, k) && k !== 'verbose'); const quoted = (keys) => keys.map((k) => `'${k}'`).join(', '); function checkArgs(name, args, tail = '') { const props = toolProps(name); const unknown = props ? unknownArgs(props, args) : []; if (unknown.length) throw new WeaveError(`${name} takes ${Object.keys(props).join(', ') || 'no arguments'}; unknown ${quoted(unknown)}${tail}`, 'invalid'); } function checkRowArgs(name, args, required) { const unknown = unknownArgs(toolProps(name), args); const extra = unknown.length ? `; unknown ${quoted(unknown)}` : ''; if (required && !isMap(args.values)) throw new WeaveError(`values is required: a map of field name to value, e.g. {"Name": "…"}${extra}`, 'invalid'); if (!required && args.values != null && !isMap(args.values)) throw new WeaveError(`values is a map of field name to value, e.g. {"Name": "…"}${extra}`, 'invalid'); checkArgs(name, args, '; field values go in values'); } const ROW_GUARDS = { weave_create_entity: (weave, args, caller) => { checkRowArgs('weave_create_entity', args, false); if (caller && !mayAdminister(weave, caller.role) && weave.findTable(args.db)?.system) throw systemWrite(weave.findTable(args.db).system); }, weave_update_entity: (weave, args, caller) => { checkRowArgs('weave_update_entity', args, true); refuseUsersWrite(weave, args.entity, caller); }, weave_delete_entity: (weave, args, caller) => refuseUsersWrite(weave, args.entity, caller), weave_views: (weave, args, caller) => { if (['share', 'unshare'].includes(args.action) && caller && !mayAdminister(weave, caller.role)) throw new Error('Sharing a view, or ending its link, needs an architect token'); }, }; export function dispatchTool(weave, name, args = {}, { caller = null } = {}) { if (!mayCall(caller, name)) throw unknownTool(name); if (caller?.seat && !SEATLESS_TOOLS.has(name)) throw new Error(caller.seat); if (caller && name === 'weave_keys' && !caller.role) throw new Error('Managing keys needs an account on this workspace'); if (caller && ADMIN_TOOLS[name]) { if ((weave.state.meta.requireAuth && !caller.role) || !mayAdminister(weave, caller.role)) throw new Error(`${name} needs an architect token on this workspace`); } if (caller && name === 'weave_forms' && ((weave.state.meta.requireAuth && !caller.role) || !mayAdminister(weave, caller.role))) throw new Error('Form management needs an architect token on this workspace'); weave.maybeRefresh?.(); ROW_GUARDS[name]?.(weave, args, caller); const command = toolCommand(name, args); const given = command?.toolArgs?.(args) ?? args; checkArgs(name, given); if (command) { const result = command.run(weave, given, { caller, origin: envOrigin(), door: 'mcp' }); return command.toolResult ? command.toolResult(result) : result; } switch (name) { case 'weave_forms': if (args.action === 'list') return listForms(weave); if (args.action === 'get') return getFormDefinition(weave, args.form); if (args.action === 'create') { if (args.definition?.kind !== undefined) throw new Error('Form management creates record forms'); return createForm(weave, args.definition); } if (args.action === 'update') return updateForm(weave, args.form, args.definition); throw new Error('Form action must be list, get, create or update'); case 'weave_form_submit': return submitForm(weave, args.form, { values: args.values, note: args.note, categories: args.categories, events: [] }, { actor: weave.actor, idempotencyKey: args.idempotencyKey, server: { version: VERSION, workspace: weave.state.meta.name } }); default: throw unknownTool(name); } } const PRIMER = readFileSync(new URL('./mcp-primer.md', import.meta.url), 'utf8'); export const primer = () => PRIMER; export function handleMcpMessage(weave, msg, { version = VERSION, caller = null, tools = toolProfile() } = {}) { const { id, method, params } = msg ?? {}; const reply = (result) => (id !== undefined ? { jsonrpc: '2.0', id, result } : null); const fail = (code, message) => (id !== undefined ? { jsonrpc: '2.0', id, error: { code, message } } : null); try { switch (method) { case 'initialize': weave.actor = 'mcp:' + (params?.clientInfo?.name ?? 'client'); return reply({ protocolVersion: params?.protocolVersion ?? PROTOCOL_VERSION, capabilities: { tools: {} }, serverInfo: { name: 'weave', version }, instructions: PRIMER, }); case 'notifications/initialized': case 'initialized': return null; case 'ping': return reply({}); case 'tools/list': return reply({ tools: listTools(tools).filter((t) => mayCall(caller, t.name)) }); case 'tools/call': { try { return reply(textResult(callTool(weave, params.name, params.arguments ?? {}, caller), storedSource(weave, params.name, params.arguments ?? {}))); } catch (err) { return reply(mcpToolError(err)); } } default: return fail(-32601, `Method not found: ${method}`); } } catch (err) { return fail(contractFor(err).mcp, err.message); } } export function startMcpServer(weave, { input = process.stdin, output = process.stdout, tools = toolProfile() } = {}) { let buffer = ''; const send = (msg) => output.write(JSON.stringify(msg) + '\n'); const handle = (msg) => { const response = handleMcpMessage(weave, msg, { tools }); if (response) send(response); }; input.setEncoding('utf8'); input.on('data', (chunk) => { buffer += chunk; let idx; while ((idx = buffer.indexOf('\n')) >= 0) { const line = buffer.slice(0, idx).trim(); buffer = buffer.slice(idx + 1); if (!line) continue; try { handle(JSON.parse(line)); } catch { send({ jsonrpc: '2.0', id: null, error: { code: -32700, message: 'Parse error' } }); } } }); }