@anchrd/intel-api 0.12.5 → 0.14.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.
package/dist/mcp/mcp.js CHANGED
@@ -1,4 +1,4 @@
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, ResolveNodeLinksInput, RevokeGrantInput, RotateAgentKeyInput, RunAgentNowInput, SaveAgentDefinitionInput, SaveAttachmentInput, SaveFlowVersionInput, SaveNodeVersionInput, SearchInput, ShareInput, StartFlowRunInput, TestToolInput, UnpublishFlowInput, UpdateBoardTaskInput, UpdateFlowInput, UpdateNodeInput, UpdateTableRowsInput, } from "@anchrd/intel-contract";
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
2
  import { McpServer, ResourceTemplate, } from "@modelcontextprotocol/sdk/server/mcp.js";
3
3
  import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js";
4
4
  import { z } from "zod";
@@ -8,18 +8,18 @@ function text(value) {
8
8
  return { content: [{ type: "text", text: JSON.stringify(value) }], isError: false };
9
9
  }
10
10
  /**
11
- * Registers one tool twice: under its name, and under the `knowledge_*` name it carried before #125.
11
+ * ⚠️ One name per tool, and the second one is gone for good (#149).
12
12
  *
13
- * ⚠️ The alias is not politeness. A tool name lives in someone else's configuration — a Portal
14
- * endpoint, an agent, a saved prompt and renaming it here alone breaks them at the moment we
15
- * deploy rather than at a moment they chose. Both entries share one handler and one schema, so
16
- * there is no second behavior to keep in step; only the name and the first sentence of the
17
- * description differ. The aliases are removed in anchrd/intel#149.
13
+ * Every tool here carried a `knowledge_*` alias beside its name from #125 until today. The alias
14
+ * was never politeness: a tool name lives in somebody else's configuration a portal endpoint, an
15
+ * agent definition, a saved prompt so renaming it here alone breaks them at the moment we deploy
16
+ * rather than at a moment they chose. ADR-0005 §8 therefore kept both, **for a transition**.
17
+ *
18
+ * A transition without an end date is not a transition, it is a second permanent name — and two
19
+ * names for one tool is exactly the friction #125 removed. It ends here. What follows for anybody
20
+ * writing a tool: there is one name, and a rename is a breaking change that needs its own ticket
21
+ * and its own notice, not a quiet second entry.
18
22
  */
19
- function registerWithAlias(server, names, config, handler) {
20
- server.registerTool(names.name, config, handler);
21
- server.registerTool(names.deprecated, { ...config, description: `Deprecated: use ${names.name}. ${config.description}` }, handler);
22
- }
23
23
  // MCP returns resource payloads inline as base64, so an attachment cannot be streamed to the client
24
24
  // and must fit in Worker memory twice over. Refuse oversized attachments instead of losing the
25
25
  // isolate; HTTP still serves them as a stream.
@@ -112,7 +112,7 @@ export async function handleMcp(request, deps) {
112
112
  name: deps.authorization.identity.name ?? null,
113
113
  }));
114
114
  if (permits(deps.authorization, "knowledge", "read")) {
115
- registerWithAlias(server, { name: "node_list", deprecated: "knowledge_list" }, {
115
+ server.registerTool("node_list", {
116
116
  title: "List nodes",
117
117
  description: "List authorized folders, documents, attachments, and tables under one parent.",
118
118
  inputSchema: ListNodesInput,
@@ -124,7 +124,7 @@ export async function handleMcp(request, deps) {
124
124
  openWorldHint: false,
125
125
  },
126
126
  }, async (input) => text(await deps.nodes.list(actor, input)));
127
- registerWithAlias(server, { name: "node_get", deprecated: "knowledge_get" }, {
127
+ server.registerTool("node_get", {
128
128
  title: "Get node",
129
129
  description: "Get one authorized node and its current immutable content version.",
130
130
  inputSchema: GetNodeInput,
@@ -136,7 +136,7 @@ export async function handleMcp(request, deps) {
136
136
  openWorldHint: false,
137
137
  },
138
138
  }, async (input) => text(await deps.nodes.get(actor, input.nodeId)));
139
- registerWithAlias(server, { name: "node_versions_list", deprecated: "knowledge_versions_list" }, {
139
+ server.registerTool("node_versions_list", {
140
140
  title: "List node versions",
141
141
  description: "List immutable versions of one authorized node.",
142
142
  inputSchema: GetNodeInput,
@@ -163,7 +163,7 @@ export async function handleMcp(request, deps) {
163
163
  openWorldHint: false,
164
164
  },
165
165
  }, async (input) => text(await deps.nodes.getVersion(actor, input.nodeId, input.versionId)));
166
- registerWithAlias(server, { name: "node_attachment_get", deprecated: "knowledge_attachment_get" }, {
166
+ server.registerTool("node_attachment_get", {
167
167
  title: "Get node attachment",
168
168
  description: "Get authorized immutable attachment metadata and its explicit MCP resource URI.",
169
169
  inputSchema: GetNodeInput,
@@ -199,7 +199,7 @@ export async function handleMcp(request, deps) {
199
199
  ],
200
200
  };
201
201
  });
202
- registerWithAlias(server, { name: "search", deprecated: "knowledge_search" }, {
202
+ server.registerTool("search", {
203
203
  title: "Search nodes",
204
204
  description: "Search only authorized indexed nodes and return version-pinned citations. Pass scopeId " +
205
205
  "with a folder node id to search that folder and everything beneath it instead of " +
@@ -240,7 +240,7 @@ export async function handleMcp(request, deps) {
240
240
  openWorldHint: false,
241
241
  },
242
242
  }, async (input) => text(await deps.nodes.getAgent(actor, input)));
243
- registerWithAlias(server, { name: "node_table_get", deprecated: "knowledge_table_get" }, {
243
+ server.registerTool("node_table_get", {
244
244
  title: "Get node table",
245
245
  description: "Read one authorized table as column names and rows.",
246
246
  inputSchema: GetTableInput,
@@ -267,7 +267,7 @@ export async function handleMcp(request, deps) {
267
267
  openWorldHint: false,
268
268
  },
269
269
  }, async (input) => text(await deps.nodes.getBoard(actor, input)));
270
- registerWithAlias(server, { name: "node_links_list", deprecated: "knowledge_links_list" }, {
270
+ server.registerTool("node_links_list", {
271
271
  title: "List node links",
272
272
  description: "List authorized outgoing links and backlinks for one node.",
273
273
  inputSchema: GetNodeInput,
@@ -286,7 +286,7 @@ export async function handleMcp(request, deps) {
286
286
  // ⚠️ A target this caller may not reach, or one that is gone, is simply absent from the answer.
287
287
  // The two are indistinguishable on purpose: telling them apart would confirm that a document
288
288
  // exists somewhere they cannot look.
289
- registerWithAlias(server, { name: "node_links_resolve", deprecated: "knowledge_links_resolve" }, {
289
+ server.registerTool("node_links_resolve", {
290
290
  title: "Resolve node links",
291
291
  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
292
  inputSchema: ResolveNodeLinksInput,
@@ -298,7 +298,7 @@ export async function handleMcp(request, deps) {
298
298
  openWorldHint: false,
299
299
  },
300
300
  }, async (input) => text(await deps.nodes.resolveLinks(actor, input)));
301
- registerWithAlias(server, { name: "node_graph", deprecated: "knowledge_graph" }, {
301
+ server.registerTool("node_graph", {
302
302
  title: "Get node graph",
303
303
  description: "Get the authorized nodes and explicit relationships for discovery.",
304
304
  inputSchema: NodeGraphInput,
@@ -330,7 +330,7 @@ export async function handleMcp(request, deps) {
330
330
  }, async (input) => text(await deps.bundle.manifest(bundleActor, input.nodeId)));
331
331
  }
332
332
  if (deps.authorization.can("intel", "admin")) {
333
- registerWithAlias(server, { name: "node_reindex", deprecated: "knowledge_reindex" }, {
333
+ server.registerTool("node_reindex", {
334
334
  title: "Reindex nodes",
335
335
  description: "Queue every current canonical node version for derived index rebuild.",
336
336
  inputSchema: EmptyInput,
@@ -344,7 +344,7 @@ export async function handleMcp(request, deps) {
344
344
  }, async () => text(await deps.nodes.reindex(actor)));
345
345
  }
346
346
  if (permits(deps.authorization, "knowledge", "create")) {
347
- registerWithAlias(server, { name: "node_create", deprecated: "knowledge_create" }, {
347
+ server.registerTool("node_create", {
348
348
  title: "Create node",
349
349
  description: "Create a governed folder, document, attachment, or table node.",
350
350
  inputSchema: CreateNodeInput,
@@ -415,7 +415,7 @@ export async function handleMcp(request, deps) {
415
415
  openWorldHint: true,
416
416
  },
417
417
  }, async (input) => text(await deps.nodes.rotateAgentKey(actor, input, { token: deps.bearer })));
418
- registerWithAlias(server, { name: "node_save", deprecated: "knowledge_save" }, {
418
+ server.registerTool("node_save", {
419
419
  title: "Save node version",
420
420
  description: "Append an immutable content version using an optimistic base version.",
421
421
  inputSchema: SaveNodeVersionInput,
@@ -427,7 +427,7 @@ export async function handleMcp(request, deps) {
427
427
  openWorldHint: false,
428
428
  },
429
429
  }, async (input) => text(await deps.nodes.save(actor, input)));
430
- registerWithAlias(server, { name: "node_attachment_save", deprecated: "knowledge_attachment_save" }, {
430
+ server.registerTool("node_attachment_save", {
431
431
  title: "Save node attachment",
432
432
  description: "Append immutable base64 file bytes to an attachment node.",
433
433
  inputSchema: SaveAttachmentInput,
@@ -459,7 +459,7 @@ export async function handleMcp(request, deps) {
459
459
  zip: decodeZipBase64(input.zipBase64),
460
460
  idempotencyKey: input.idempotencyKey,
461
461
  })));
462
- registerWithAlias(server, { name: "node_table_define", deprecated: "knowledge_table_define" }, {
462
+ server.registerTool("node_table_define", {
463
463
  title: "Define node table columns",
464
464
  description: "Write the column names of an empty table. The header is the contract every append is checked against and cannot be rewritten.",
465
465
  inputSchema: DefineTableInput,
@@ -473,7 +473,7 @@ export async function handleMcp(request, deps) {
473
473
  }, async (input) => text(await deps.nodes.defineTable(actor, input)));
474
474
  // The tool #40 exists for: an agent collecting findings on a schedule appends them without
475
475
  // reading or resending what is already there, and two agents appending at once lose nothing.
476
- registerWithAlias(server, { name: "node_table_append", deprecated: "knowledge_table_append" }, {
476
+ server.registerTool("node_table_append", {
477
477
  title: "Append node table rows",
478
478
  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
479
  inputSchema: AppendTableRowsInput,
@@ -540,7 +540,7 @@ export async function handleMcp(request, deps) {
540
540
  */
541
541
  server.registerTool("board_configure", {
542
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.',
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
544
  inputSchema: ConfigureBoardInput,
545
545
  annotations: {
546
546
  title: "Configure board statuses",
@@ -553,7 +553,7 @@ export async function handleMcp(request, deps) {
553
553
  }, async (input) => text(await deps.nodes.configureBoard(actor, input)));
554
554
  server.registerTool("board_task_add", {
555
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. Needs no baseVersionId: tasks are addressed by id, so parallel writers do not collide.",
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
557
  inputSchema: AddBoardTaskInput,
558
558
  annotations: {
559
559
  title: "Add board task",
@@ -565,7 +565,7 @@ export async function handleMcp(request, deps) {
565
565
  }, async (input) => text(await deps.nodes.addBoardTask(actor, input)));
566
566
  server.registerTool("board_task_update", {
567
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.",
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
569
  inputSchema: UpdateBoardTaskInput,
570
570
  annotations: {
571
571
  title: "Update board task",
@@ -601,7 +601,21 @@ export async function handleMcp(request, deps) {
601
601
  openWorldHint: false,
602
602
  },
603
603
  }, async (input) => text(await deps.nodes.deleteBoardTask(actor, input)));
604
- registerWithAlias(server, { name: "node_update", deprecated: "knowledge_update" }, {
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
+ server.registerTool("node_update", {
605
619
  title: "Update node",
606
620
  description: "Rename, move, or describe a node.",
607
621
  inputSchema: UpdateNodeInput,
@@ -613,7 +627,7 @@ export async function handleMcp(request, deps) {
613
627
  openWorldHint: false,
614
628
  },
615
629
  }, async (input) => text(await deps.nodes.update(actor, input)));
616
- registerWithAlias(server, { name: "node_archive", deprecated: "knowledge_archive" }, {
630
+ server.registerTool("node_archive", {
617
631
  title: "Archive node",
618
632
  description: "Archive or restore one node using optimistic concurrency. Archiving an agent also switches off its Gate application; restoring switches it back on.",
619
633
  inputSchema: ArchiveNodeInput,
@@ -632,7 +646,7 @@ export async function handleMcp(request, deps) {
632
646
  async (input) => text(await deps.nodes.archive(actor, input, { token: deps.bearer })));
633
647
  }
634
648
  if (permits(deps.authorization, "knowledge", "share")) {
635
- registerWithAlias(server, { name: "node_shares_list", deprecated: "knowledge_shares_list" }, {
649
+ server.registerTool("node_shares_list", {
636
650
  title: "List node shares",
637
651
  description: "List direct grants for nodes the caller is allowed to manage.",
638
652
  inputSchema: ListGrantsInput,
@@ -644,7 +658,7 @@ export async function handleMcp(request, deps) {
644
658
  openWorldHint: false,
645
659
  },
646
660
  }, async (input) => text(await deps.nodes.listGrants(actor, input.resourceId)));
647
- registerWithAlias(server, { name: "node_share", deprecated: "knowledge_share" }, {
661
+ server.registerTool("node_share", {
648
662
  title: "Share node",
649
663
  description: "Grant inherited node access to a Gate user, verified email, or the organization.",
650
664
  inputSchema: ShareInput,
@@ -656,7 +670,7 @@ export async function handleMcp(request, deps) {
656
670
  openWorldHint: false,
657
671
  },
658
672
  }, async (input) => text(await deps.nodes.share(actor, input)));
659
- registerWithAlias(server, { name: "node_revoke_share", deprecated: "knowledge_revoke_share" }, {
673
+ server.registerTool("node_revoke_share", {
660
674
  title: "Revoke node share",
661
675
  description: "Revoke one direct node grant by ID.",
662
676
  inputSchema: RevokeGrantInput,
@@ -1,4 +1,50 @@
1
+ import { type BoardDocument } from "@anchrd/intel-contract";
1
2
  import type { BoardDeps, BoardOperations } from "./board.types.js";
3
+ /**
4
+ * A stored board brought up to the current schema, before it is validated.
5
+ *
6
+ * ⚠️ It runs before `BoardDocument.parse`, on `unknown`, because after that parse a missing field or
7
+ * a repeated id is already a refusal — and `parseStoredBoard` is deliberately loud
8
+ * (`board_unreadable`), so an un-upgraded board would not read as "old", it would read as corrupt.
9
+ *
10
+ * The upgrade is persisted by the next write of any kind, because every write serialises the whole
11
+ * document. Nothing has to be migrated ahead of time and nothing has to be re-migrated.
12
+ */
13
+ export declare function upgradeStoredBoard(parsed: unknown): unknown;
14
+ /**
15
+ * The first id a board document uses for two different things (anchrd/intel#321).
16
+ *
17
+ * ⚠️ Deliberately NOT a rule on `BoardDocument`, and that is the whole decision. A schema rule would
18
+ * reach every parse of a stored body — `parseStoredBoard`, `indexing.ts`, the link reader — and a
19
+ * board imported before this existed would stop answering with `board_unreadable`: the whole board
20
+ * gone over something that costs one view. It is the trap #311 walked into, and #318 walked into
21
+ * from the other side.
22
+ *
23
+ * ⚠️ And NOT folded in `upgradeStoredBoard` either, which is where the sibling rules of #318 went. A
24
+ * repeated `dependsOn`, label or reference carries no information, so dropping the second mention
25
+ * loses nothing. Two tasks under one id are two whole tasks — titles, dates, descriptions — and a
26
+ * fold on the READ is written back by the next save of any kind, because every write serialises the
27
+ * whole document. That is one task gone for good, for every board, without anybody having asked.
28
+ * The UI folds the same pair for a DRAWING (`orderedTasks`), which costs nothing: the document keeps
29
+ * both, and repairing the pair brings the second card back.
30
+ *
31
+ * ⚠️ What this does NOT rescue, and it is worth knowing: `replaced` matches a task by id, so the
32
+ * first `board_task_update` against such a pair writes the SAME task into both entries. The stored
33
+ * board is then two identical tasks rather than two different ones. That is a consequence of
34
+ * addressing a board by task id at all (#285) and predates this rule; the reason the rule is at the
35
+ * import is to stop the pair from existing, not to make it survivable. A board that already holds
36
+ * one is repaired by `repairTaskIds` below — asked for, never done on the quiet — and the refusal
37
+ * here names it, because "your file is wrong" without a way out is where anchrd/intel#341 started.
38
+ *
39
+ * So it is asked exactly where there is a caller to answer: the bundle import, the one door a board
40
+ * document written elsewhere comes in through. Everything else mints task ids itself (`deps.id()`)
41
+ * and takes its status list through `ConfigureBoardInput`, which has demanded distinct ids since
42
+ * #285.
43
+ */
44
+ export declare function repeatedBoardId(document: BoardDocument): {
45
+ list: "statuses" | "tasks";
46
+ id: string;
47
+ } | null;
2
48
  /**
3
49
  * Every board operation, applied to the document rather than to the file (#285).
4
50
  *