@anchrd/intel-api 0.14.0 → 0.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. package/dist/adapters/cloudflare/cloudflare.js +0 -68
  2. package/dist/adapters/cloudflare/cloudflare.types.d.ts +0 -39
  3. package/dist/adapters/db/db-flows.js +3 -2
  4. package/dist/adapters/db/db-grants.d.ts +1 -1
  5. package/dist/adapters/db/db-grants.js +1 -1
  6. package/dist/adapters/db/db.js +16 -113
  7. package/dist/adapters/openid/openid.js +1 -1
  8. package/dist/adapters/remote-tools/remote-tools.js +1 -1
  9. package/dist/bundle/bundle.js +31 -141
  10. package/dist/bundle/bundle.types.d.ts +1 -1
  11. package/dist/cli/cli.js +18 -11
  12. package/dist/flows/flows.d.ts +1 -1
  13. package/dist/flows/flows.js +1 -1
  14. package/dist/flows/flows.types.d.ts +4 -1
  15. package/dist/http/http.js +43 -238
  16. package/dist/http/http.types.d.ts +0 -8
  17. package/dist/indexing/indexing.js +18 -89
  18. package/dist/intel/intel.js +4 -9
  19. package/dist/intel/intel.types.d.ts +0 -6
  20. package/dist/mcp/mcp.js +84 -361
  21. package/dist/mcp/mcp.types.d.ts +2 -7
  22. package/dist/nodes/document-links/document-links.d.ts +6 -8
  23. package/dist/nodes/document-links/document-links.js +8 -31
  24. package/dist/nodes/nodes.js +24 -890
  25. package/dist/nodes/nodes.types.d.ts +14 -173
  26. package/dist/tools/tool-servers/tool-servers.d.ts +1 -1
  27. package/dist/tools/tool-servers/tool-servers.js +1 -1
  28. package/dist/tools/tools.js +37 -148
  29. package/dist/tools/tools.types.d.ts +1 -22
  30. package/migrations/0018_no_context_policy_at_last.sql +13 -6
  31. package/migrations/0019_one_name_for_the_grants.sql +52 -0
  32. package/package.json +2 -2
  33. package/dist/adapters/cloudflare-api/cloudflare-api.d.ts +0 -22
  34. package/dist/adapters/cloudflare-api/cloudflare-api.js +0 -306
  35. package/dist/adapters/cloudflare-api/cloudflare-api.types.d.ts +0 -64
  36. package/dist/adapters/cloudflare-api/cloudflare-api.types.js +0 -1
  37. package/dist/adapters/gate-applications/gate-applications.d.ts +0 -23
  38. package/dist/adapters/gate-applications/gate-applications.js +0 -88
  39. package/dist/adapters/tool-delegation/tool-delegation.d.ts +0 -22
  40. package/dist/adapters/tool-delegation/tool-delegation.js +0 -90
  41. package/dist/agent-costs/agent-costs.d.ts +0 -16
  42. package/dist/agent-costs/agent-costs.js +0 -105
  43. package/dist/agent-costs/agent-costs.types.d.ts +0 -30
  44. package/dist/agent-costs/agent-costs.types.js +0 -1
  45. package/dist/agent-runtime/agent-runtime.d.ts +0 -16
  46. package/dist/agent-runtime/agent-runtime.js +0 -150
  47. package/dist/agent-runtime/agent-runtime.types.d.ts +0 -122
  48. package/dist/agent-runtime/agent-runtime.types.js +0 -1
  49. package/dist/model-catalog/model-catalog.d.ts +0 -2
  50. package/dist/model-catalog/model-catalog.js +0 -99
  51. package/dist/model-catalog/model-catalog.types.d.ts +0 -15
  52. package/dist/model-catalog/model-catalog.types.js +0 -1
  53. package/dist/nodes/board/board.d.ts +0 -61
  54. package/dist/nodes/board/board.js +0 -826
  55. package/dist/nodes/board/board.types.d.ts +0 -38
  56. package/dist/nodes/board/board.types.js +0 -1
  57. package/migrations/0013_agents_in_the_tree.sql +0 -76
  58. package/migrations/0014_agent_applications.sql +0 -25
  59. package/migrations/0015_tools_delegated_from_a_connection.sql +0 -15
  60. package/migrations/0016_boards_in_the_tree.sql +0 -80
package/dist/mcp/mcp.js CHANGED
@@ -1,5 +1,11 @@
1
- import { AddBoardTaskInput, AppendTableRowsInput, ArchiveFlowInput, ArchiveNodeInput, CancelFlowRunInput, CompleteFlowRunStepInput, ConfigureBoardInput, CreateAgentInput, CreateFlowInput, CreateNodeInput, DefineTableInput, DeleteBoardTaskInput, DeleteTableRowsInput, ExecuteToolInput, GetAgentInput, GetBoardInput, GetFlowInput, GetFlowRunInput, GetFlowVersionInput, GetNodeInput, GetNodeVersionInput, GetTableInput, IntelId, ListAgentsInput, ListFlowRunsInput, ListFlowsInput, ListGrantsInput, ListNodesInput, MoveBoardTaskInput, NodeGraphInput, PauseAgentInput, PreviewFlowPublishInput, PublishFlowInput, RedefineTableInput, RelationGraphInput, RepairBoardTaskIdsInput, ResolveNodeLinksInput, RevokeGrantInput, RotateAgentKeyInput, RunAgentNowInput, SaveAgentDefinitionInput, SaveAttachmentInput, SaveFlowVersionInput, SaveNodeVersionInput, SearchInput, ShareInput, StartFlowRunInput, TestToolInput, UnpublishFlowInput, UpdateBoardTaskInput, UpdateFlowInput, UpdateNodeInput, UpdateTableRowsInput, } from "@anchrd/intel-contract";
2
- import { McpServer, ResourceTemplate, } from "@modelcontextprotocol/sdk/server/mcp.js";
1
+ import { IdempotencyKey, IntelId } from "@anchrd/intel-contract";
2
+ import { ArchiveFlowInput, CreateFlowInput, GetFlowInput, GetFlowVersionInput, ListFlowsInput, PreviewFlowPublishInput, PublishFlowInput, RelationGraphInput, SaveFlowVersionInput, UnpublishFlowInput, UpdateFlowInput, } from "@anchrd/intel-contract/flow";
3
+ import { CancelFlowRunInput, CompleteFlowRunStepInput, GetFlowRunInput, ListFlowRunsInput, StartFlowRunInput, } from "@anchrd/intel-contract/flow-run";
4
+ import { ArchiveNodeInput, CreateNodeInput, GetNodeInput, GetNodeVersionInput, ListNodesInput, NodeGraphInput, ResolveNodeLinksInput, SaveAttachmentInput, SaveNodeVersionInput, SearchInput, UpdateNodeInput, } from "@anchrd/intel-contract/node";
5
+ import { ListGrantsInput, RevokeGrantInput, ShareInput } from "@anchrd/intel-contract/share";
6
+ import { AppendTableRowsInput, DefineTableInput, DeleteTableRowsInput, GetTableInput, RedefineTableInput, UpdateTableRowsInput, } from "@anchrd/intel-contract/table";
7
+ import { ExecuteToolInput, TestToolInput } from "@anchrd/intel-contract/tool";
8
+ import { McpServer, ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
3
9
  import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js";
4
10
  import { z } from "zod";
5
11
  import { permits } from "../shared/gate-authorization/gate-authorization.js";
@@ -61,24 +67,24 @@ export async function handleMcp(request, deps) {
61
67
  canReadFlows: permits(deps.authorization, "flows", "read"),
62
68
  canCreateFlows: permits(deps.authorization, "flows", "create"),
63
69
  };
64
- const agentActor = {
65
- id: deps.authorization.identity.id,
66
- email: deps.authorization.identity.email,
67
- isAdmin: deps.authorization.can("intel", "admin"),
68
- canRun: permits(deps.authorization, "agents", "run"),
69
- };
70
70
  const server = new McpServer({ name: "intel", version: "0.1.0" });
71
71
  const EmptyInput = z.strictObject({});
72
72
  // `null` is the root: the whole tree as this caller may read it. The field is required rather
73
73
  // than defaulted so "everything" is always said, never fallen into.
74
- const ExportManifestInput = z.strictObject({ nodeId: IntelId.nullable() });
74
+ const ExportManifestInput = z.strictObject({
75
+ nodeId: IntelId.nullable().describe('Folder or node to plan the export of, or `null` for the whole tree as this caller may read it. Required rather than defaulted, so "everything" is always said and never fallen into.'),
76
+ });
75
77
  // The zip travels inline as base64, which is MCP's one way of carrying bytes. ~14M characters is
76
78
  // ~10 MB of zip — the same order as the attachment inline limit, and for the same isolate-memory
77
79
  // reason. Bigger bundles take the HTTP door, which streams.
78
80
  const ImportBundleInput = z.strictObject({
79
- nodeId: IntelId.nullable(),
80
- zipBase64: z.string().min(1).max(14_000_000),
81
- idempotencyKey: z.string().min(8).max(200),
81
+ nodeId: IntelId.nullable().describe("Folder the bundle lands under, or `null` for the top level. The bundle's own structure is kept beneath it."),
82
+ zipBase64: z
83
+ .string()
84
+ .min(1)
85
+ .max(14_000_000)
86
+ .describe("The zip bundle, base64-encoded. Around 10 MB of zip; a bigger bundle takes the HTTP door, which streams instead of holding it in memory."),
87
+ idempotencyKey: IdempotencyKey,
82
88
  });
83
89
  function decodeZipBase64(value) {
84
90
  if (value.length % 4 !== 0 || !/^[A-Za-z0-9+/]*={0,2}$/.test(value)) {
@@ -111,7 +117,7 @@ export async function handleMcp(request, deps) {
111
117
  email: deps.authorization.identity.email,
112
118
  name: deps.authorization.identity.name ?? null,
113
119
  }));
114
- if (permits(deps.authorization, "knowledge", "read")) {
120
+ if (permits(deps.authorization, "nodes", "read")) {
115
121
  server.registerTool("node_list", {
116
122
  title: "List nodes",
117
123
  description: "List authorized folders, documents, attachments, and tables under one parent.",
@@ -136,7 +142,7 @@ export async function handleMcp(request, deps) {
136
142
  openWorldHint: false,
137
143
  },
138
144
  }, async (input) => text(await deps.nodes.get(actor, input.nodeId)));
139
- server.registerTool("node_versions_list", {
145
+ server.registerTool("node_version_list", {
140
146
  title: "List node versions",
141
147
  description: "List immutable versions of one authorized node.",
142
148
  inputSchema: GetNodeInput,
@@ -213,33 +219,6 @@ export async function handleMcp(request, deps) {
213
219
  openWorldHint: false,
214
220
  },
215
221
  }, async (input) => text(await deps.nodes.search(actor, input)));
216
- // ⚠️ Plain `registerTool` and no alias: these tools are new with #139 and never had a
217
- // `knowledge_` name, so inventing one would create a deprecated name that was never used —
218
- // and #149, which removes the aliases, would then have one more thing to remove.
219
- server.registerTool("agent_list", {
220
- title: "List agents",
221
- description: "List the authorized agents, over the whole tree or under one folder. Omit parentId for every agent, pass null for the root level.",
222
- inputSchema: ListAgentsInput,
223
- annotations: {
224
- title: "List agents",
225
- readOnlyHint: true,
226
- destructiveHint: false,
227
- idempotentHint: true,
228
- openWorldHint: false,
229
- },
230
- }, async (input) => text(await deps.nodes.listAgents(actor, input)));
231
- server.registerTool("agent_get", {
232
- title: "Get agent",
233
- description: "Read one authorized agent with its current definition: the nodes it references and their role, its schedules, and its model.",
234
- inputSchema: GetAgentInput,
235
- annotations: {
236
- title: "Get agent",
237
- readOnlyHint: true,
238
- destructiveHint: false,
239
- idempotentHint: true,
240
- openWorldHint: false,
241
- },
242
- }, async (input) => text(await deps.nodes.getAgent(actor, input)));
243
222
  server.registerTool("node_table_get", {
244
223
  title: "Get node table",
245
224
  description: "Read one authorized table as column names and rows.",
@@ -252,22 +231,7 @@ export async function handleMcp(request, deps) {
252
231
  openWorldHint: false,
253
232
  },
254
233
  }, async (input) => text(await deps.nodes.getTable(actor, input.nodeId)));
255
- // A board answers whole: the statuses and every task, in one read. There is no per-task read
256
- // beside it, because a card is small and the interesting question is almost always about
257
- // several of them at once — what is in review, what waits for what (#285).
258
- server.registerTool("board_get", {
259
- title: "Get board",
260
- description: "Read one authorized board: its status list and every task with its full field set. A board that has never been written answers with the default statuses and no tasks.",
261
- inputSchema: GetBoardInput,
262
- annotations: {
263
- title: "Get board",
264
- readOnlyHint: true,
265
- destructiveHint: false,
266
- idempotentHint: true,
267
- openWorldHint: false,
268
- },
269
- }, async (input) => text(await deps.nodes.getBoard(actor, input)));
270
- server.registerTool("node_links_list", {
234
+ server.registerTool("node_link_list", {
271
235
  title: "List node links",
272
236
  description: "List authorized outgoing links and backlinks for one node.",
273
237
  inputSchema: GetNodeInput,
@@ -281,12 +245,12 @@ export async function handleMcp(request, deps) {
281
245
  }, async (input) => text(await deps.nodes.listLinks(actor, input.nodeId)));
282
246
  // The reader's half of a document link (#41): the titles of the linked documents this caller
283
247
  // may see. There is no tool to create or delete a link — a relationship is written in the text
284
- // and `node_save` is what records it, so there is only one way to make one.
248
+ // and `node_version_create` is what records it, so there is only one way to make one.
285
249
  //
286
250
  // ⚠️ A target this caller may not reach, or one that is gone, is simply absent from the answer.
287
251
  // The two are indistinguishable on purpose: telling them apart would confirm that a document
288
252
  // exists somewhere they cannot look.
289
- server.registerTool("node_links_resolve", {
253
+ server.registerTool("node_link_resolve", {
290
254
  title: "Resolve node links",
291
255
  description: "Resolve document link targets to the titles this caller is authorized to see. Targets that are unreachable or deleted are absent from the result.",
292
256
  inputSchema: ResolveNodeLinksInput,
@@ -316,12 +280,12 @@ export async function handleMcp(request, deps) {
316
280
  // resource payload travels inline as base64 and would have to fit in Worker memory twice, the
317
281
  // same limit that caps `node_attachment_get` at 10 MB — a whole subtree is the case that limit
318
282
  // exists for. HTTP `GET /nodes/:nodeId/export` streams the real archive.
319
- server.registerTool("node_export_manifest", {
320
- title: "Get export manifest",
283
+ server.registerTool("node_export", {
284
+ title: "Export nodes",
321
285
  description: "Plan a bundle export: the manifest of one authorized subtree (or the whole tree when nodeId is null) with each entry's bundle path, kind, and media type. Content bytes are fetched per node; the binary zip is served over HTTP.",
322
286
  inputSchema: ExportManifestInput,
323
287
  annotations: {
324
- title: "Get export manifest",
288
+ title: "Export nodes",
325
289
  readOnlyHint: true,
326
290
  destructiveHint: false,
327
291
  idempotentHint: true,
@@ -343,7 +307,7 @@ export async function handleMcp(request, deps) {
343
307
  },
344
308
  }, async () => text(await deps.nodes.reindex(actor)));
345
309
  }
346
- if (permits(deps.authorization, "knowledge", "create")) {
310
+ if (permits(deps.authorization, "nodes", "create")) {
347
311
  server.registerTool("node_create", {
348
312
  title: "Create node",
349
313
  description: "Create a governed folder, document, attachment, or table node.",
@@ -356,83 +320,26 @@ export async function handleMcp(request, deps) {
356
320
  openWorldHint: false,
357
321
  },
358
322
  }, async (input) => text(await deps.nodes.create(actor, input)));
359
- /**
360
- * ⚠️ There is no longer ANY tool result in Intel that contains a secret, and that is the whole
361
- * of D29 seen from here. Until #207 this one tool answered with the Application key in plain
362
- * text — a deliberate exception, because the result WAS the credential and a redacted answer
363
- * would have meant "an agent you can create but never run". The exception is gone rather than
364
- * weakened: the key now travels from Gate into the agent runtime over Intel's service binding
365
- * inside this call, so there is nothing left to hand to a model. `agent_create`, `agent_get`,
366
- * `agent_list` and every node read answer with `applicationId` alone, which names the principal
367
- * without authenticating it.
368
- */
369
- server.registerTool("agent_create", {
370
- title: "Create agent",
371
- description: "Create an agent node with its first definition, and the Gate application it runs as. The definition names nodes and their role, schedules and a model — never accounts, secrets or channels. The agent's application key is handed to the agent runtime internally and never appears in this result; the agent can run straight away. A repeated idempotency key returns the existing agent and mints nothing.",
372
- inputSchema: CreateAgentInput,
373
- annotations: {
374
- title: "Create agent",
375
- readOnlyHint: false,
376
- destructiveHint: false,
377
- idempotentHint: true,
378
- // Creating an agent creates a principal in Gate, which is a system outside Intel.
379
- openWorldHint: true,
380
- },
381
- }, async (input) => text(await deps.nodes.createAgent(actor, input, { token: deps.bearer })));
382
323
  }
383
- if (permits(deps.authorization, "knowledge", "write")) {
384
- server.registerTool("agent_update", {
385
- title: "Update agent definition",
386
- description: "Append an immutable definition version to an agent using an optimistic base version.",
387
- inputSchema: SaveAgentDefinitionInput,
388
- annotations: {
389
- title: "Update agent definition",
390
- readOnlyHint: false,
391
- destructiveHint: false,
392
- idempotentHint: true,
393
- openWorldHint: false,
394
- },
395
- }, async (input) => text(await deps.nodes.saveAgentDefinition(actor, input, { token: deps.bearer })));
396
- /**
397
- * The MCP half of the "Replace key" action in the agent profile (D29, #207).
398
- *
399
- * ⚠️ `knowledge/write` like `agent_update`, not `agents/run`: which principal an agent acts as
400
- * is a property of the agent, changed by whoever may change the agent. And the result names the
401
- * principal and the moment — never the key, which went to the runtime and nowhere else.
402
- */
403
- server.registerTool("agent_rotate_key", {
404
- title: "Replace an agent's application key",
405
- description: "Issue a new Gate application key for an agent and give it to the agent runtime. The previous key stops working immediately. Use it when a run fails with agent_key_missing, or when a key may have been exposed. The key itself is never returned.",
406
- inputSchema: RotateAgentKeyInput,
407
- annotations: {
408
- title: "Replace an agent's application key",
409
- readOnlyHint: false,
410
- // The old key stops working the moment this runs, and anything still using it breaks.
411
- destructiveHint: true,
412
- // Asking twice issues twice, and the second answer invalidates the first key.
413
- idempotentHint: false,
414
- // The principal lives in Gate, which is a system outside Intel.
415
- openWorldHint: true,
416
- },
417
- }, async (input) => text(await deps.nodes.rotateAgentKey(actor, input, { token: deps.bearer })));
418
- server.registerTool("node_save", {
419
- title: "Save node version",
324
+ if (permits(deps.authorization, "nodes", "write")) {
325
+ server.registerTool("node_version_create", {
326
+ title: "Create node version",
420
327
  description: "Append an immutable content version using an optimistic base version.",
421
328
  inputSchema: SaveNodeVersionInput,
422
329
  annotations: {
423
- title: "Save node version",
330
+ title: "Create node version",
424
331
  readOnlyHint: false,
425
332
  destructiveHint: false,
426
333
  idempotentHint: true,
427
334
  openWorldHint: false,
428
335
  },
429
336
  }, async (input) => text(await deps.nodes.save(actor, input)));
430
- server.registerTool("node_attachment_save", {
431
- title: "Save node attachment",
337
+ server.registerTool("node_attachment_create", {
338
+ title: "Create node attachment version",
432
339
  description: "Append immutable base64 file bytes to an attachment node.",
433
340
  inputSchema: SaveAttachmentInput,
434
341
  annotations: {
435
- title: "Save node attachment",
342
+ title: "Create node attachment version",
436
343
  readOnlyHint: false,
437
344
  destructiveHint: false,
438
345
  idempotentHint: true,
@@ -443,12 +350,12 @@ export async function handleMcp(request, deps) {
443
350
  // the export — or a naked zipped folder — lands as a new subtree under the target. Same
444
351
  // service as HTTP, so authorization, limits, remapping, and the all-or-nothing write cannot
445
352
  // differ by surface; only the transport differs (inline base64 here, a streamed body there).
446
- server.registerTool("node_import_bundle", {
447
- title: "Import bundle",
353
+ server.registerTool("node_import", {
354
+ title: "Import nodes",
448
355
  description: "Import a zip bundle (base64) as a new subtree under an authorized target folder — null files at the root. Kinds come from manifest.json when present, from file extensions otherwise. Always creates new nodes; links and flow references between bundled entries are rewritten to the new IDs. Bundles beyond ~10 MB take the streaming HTTP route POST /nodes/:nodeId/import.",
449
356
  inputSchema: ImportBundleInput,
450
357
  annotations: {
451
- title: "Import bundle",
358
+ title: "Import nodes",
452
359
  readOnlyHint: false,
453
360
  destructiveHint: false,
454
361
  idempotentHint: true,
@@ -459,12 +366,12 @@ export async function handleMcp(request, deps) {
459
366
  zip: decodeZipBase64(input.zipBase64),
460
367
  idempotencyKey: input.idempotencyKey,
461
368
  })));
462
- server.registerTool("node_table_define", {
463
- title: "Define node table columns",
369
+ server.registerTool("node_table_create", {
370
+ title: "Create node table columns",
464
371
  description: "Write the column names of an empty table. The header is the contract every append is checked against and cannot be rewritten.",
465
372
  inputSchema: DefineTableInput,
466
373
  annotations: {
467
- title: "Define node table columns",
374
+ title: "Create node table columns",
468
375
  readOnlyHint: false,
469
376
  destructiveHint: false,
470
377
  idempotentHint: true,
@@ -473,12 +380,12 @@ export async function handleMcp(request, deps) {
473
380
  }, async (input) => text(await deps.nodes.defineTable(actor, input)));
474
381
  // The tool #40 exists for: an agent collecting findings on a schedule appends them without
475
382
  // reading or resending what is already there, and two agents appending at once lose nothing.
476
- server.registerTool("node_table_append", {
477
- title: "Append node table rows",
383
+ server.registerTool("node_table_row_create", {
384
+ title: "Create node table rows",
478
385
  description: "Append rows to a table without reading or resending its existing content. Each row must have exactly as many cells as the table has columns; a row that does not is rejected and nothing is written.",
479
386
  inputSchema: AppendTableRowsInput,
480
387
  annotations: {
481
- title: "Append node table rows",
388
+ title: "Create node table rows",
482
389
  readOnlyHint: false,
483
390
  destructiveHint: false,
484
391
  // The idempotency key makes a repeat of the same call a no-op; without one, appending
@@ -491,7 +398,7 @@ export async function handleMcp(request, deps) {
491
398
  // rows — they carry no IDs on purpose — so every mutation names the version the positions were
492
399
  // read from and is refused with `version_conflict` when the table moved on. No `knowledge_*`
493
400
  // alias: these tools are new under the post-#125 naming and never carried the old prefix.
494
- server.registerTool("node_table_update_rows", {
401
+ server.registerTool("node_table_row_update", {
495
402
  title: "Update node table rows",
496
403
  description: "Replace rows at the given zero-based positions. Requires the baseVersionId the positions were read from (see node_table_get); answers version_conflict and writes nothing when the table has changed since. Each replacement row must match the column count.",
497
404
  inputSchema: UpdateTableRowsInput,
@@ -504,7 +411,7 @@ export async function handleMcp(request, deps) {
504
411
  openWorldHint: false,
505
412
  },
506
413
  }, async (input) => text(await deps.nodes.updateTableRows(actor, input)));
507
- server.registerTool("node_table_delete_rows", {
414
+ server.registerTool("node_table_row_delete", {
508
415
  title: "Delete node table rows",
509
416
  description: "Remove rows at the given zero-based positions. Requires the baseVersionId the positions were read from; answers version_conflict and writes nothing when the table has changed since.",
510
417
  inputSchema: DeleteTableRowsInput,
@@ -516,105 +423,18 @@ export async function handleMcp(request, deps) {
516
423
  openWorldHint: false,
517
424
  },
518
425
  }, async (input) => text(await deps.nodes.deleteTableRows(actor, input)));
519
- server.registerTool("node_table_redefine", {
520
- title: "Redefine node table columns",
426
+ server.registerTool("node_table_update", {
427
+ title: "Update node table columns",
521
428
  description: "Change the header of a defined table through an explicit column mapping: each new column names the current column that fills it (rename or keep), names none to start empty (add), and a current column no entry names is removed with its cells. Requires the baseVersionId the mapping was read from. Defining over an existing header without a mapping stays refused.",
522
429
  inputSchema: RedefineTableInput,
523
430
  annotations: {
524
- title: "Redefine node table columns",
431
+ title: "Update node table columns",
525
432
  readOnlyHint: false,
526
433
  destructiveHint: true,
527
434
  idempotentHint: true,
528
435
  openWorldHint: false,
529
436
  },
530
437
  }, async (input) => text(await deps.nodes.redefineTable(actor, input)));
531
- /**
532
- * The board's write surface (#285). No `knowledge_*` alias on any of them — these are new under
533
- * the post-#125 naming.
534
- *
535
- * ⚠️ Not one of them takes a `baseVersionId`, and the descriptions say so. That is the property
536
- * an agent has to be able to rely on: two agents working the same board at the same time touch
537
- * different tasks, and neither is asked to read the whole board first or to retry a conflict
538
- * that says nothing about what they were doing. The server applies each call by task id to the
539
- * board as it stands at that moment.
540
- */
541
- server.registerTool("board_configure", {
542
- title: "Configure board statuses",
543
- description: 'Write the board\'s whole status list, in the order it should be drawn. Adding, renaming and reordering columns are all this call. The "archived" status is part of every board and cannot be removed, and a status that tasks still sit in cannot be removed either. Set "terminal" on a column to say that standing in it means the work is finished — that is what decides whether a task waiting on another is still blocked. Several columns may be terminal; "archived" always is and cannot be set otherwise. Omitting it leaves an ordinary column not terminal.',
544
- inputSchema: ConfigureBoardInput,
545
- annotations: {
546
- title: "Configure board statuses",
547
- readOnlyHint: false,
548
- // Removing a column is possible here, so this is not a purely additive write.
549
- destructiveHint: true,
550
- idempotentHint: true,
551
- openWorldHint: false,
552
- },
553
- }, async (input) => text(await deps.nodes.configureBoard(actor, input)));
554
- server.registerTool("board_task_add", {
555
- title: "Add board task",
556
- description: "Add one task to a board. The server assigns its id and its place in the order; name afterTaskId or beforeTaskId to put it between two cards, or neither to put it last in its column. parentId makes it a subtask of another task on the same board, dependsOn names tasks of the same board it waits for, and references names Intel nodes. dependsOn, labels and references are sets: naming the same task, word or node twice is refused rather than quietly folded together. Needs no baseVersionId: tasks are addressed by id, so parallel writers do not collide.",
557
- inputSchema: AddBoardTaskInput,
558
- annotations: {
559
- title: "Add board task",
560
- readOnlyHint: false,
561
- destructiveHint: false,
562
- idempotentHint: true,
563
- openWorldHint: false,
564
- },
565
- }, async (input) => text(await deps.nodes.addBoardTask(actor, input)));
566
- server.registerTool("board_task_update", {
567
- title: "Update board task",
568
- description: "Change what a task says: title, assignee, labels, dates, dependencies, description or references. Where a task SITS — its status, its parent, its order — is board_task_move instead. Every named field replaces its current value whole; fields that are not named stay as they are. dependsOn, labels and references are sets: appending to a list this task already holds is refused rather than quietly folded together, so read the current value before you write it back.",
569
- inputSchema: UpdateBoardTaskInput,
570
- annotations: {
571
- title: "Update board task",
572
- readOnlyHint: false,
573
- // Every named field replaces what stood there; the version history keeps it, the task
574
- // does not.
575
- destructiveHint: true,
576
- idempotentHint: true,
577
- openWorldHint: false,
578
- },
579
- }, async (input) => text(await deps.nodes.updateBoardTask(actor, input)));
580
- server.registerTool("board_task_move", {
581
- title: "Move board task",
582
- description: 'Move one task: into another status, under another parent, or between two neighbours. Archiving a task is this call with status "archived" — there is no separate verb, and nothing is deleted by it. Two moves on two different tasks both go through.',
583
- inputSchema: MoveBoardTaskInput,
584
- annotations: {
585
- title: "Move board task",
586
- readOnlyHint: false,
587
- destructiveHint: false,
588
- idempotentHint: true,
589
- openWorldHint: false,
590
- },
591
- }, async (input) => text(await deps.nodes.moveBoardTask(actor, input)));
592
- server.registerTool("board_task_delete", {
593
- title: "Delete board task",
594
- description: 'Remove a task AND every subtask under it, recursively. The answer counts everything that went, and dependsOn entries in the remaining tasks that pointed at a deleted one are removed with it. To take a task off the board without losing it, move it to the "archived" status instead.',
595
- inputSchema: DeleteBoardTaskInput,
596
- annotations: {
597
- title: "Delete board task",
598
- readOnlyHint: false,
599
- destructiveHint: true,
600
- idempotentHint: true,
601
- openWorldHint: false,
602
- },
603
- }, async (input) => text(await deps.nodes.deleteBoardTask(actor, input)));
604
- server.registerTool("board_repair_task_ids", {
605
- title: "Repair repeated board task ids",
606
- description: "Give a fresh id to every task entry on a board that repeats an id another entry already used. Only a board imported from a hand-written or foreign bundle can hold such a pair, and it cannot be repaired with the other calls: they address a task by id, so they would read, write or remove both entries at once. The first entry under an id keeps it — that is the one every parentId and dependsOn on the board already means — and each later one becomes its own task with everything else it carries, so nothing is lost and no edge is invented. The answer names each pair. A board that names every task once is refused rather than rewritten. Repeated STATUS ids are repaired with board_configure instead, by writing the status list with distinct ids.",
607
- inputSchema: RepairBoardTaskIdsInput,
608
- annotations: {
609
- title: "Repair repeated board task ids",
610
- readOnlyHint: false,
611
- // Nothing is removed and nothing is overwritten: one entry gains an id of its own and
612
- // every other field on every task stays exactly as it stood.
613
- destructiveHint: false,
614
- idempotentHint: true,
615
- openWorldHint: false,
616
- },
617
- }, async (input) => text(await deps.nodes.repairBoardTaskIds(actor, input)));
618
438
  server.registerTool("node_update", {
619
439
  title: "Update node",
620
440
  description: "Rename, move, or describe a node.",
@@ -629,53 +449,53 @@ export async function handleMcp(request, deps) {
629
449
  }, async (input) => text(await deps.nodes.update(actor, input)));
630
450
  server.registerTool("node_archive", {
631
451
  title: "Archive node",
632
- description: "Archive or restore one node using optimistic concurrency. Archiving an agent also switches off its Gate application; restoring switches it back on.",
452
+ description: "Archive or restore one node. Archiving hides it from listings and search and takes its vectors out of the index; nothing is deleted and every version stays readable, so restoring is the same call with `archived: false`.",
633
453
  inputSchema: ArchiveNodeInput,
634
454
  annotations: {
635
455
  title: "Archive node",
636
456
  readOnlyHint: false,
637
457
  destructiveHint: true,
638
458
  idempotentHint: true,
639
- // The one node mutation that can reach a system outside Intel: archiving an agent
640
- // switches its Gate Application (#182). Nothing else here leaves the installation.
641
- openWorldHint: true,
459
+ // ⚠️ `true` until #418, and the reason is worth keeping: archiving an agent used to
460
+ // switch its Gate Application off (#182), which was the one node mutation that reached
461
+ // outside. With the agents parked nothing here leaves the installation — and an
462
+ // openWorldHint that is wrong in the cautious direction is still wrong, because it is
463
+ // the field a client decides on.
464
+ openWorldHint: false,
642
465
  },
643
- },
644
- // The caller's own bearer, exactly as the runtime proxy uses it and for the same reason:
645
- // switching a machine principal is an act in Gate, performed by the person asking.
646
- async (input) => text(await deps.nodes.archive(actor, input, { token: deps.bearer })));
466
+ }, async (input) => text(await deps.nodes.archive(actor, input)));
647
467
  }
648
- if (permits(deps.authorization, "knowledge", "share")) {
649
- server.registerTool("node_shares_list", {
650
- title: "List node shares",
468
+ if (permits(deps.authorization, "nodes", "share")) {
469
+ server.registerTool("node_grant_list", {
470
+ title: "List node grants",
651
471
  description: "List direct grants for nodes the caller is allowed to manage.",
652
472
  inputSchema: ListGrantsInput,
653
473
  annotations: {
654
- title: "List node shares",
474
+ title: "List node grants",
655
475
  readOnlyHint: true,
656
476
  destructiveHint: false,
657
477
  idempotentHint: true,
658
478
  openWorldHint: false,
659
479
  },
660
480
  }, async (input) => text(await deps.nodes.listGrants(actor, input.resourceId)));
661
- server.registerTool("node_share", {
662
- title: "Share node",
481
+ server.registerTool("node_grant_create", {
482
+ title: "Grant node access",
663
483
  description: "Grant inherited node access to a Gate user, verified email, or the organization.",
664
484
  inputSchema: ShareInput,
665
485
  annotations: {
666
- title: "Share node",
486
+ title: "Grant node access",
667
487
  readOnlyHint: false,
668
488
  destructiveHint: false,
669
489
  idempotentHint: true,
670
490
  openWorldHint: false,
671
491
  },
672
492
  }, async (input) => text(await deps.nodes.share(actor, input)));
673
- server.registerTool("node_revoke_share", {
674
- title: "Revoke node share",
493
+ server.registerTool("node_grant_revoke", {
494
+ title: "Revoke node grant",
675
495
  description: "Revoke one direct node grant by ID.",
676
496
  inputSchema: RevokeGrantInput,
677
497
  annotations: {
678
- title: "Revoke node share",
498
+ title: "Revoke node grant",
679
499
  readOnlyHint: false,
680
500
  destructiveHint: true,
681
501
  idempotentHint: true,
@@ -684,7 +504,7 @@ export async function handleMcp(request, deps) {
684
504
  }, async (input) => text(await deps.nodes.revokeGrant(actor, input)));
685
505
  }
686
506
  if (permits(deps.authorization, "flows", "read")) {
687
- server.registerTool("flows_list", {
507
+ server.registerTool("flow_list", {
688
508
  title: "List flows",
689
509
  description: "List authorized flows, either all of them or the ones filed in one folder of the shared tree.",
690
510
  inputSchema: ListFlowsInput,
@@ -711,7 +531,7 @@ export async function handleMcp(request, deps) {
711
531
  // Named like `knowledge_versions_list`, because it is the same question about the other kind of
712
532
  // thing in the tree. This pair is what turns the `versionId` that `flow_publish` and
713
533
  // `flow_publish_preview` demand into something an MCP client can actually obtain (#144).
714
- server.registerTool("flow_versions_list", {
534
+ server.registerTool("flow_version_list", {
715
535
  title: "List flow versions",
716
536
  description: "List the immutable versions of one authorized flow, oldest first, marking which of them is published. Metadata only; flow_version_get loads a version's graph.",
717
537
  inputSchema: GetFlowInput,
@@ -735,7 +555,7 @@ export async function handleMcp(request, deps) {
735
555
  openWorldHint: false,
736
556
  },
737
557
  }, async (input) => text(await deps.flows.getVersion(flowActor, input)));
738
- server.registerTool("flow_calls_list", {
558
+ server.registerTool("flow_call_list", {
739
559
  title: "List called flows",
740
560
  description: "List the flows one flow calls, read out of its graph rather than out of where it is filed.",
741
561
  inputSchema: GetFlowInput,
@@ -750,7 +570,7 @@ export async function handleMcp(request, deps) {
750
570
  // ⚠️ Only nodes the requesting user may see are in the answer, placeholders included: an edge to
751
571
  // a grey box would already say that something is there. The same rule the screen follows,
752
572
  // because it is the same service (#19).
753
- server.registerTool("flow_relation_graph", {
573
+ server.registerTool("flow_graph", {
754
574
  title: "Read the relation graph",
755
575
  description: "Read what accesses what for one folder or one flow: which flow reads which document and which flow calls which flow.",
756
576
  inputSchema: RelationGraphInput,
@@ -774,7 +594,7 @@ export async function handleMcp(request, deps) {
774
594
  openWorldHint: false,
775
595
  },
776
596
  }, async (input) => text(await deps.flows.validate(flowActor, input.flowId)));
777
- server.registerTool("flow_requirements_list", {
597
+ server.registerTool("flow_requirement_list", {
778
598
  title: "List what a flow needs",
779
599
  description: "List the documents and MCP tools one flow's graph names. Documents the calling user cannot see are counted rather than named, and no claim is made about whether anyone may reach them: for tools that cannot be known in advance, because the catalog is a live query with each user's own portal token.",
780
600
  inputSchema: GetFlowInput,
@@ -801,7 +621,7 @@ export async function handleMcp(request, deps) {
801
621
  // ⚠️ Model context like every other tool result: what a run did, never what it produced. The run
802
622
  // input and output are absent because a run reaches nodes and tools with the rights of
803
623
  // whoever started it, and only their own runs are the calling user's to read out that way.
804
- server.registerTool("flow_runs_list", {
624
+ server.registerTool("flow_run_list", {
805
625
  title: "List flow runs",
806
626
  description: "List the runs of one flow, newest first, with status, start, duration, what triggered them, and for a failed run the step that ended it. Optionally only the failed ones. Runs the calling user may not see are absent, and a failure inside a called flow they may not see is named by the calling step alone.",
807
627
  inputSchema: ListFlowRunsInput,
@@ -813,7 +633,7 @@ export async function handleMcp(request, deps) {
813
633
  openWorldHint: false,
814
634
  },
815
635
  }, async (input) => text(await deps.flows.listRuns(flowActor, input)));
816
- server.registerTool("flow_run_steps_list", {
636
+ server.registerTool("flow_run_step_list", {
817
637
  title: "List flow run steps",
818
638
  description: "List every completed step of one run, oldest first, with its outcome, branch, and the reason a failed step gives, plus the call chain the run belongs to.",
819
639
  inputSchema: GetFlowRunInput,
@@ -827,7 +647,7 @@ export async function handleMcp(request, deps) {
827
647
  }, async (input) => text(await deps.flows.listRunSteps(flowActor, input.runId)));
828
648
  }
829
649
  // A flow has no share tool of its own. Sharing happens on the folder a flow is filed in, through
830
- // node_share, so a narrower grant cannot sit beside the folder grant (ADR-0004 §2).
650
+ // node_grant_create, so a narrower grant cannot sit beside the folder grant (ADR-0004 §2).
831
651
  if (permits(deps.authorization, "flows", "create")) {
832
652
  server.registerTool("flow_create", {
833
653
  title: "Create flow",
@@ -869,12 +689,12 @@ export async function handleMcp(request, deps) {
869
689
  openWorldHint: false,
870
690
  },
871
691
  }, async (input) => text(await deps.flows.archive(flowActor, input)));
872
- server.registerTool("flow_save", {
873
- title: "Save flow",
692
+ server.registerTool("flow_version_create", {
693
+ title: "Create flow version",
874
694
  description: "Append a validated immutable flow graph version with optimistic concurrency.",
875
695
  inputSchema: SaveFlowVersionInput,
876
696
  annotations: {
877
- title: "Save flow",
697
+ title: "Create flow version",
878
698
  readOnlyHint: false,
879
699
  destructiveHint: false,
880
700
  idempotentHint: true,
@@ -938,7 +758,7 @@ export async function handleMcp(request, deps) {
938
758
  openWorldHint: false,
939
759
  },
940
760
  }, async (input) => text(await deps.flows.start(flowActor, input)));
941
- server.registerTool("flow_run_complete_step", {
761
+ server.registerTool("flow_run_step_complete", {
942
762
  title: "Complete flow step",
943
763
  description: "Submit a node result or semantic branch to advance a durable run without persisting credentials.",
944
764
  inputSchema: CompleteFlowRunStepInput,
@@ -965,103 +785,8 @@ export async function handleMcp(request, deps) {
965
785
  },
966
786
  }, async (input) => text(await deps.flows.cancelRun(flowActor, input)));
967
787
  }
968
- /**
969
- * Driving an agent, over the same door the screen uses.
970
- *
971
- * ⚠️ The three below are the MCP half of the two head actions on the agent page — the repository's
972
- * "one business layer, three thin surfaces" rule, kept in the same delivery. They call the runtime
973
- * through `deps.agents`, which resolves the resource ACL and forwards the CALLER's bearer; a tool
974
- * that reached the runtime with anything else would be Intel acting as somebody it is not.
975
- */
976
- if (permits(deps.authorization, "agents", "run")) {
977
- server.registerTool("agent_pause", {
978
- title: "Pause agent",
979
- description: "Stop an agent from acting on its own: no schedule fires, no task and no mail is taken. Its chat keeps answering, and nothing is queued up for later.",
980
- inputSchema: PauseAgentInput,
981
- annotations: {
982
- title: "Pause agent",
983
- readOnlyHint: false,
984
- // Nothing is deleted, and yet work that would have happened does not — a client deciding
985
- // whether to ask first has to be told that.
986
- destructiveHint: true,
987
- idempotentHint: true,
988
- openWorldHint: false,
989
- },
990
- }, async (input) => text(await deps.agents.request({
991
- actor: agentActor,
992
- token: deps.bearer,
993
- agentId: input.nodeId,
994
- method: "POST",
995
- path: "/pause",
996
- body: "{}",
997
- })));
998
- server.registerTool("agent_resume", {
999
- title: "Resume agent",
1000
- description: "Let a paused agent act again. Its schedules are armed from the definition as it stands now; nothing that was due while it stood still is caught up.",
1001
- inputSchema: PauseAgentInput,
1002
- annotations: {
1003
- title: "Resume agent",
1004
- readOnlyHint: false,
1005
- destructiveHint: false,
1006
- idempotentHint: true,
1007
- openWorldHint: false,
1008
- },
1009
- }, async (input) => text(await deps.agents.request({
1010
- actor: agentActor,
1011
- token: deps.bearer,
1012
- agentId: input.nodeId,
1013
- method: "POST",
1014
- path: "/resume",
1015
- body: "{}",
1016
- })));
1017
- /**
1018
- * ⚠️ The MCP half of the profile's cost figures, in the same delivery slice — the repository's
1019
- * "every user-visible capability has an Intel MCP equivalent" rule.
1020
- *
1021
- * ⚠️ Under `agents/run` beside pause and resume, not under `knowledge/read`. What an agent
1022
- * spends is as sensitive as what it did, and this answer stands beside the run list rather than
1023
- * beside the node.
1024
- *
1025
- * ⚠️ `status` travels with the numbers and is not flattened away. A client that read an empty
1026
- * `runs` list as "this agent is free" would be reporting a Cloudflare outage as a saving.
1027
- */
1028
- server.registerTool("agent_costs", {
1029
- title: "Agent costs",
1030
- description: "What one agent has actually cost, read from Cloudflare's AI Gateway log: per run, and as a total over the last 7 and 30 days. `status` says whether the numbers were read at all — `not_configured` and `unreadable` both mean no figures, never zero cost. `partial` means the read hit its page limit, so the totals are a floor.",
1031
- inputSchema: PauseAgentInput,
1032
- annotations: {
1033
- title: "Agent costs",
1034
- readOnlyHint: true,
1035
- destructiveHint: false,
1036
- idempotentHint: true,
1037
- openWorldHint: false,
1038
- },
1039
- }, async (input) => text(await deps.agentCosts.read(agentActor, input.nodeId)));
1040
- server.registerTool("agent_run_now", {
1041
- title: "Run agent now",
1042
- description: "Fire one of the agent's own schedules straight away, against the document or flow it names. The run appears in the agent's run list; a target the agent does not schedule is refused.",
1043
- inputSchema: RunAgentNowInput,
1044
- annotations: {
1045
- title: "Run agent now",
1046
- readOnlyHint: false,
1047
- destructiveHint: false,
1048
- // Every run is a new run, so asking twice runs it twice.
1049
- idempotentHint: false,
1050
- // The agent may reach whatever its own definition grants it, so what a run touches is not
1051
- // knowable from this schema.
1052
- openWorldHint: true,
1053
- },
1054
- }, async (input) => text(await deps.agents.request({
1055
- actor: agentActor,
1056
- token: deps.bearer,
1057
- agentId: input.nodeId,
1058
- method: "POST",
1059
- path: "/run",
1060
- body: JSON.stringify({ target: input.target }),
1061
- })));
1062
- }
1063
788
  if (permits(deps.authorization, "tools", "read")) {
1064
- server.registerTool("tools_catalog_list", {
789
+ server.registerTool("tool_list", {
1065
790
  title: "List tools",
1066
791
  description: "List the MCP tools the calling user can reach through the portal, live rather than cached.",
1067
792
  inputSchema: EmptyInput,
@@ -1073,12 +798,10 @@ export async function handleMcp(request, deps) {
1073
798
  openWorldHint: false,
1074
799
  },
1075
800
  }, async () => text(await deps.tools.catalog(toolActor)));
1076
- // The MCP twin of the picker in the agent profile (D30): whole servers, so an agent can be
1077
- // given tools from a client instead of from the screen. Assigning and removing is
1078
- // `agent_update` — a delegation is a field of the definition, not a surface of its own.
1079
- server.registerTool("tools_servers_list", {
801
+ // The servers behind the same live list (D30), as the portal itself names them.
802
+ server.registerTool("tool_server_list", {
1080
803
  title: "List tool servers",
1081
- description: "List the MCP servers the calling user reaches through the portal, as the portal itself names them. These are the handles an agent definition may delegate.",
804
+ description: "List the MCP servers the calling user reaches through the portal, as the portal itself names them. The handle is the prefix on every tool name from that server, which is how tool_execute is routed.",
1082
805
  inputSchema: EmptyInput,
1083
806
  annotations: {
1084
807
  title: "List tool servers",
@@ -1090,7 +813,7 @@ export async function handleMcp(request, deps) {
1090
813
  }, async () => text(await deps.tools.servers(toolActor)));
1091
814
  }
1092
815
  if (permits(deps.authorization, "tools", "test")) {
1093
- server.registerTool("tools_test", {
816
+ server.registerTool("tool_test", {
1094
817
  title: "Test tool",
1095
818
  description: "Validate input and execute one explicitly read-only cached MCP tool with the caller's Gate connection.",
1096
819
  inputSchema: TestToolInput,
@@ -1104,7 +827,7 @@ export async function handleMcp(request, deps) {
1104
827
  }, async (input) => text(await deps.tools.test(toolActor, input)));
1105
828
  }
1106
829
  if (permits(deps.authorization, "tools", "execute")) {
1107
- server.registerTool("tools_execute", {
830
+ server.registerTool("tool_execute", {
1108
831
  title: "Execute tool",
1109
832
  description: "Validate and execute a discovered MCP tool with the caller's just-in-time Gate connection.",
1110
833
  inputSchema: ExecuteToolInput,