Ctrl K
ComunidadGuías…

src/mcp.js

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
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 = `<weave_data table="${attr(source.table)}" entity="${attr(source.entity)}" field="${attr(source.field)}">`;
  return { content: [{ type: 'text', text: `${head}\n${seal(text)}\n</weave_data>` }] };
}

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' } });
      }
    }
  });
}