@anchrd/intel-api 0.6.7 → 0.9.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 (58) hide show
  1. package/README.md +63 -3
  2. package/dist/adapters/cloudflare/cloudflare.js +102 -37
  3. package/dist/adapters/cloudflare/cloudflare.types.d.ts +20 -0
  4. package/dist/adapters/content/content.d.ts +1 -1
  5. package/dist/adapters/db/db-flows.js +148 -20
  6. package/dist/adapters/db/db-grants.d.ts +13 -2
  7. package/dist/adapters/db/db-grants.js +25 -8
  8. package/dist/adapters/db/db-indexing.d.ts +2 -2
  9. package/dist/adapters/db/db-indexing.js +26 -19
  10. package/dist/adapters/db/db.d.ts +3 -3
  11. package/dist/adapters/db/db.js +442 -118
  12. package/dist/adapters/gate-applications/gate-applications.d.ts +23 -0
  13. package/dist/adapters/gate-applications/gate-applications.js +88 -0
  14. package/dist/adapters/index-queue/index-queue.d.ts +1 -1
  15. package/dist/adapters/index-queue/index-queue.js +2 -2
  16. package/dist/adapters/semantic-index/semantic-index.types.d.ts +2 -2
  17. package/dist/adapters/tool-delegation/tool-delegation.d.ts +22 -0
  18. package/dist/adapters/tool-delegation/tool-delegation.js +90 -0
  19. package/dist/agent-runtime/agent-runtime.d.ts +16 -0
  20. package/dist/agent-runtime/agent-runtime.js +150 -0
  21. package/dist/agent-runtime/agent-runtime.types.d.ts +122 -0
  22. package/dist/bundle/bundle.d.ts +4 -0
  23. package/dist/bundle/bundle.js +1048 -0
  24. package/dist/bundle/bundle.types.d.ts +33 -0
  25. package/dist/bundle/bundle.types.js +1 -0
  26. package/dist/cli/cli.js +10 -1
  27. package/dist/flows/flows.d.ts +8 -8
  28. package/dist/flows/flows.js +158 -42
  29. package/dist/flows/flows.types.d.ts +40 -7
  30. package/dist/http/http.d.ts +1 -0
  31. package/dist/http/http.js +348 -61
  32. package/dist/http/http.types.d.ts +6 -2
  33. package/dist/indexing/indexing.js +14 -2
  34. package/dist/indexing/indexing.types.d.ts +2 -2
  35. package/dist/intel/intel.js +12 -3
  36. package/dist/intel/intel.types.d.ts +6 -2
  37. package/dist/mcp/mcp.js +519 -124
  38. package/dist/mcp/mcp.types.d.ts +11 -2
  39. package/dist/nodes/nodes.d.ts +2 -0
  40. package/dist/nodes/nodes.js +1466 -0
  41. package/dist/nodes/nodes.types.d.ts +402 -0
  42. package/dist/nodes/nodes.types.js +1 -0
  43. package/dist/tools/tool-servers/tool-servers.d.ts +46 -0
  44. package/dist/tools/tool-servers/tool-servers.js +114 -0
  45. package/dist/tools/tools.js +190 -31
  46. package/dist/tools/tools.types.d.ts +23 -1
  47. package/migrations/0011_one_name_for_the_tree.sql +53 -0
  48. package/migrations/0012_table_snapshots.sql +29 -0
  49. package/migrations/0013_agents_in_the_tree.sql +76 -0
  50. package/migrations/0014_agent_applications.sql +25 -0
  51. package/migrations/0015_tools_delegated_from_a_connection.sql +15 -0
  52. package/package.json +3 -2
  53. package/dist/knowledge/knowledge.d.ts +0 -2
  54. package/dist/knowledge/knowledge.js +0 -761
  55. package/dist/knowledge/knowledge.types.d.ts +0 -198
  56. /package/dist/{knowledge/knowledge.types.js → agent-runtime/agent-runtime.types.js} +0 -0
  57. /package/dist/{knowledge → nodes}/document-links/document-links.d.ts +0 -0
  58. /package/dist/{knowledge → nodes}/document-links/document-links.js +0 -0
package/dist/mcp/mcp.js CHANGED
@@ -1,5 +1,5 @@
1
- import { AppendKnowledgeTableRowsInput, ArchiveFlowInput, ArchiveKnowledgeNodeInput, CompleteFlowRunStepInput, CreateFlowInput, CreateKnowledgeNodeInput, DefineKnowledgeTableInput, ExecuteToolInput, GetFlowInput, GetFlowRunInput, GetKnowledgeNodeInput, GetKnowledgeTableInput, KnowledgeGraphInput, ListFlowRunsInput, ListFlowsInput, ListKnowledgeGrantsInput, ListKnowledgeNodesInput, PreviewFlowPublishInput, PublishFlowInput, RelationGraphInput, ResolveKnowledgeLinksInput, RevokeKnowledgeGrantInput, SaveFlowVersionInput, SaveKnowledgeAttachmentInput, SaveKnowledgeVersionInput, SearchKnowledgeInput, ShareKnowledgeInput, StartFlowRunInput, TestToolInput, UpdateFlowInput, UpdateKnowledgeNodeInput, } from "@anchrd/intel-contract";
2
- import { McpServer, ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
1
+ import { AppendTableRowsInput, ArchiveFlowInput, ArchiveNodeInput, CancelFlowRunInput, CompleteFlowRunStepInput, CreateAgentInput, CreateFlowInput, CreateNodeInput, DefineTableInput, DeleteTableRowsInput, ExecuteToolInput, GetAgentInput, GetFlowInput, GetFlowRunInput, GetFlowVersionInput, GetNodeInput, GetNodeVersionInput, GetTableInput, IntelId, ListAgentsInput, ListFlowRunsInput, ListFlowsInput, ListGrantsInput, ListNodesInput, NodeGraphInput, PauseAgentInput, PreviewFlowPublishInput, PublishFlowInput, RedefineTableInput, RelationGraphInput, ResolveNodeLinksInput, RevokeGrantInput, RotateAgentKeyInput, RunAgentNowInput, SaveAgentDefinitionInput, SaveAttachmentInput, SaveFlowVersionInput, SaveNodeVersionInput, SearchInput, ShareInput, StartFlowRunInput, TestToolInput, UnpublishFlowInput, UpdateFlowInput, UpdateNodeInput, UpdateTableRowsInput, } from "@anchrd/intel-contract";
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";
5
5
  import { permits } from "../shared/gate-authorization/gate-authorization.js";
@@ -7,6 +7,19 @@ import { IntelError } from "../shared/intel-error/intel-error.js";
7
7
  function text(value) {
8
8
  return { content: [{ type: "text", text: JSON.stringify(value) }], isError: false };
9
9
  }
10
+ /**
11
+ * Registers one tool twice: under its name, and under the `knowledge_*` name it carried before #125.
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.
18
+ */
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
+ }
10
23
  // MCP returns resource payloads inline as base64, so an attachment cannot be streamed to the client
11
24
  // and must fit in Worker memory twice over. Refuse oversized attachments instead of losing the
12
25
  // isolate; HTTP still serves them as a stream.
@@ -41,59 +54,129 @@ export async function handleMcp(request, deps) {
41
54
  canRun: permits(deps.authorization, "flows", "run"),
42
55
  isAdmin: deps.authorization.can("intel", "admin"),
43
56
  };
57
+ const bundleActor = {
58
+ id: deps.authorization.identity.id,
59
+ email: deps.authorization.identity.email,
60
+ isAdmin: deps.authorization.can("intel", "admin"),
61
+ canReadFlows: permits(deps.authorization, "flows", "read"),
62
+ canCreateFlows: permits(deps.authorization, "flows", "create"),
63
+ };
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
+ };
44
70
  const server = new McpServer({ name: "intel", version: "0.1.0" });
45
71
  const EmptyInput = z.strictObject({});
72
+ // `null` is the root: the whole tree as this caller may read it. The field is required rather
73
+ // than defaulted so "everything" is always said, never fallen into.
74
+ const ExportManifestInput = z.strictObject({ nodeId: IntelId.nullable() });
75
+ // The zip travels inline as base64, which is MCP's one way of carrying bytes. ~14M characters is
76
+ // ~10 MB of zip — the same order as the attachment inline limit, and for the same isolate-memory
77
+ // reason. Bigger bundles take the HTTP door, which streams.
78
+ 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),
82
+ });
83
+ function decodeZipBase64(value) {
84
+ if (value.length % 4 !== 0 || !/^[A-Za-z0-9+/]*={0,2}$/.test(value)) {
85
+ throw new IntelError(400, "import_invalid_bundle", "zipBase64 is not valid base64");
86
+ }
87
+ try {
88
+ return Uint8Array.from(atob(value), (character) => character.charCodeAt(0));
89
+ }
90
+ catch {
91
+ throw new IntelError(400, "import_invalid_bundle", "zipBase64 is not valid base64");
92
+ }
93
+ }
94
+ // The caller's own identity (#148): the MCP twin of `GET /session`, and like it behind no
95
+ // capability at all — the answer is the token's own subject, and an agent that may connect may
96
+ // ask who it is connected as. Deliberately id, email, and name and nothing else: no
97
+ // capabilities, no token, nothing a caller could mistake for a permission.
98
+ server.registerTool("session_get", {
99
+ title: "Get session identity",
100
+ description: "Get the identity this connection acts as: user id, email, and display name. The MCP counterpart of GET /session; it answers from the token's own subject and never lists permissions.",
101
+ inputSchema: EmptyInput,
102
+ annotations: {
103
+ title: "Get session identity",
104
+ readOnlyHint: true,
105
+ destructiveHint: false,
106
+ idempotentHint: true,
107
+ openWorldHint: false,
108
+ },
109
+ }, async () => text({
110
+ id: deps.authorization.identity.id,
111
+ email: deps.authorization.identity.email,
112
+ name: deps.authorization.identity.name ?? null,
113
+ }));
46
114
  if (permits(deps.authorization, "knowledge", "read")) {
47
- server.registerTool("knowledge_list", {
48
- title: "List knowledge",
115
+ registerWithAlias(server, { name: "node_list", deprecated: "knowledge_list" }, {
116
+ title: "List nodes",
49
117
  description: "List authorized folders, documents, attachments, and tables under one parent.",
50
- inputSchema: ListKnowledgeNodesInput,
118
+ inputSchema: ListNodesInput,
51
119
  annotations: {
52
- title: "List knowledge",
120
+ title: "List nodes",
53
121
  readOnlyHint: true,
54
122
  destructiveHint: false,
55
123
  idempotentHint: true,
56
124
  openWorldHint: false,
57
125
  },
58
- }, async (input) => text(await deps.knowledge.list(actor, input)));
59
- server.registerTool("knowledge_get", {
60
- title: "Get knowledge",
61
- description: "Get one authorized Knowledge node and its current immutable content version.",
62
- inputSchema: GetKnowledgeNodeInput,
126
+ }, async (input) => text(await deps.nodes.list(actor, input)));
127
+ registerWithAlias(server, { name: "node_get", deprecated: "knowledge_get" }, {
128
+ title: "Get node",
129
+ description: "Get one authorized node and its current immutable content version.",
130
+ inputSchema: GetNodeInput,
63
131
  annotations: {
64
- title: "Get knowledge",
132
+ title: "Get node",
65
133
  readOnlyHint: true,
66
134
  destructiveHint: false,
67
135
  idempotentHint: true,
68
136
  openWorldHint: false,
69
137
  },
70
- }, async (input) => text(await deps.knowledge.get(actor, input.nodeId)));
71
- server.registerTool("knowledge_versions_list", {
72
- title: "List knowledge versions",
73
- description: "List immutable versions of one authorized Knowledge node.",
74
- inputSchema: GetKnowledgeNodeInput,
138
+ }, async (input) => text(await deps.nodes.get(actor, input.nodeId)));
139
+ registerWithAlias(server, { name: "node_versions_list", deprecated: "knowledge_versions_list" }, {
140
+ title: "List node versions",
141
+ description: "List immutable versions of one authorized node.",
142
+ inputSchema: GetNodeInput,
75
143
  annotations: {
76
- title: "List knowledge versions",
144
+ title: "List node versions",
77
145
  readOnlyHint: true,
78
146
  destructiveHint: false,
79
147
  idempotentHint: true,
80
148
  openWorldHint: false,
81
149
  },
82
- }, async (input) => text(await deps.knowledge.listVersions(actor, input.nodeId)));
83
- server.registerTool("knowledge_attachment_get", {
84
- title: "Get knowledge attachment",
150
+ }, async (input) => text(await deps.nodes.listVersions(actor, input.nodeId)));
151
+ // The content behind one row of the version list (#147): what a version-pinned search citation
152
+ // points at. No `knowledge_*` alias — the tool is new under the post-#125 naming and never
153
+ // carried the old prefix.
154
+ server.registerTool("node_version_get", {
155
+ title: "Get node version content",
156
+ description: "Read one pinned immutable version of an authorized node, content included — the version a search citation names, whether or not it is current. For an attachment the answer is its metadata without the bytes. A version that does not belong to the node is not found.",
157
+ inputSchema: GetNodeVersionInput,
158
+ annotations: {
159
+ title: "Get node version content",
160
+ readOnlyHint: true,
161
+ destructiveHint: false,
162
+ idempotentHint: true,
163
+ openWorldHint: false,
164
+ },
165
+ }, async (input) => text(await deps.nodes.getVersion(actor, input.nodeId, input.versionId)));
166
+ registerWithAlias(server, { name: "node_attachment_get", deprecated: "knowledge_attachment_get" }, {
167
+ title: "Get node attachment",
85
168
  description: "Get authorized immutable attachment metadata and its explicit MCP resource URI.",
86
- inputSchema: GetKnowledgeNodeInput,
169
+ inputSchema: GetNodeInput,
87
170
  annotations: {
88
- title: "Get knowledge attachment",
171
+ title: "Get node attachment",
89
172
  readOnlyHint: true,
90
173
  destructiveHint: false,
91
174
  idempotentHint: true,
92
175
  openWorldHint: false,
93
176
  },
94
- }, async (input) => text(await deps.knowledge.getAttachment(actor, input.nodeId)));
95
- server.registerResource("knowledge-attachment", new ResourceTemplate("intel://knowledge/{nodeId}/attachment", { list: undefined }), { title: "Intel knowledge attachment" }, async (uri, variables) => {
96
- const { attachment, body } = await deps.knowledge.readAttachment(actor, String(variables.nodeId));
177
+ }, async (input) => text(await deps.nodes.getAttachment(actor, input.nodeId)));
178
+ server.registerResource("node-attachment", new ResourceTemplate("intel://nodes/{nodeId}/attachment", { list: undefined }), { title: "Intel node attachment" }, async (uri, variables) => {
179
+ const { attachment, body } = await deps.nodes.readAttachment(actor, String(variables.nodeId));
97
180
  if (attachment.version.size > AttachmentInlineLimit) {
98
181
  await body.cancel();
99
182
  throw new IntelError(413, "attachment_too_large", "Attachment is too large to inline; read it over HTTP instead");
@@ -108,155 +191,279 @@ export async function handleMcp(request, deps) {
108
191
  ],
109
192
  };
110
193
  });
111
- server.registerResource("knowledge-document", new ResourceTemplate("intel://knowledge/{nodeId}", { list: undefined }), { title: "Intel knowledge document", mimeType: "application/json" }, async (uri, variables) => {
112
- const document = await deps.knowledge.get(actor, String(variables.nodeId));
194
+ server.registerResource("node-document", new ResourceTemplate("intel://nodes/{nodeId}", { list: undefined }), { title: "Intel node document", mimeType: "application/json" }, async (uri, variables) => {
195
+ const document = await deps.nodes.get(actor, String(variables.nodeId));
113
196
  return {
114
197
  contents: [
115
198
  { uri: uri.href, mimeType: "application/json", text: JSON.stringify(document) },
116
199
  ],
117
200
  };
118
201
  });
119
- server.registerTool("knowledge_search", {
120
- title: "Search knowledge",
121
- description: "Search only authorized indexed Knowledge and return version-pinned citations.",
122
- inputSchema: SearchKnowledgeInput,
202
+ registerWithAlias(server, { name: "search", deprecated: "knowledge_search" }, {
203
+ title: "Search nodes",
204
+ description: "Search only authorized indexed nodes and return version-pinned citations. Pass scopeId " +
205
+ "with a folder node id to search that folder and everything beneath it instead of " +
206
+ "everything readable.",
207
+ inputSchema: SearchInput,
208
+ annotations: {
209
+ title: "Search nodes",
210
+ readOnlyHint: true,
211
+ destructiveHint: false,
212
+ idempotentHint: true,
213
+ openWorldHint: false,
214
+ },
215
+ }, 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,
123
235
  annotations: {
124
- title: "Search knowledge",
236
+ title: "Get agent",
125
237
  readOnlyHint: true,
126
238
  destructiveHint: false,
127
239
  idempotentHint: true,
128
240
  openWorldHint: false,
129
241
  },
130
- }, async (input) => text(await deps.knowledge.search(actor, input)));
131
- server.registerTool("knowledge_table_get", {
132
- title: "Get knowledge table",
242
+ }, async (input) => text(await deps.nodes.getAgent(actor, input)));
243
+ registerWithAlias(server, { name: "node_table_get", deprecated: "knowledge_table_get" }, {
244
+ title: "Get node table",
133
245
  description: "Read one authorized table as column names and rows.",
134
- inputSchema: GetKnowledgeTableInput,
246
+ inputSchema: GetTableInput,
135
247
  annotations: {
136
- title: "Get knowledge table",
248
+ title: "Get node table",
137
249
  readOnlyHint: true,
138
250
  destructiveHint: false,
139
251
  idempotentHint: true,
140
252
  openWorldHint: false,
141
253
  },
142
- }, async (input) => text(await deps.knowledge.getTable(actor, input.nodeId)));
143
- server.registerTool("knowledge_links_list", {
144
- title: "List knowledge links",
145
- description: "List authorized outgoing links and backlinks for one Knowledge node.",
146
- inputSchema: GetKnowledgeNodeInput,
254
+ }, async (input) => text(await deps.nodes.getTable(actor, input.nodeId)));
255
+ registerWithAlias(server, { name: "node_links_list", deprecated: "knowledge_links_list" }, {
256
+ title: "List node links",
257
+ description: "List authorized outgoing links and backlinks for one node.",
258
+ inputSchema: GetNodeInput,
147
259
  annotations: {
148
- title: "List knowledge links",
260
+ title: "List node links",
149
261
  readOnlyHint: true,
150
262
  destructiveHint: false,
151
263
  idempotentHint: true,
152
264
  openWorldHint: false,
153
265
  },
154
- }, async (input) => text(await deps.knowledge.listLinks(actor, input.nodeId)));
266
+ }, async (input) => text(await deps.nodes.listLinks(actor, input.nodeId)));
155
267
  // The reader's half of a document link (#41): the titles of the linked documents this caller
156
268
  // may see. There is no tool to create or delete a link — a relationship is written in the text
157
- // and `knowledge_save` is what records it, so there is only one way to make one.
269
+ // and `node_save` is what records it, so there is only one way to make one.
158
270
  //
159
271
  // ⚠️ A target this caller may not reach, or one that is gone, is simply absent from the answer.
160
272
  // The two are indistinguishable on purpose: telling them apart would confirm that a document
161
273
  // exists somewhere they cannot look.
162
- server.registerTool("knowledge_links_resolve", {
163
- title: "Resolve knowledge links",
274
+ registerWithAlias(server, { name: "node_links_resolve", deprecated: "knowledge_links_resolve" }, {
275
+ title: "Resolve node links",
164
276
  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.",
165
- inputSchema: ResolveKnowledgeLinksInput,
277
+ inputSchema: ResolveNodeLinksInput,
278
+ annotations: {
279
+ title: "Resolve node links",
280
+ readOnlyHint: true,
281
+ destructiveHint: false,
282
+ idempotentHint: true,
283
+ openWorldHint: false,
284
+ },
285
+ }, async (input) => text(await deps.nodes.resolveLinks(actor, input)));
286
+ registerWithAlias(server, { name: "node_graph", deprecated: "knowledge_graph" }, {
287
+ title: "Get node graph",
288
+ description: "Get the authorized nodes and explicit relationships for discovery.",
289
+ inputSchema: NodeGraphInput,
166
290
  annotations: {
167
- title: "Resolve knowledge links",
291
+ title: "Get node graph",
168
292
  readOnlyHint: true,
169
293
  destructiveHint: false,
170
294
  idempotentHint: true,
171
295
  openWorldHint: false,
172
296
  },
173
- }, async (input) => text(await deps.knowledge.resolveLinks(actor, input)));
174
- server.registerTool("knowledge_graph", {
175
- title: "Get knowledge graph",
176
- description: "Get the authorized Knowledge nodes and explicit relationships for discovery.",
177
- inputSchema: KnowledgeGraphInput,
297
+ }, async (input) => text(await deps.nodes.graph(actor, input)));
298
+ // The export as MCP offers it (#136): the manifest — every readable entry with its bundle path
299
+ // and media type. The contents behind the paths are read one by one through `node_get`,
300
+ // `node_table_get`, and `node_attachment_get`. ⚠️ Deliberately no binary zip over MCP: a
301
+ // resource payload travels inline as base64 and would have to fit in Worker memory twice, the
302
+ // same limit that caps `node_attachment_get` at 10 MB — a whole subtree is the case that limit
303
+ // exists for. HTTP `GET /nodes/:nodeId/export` streams the real archive.
304
+ server.registerTool("node_export_manifest", {
305
+ title: "Get export manifest",
306
+ 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.",
307
+ inputSchema: ExportManifestInput,
178
308
  annotations: {
179
- title: "Get knowledge graph",
309
+ title: "Get export manifest",
180
310
  readOnlyHint: true,
181
311
  destructiveHint: false,
182
312
  idempotentHint: true,
183
313
  openWorldHint: false,
184
314
  },
185
- }, async (input) => text(await deps.knowledge.graph(actor, input)));
315
+ }, async (input) => text(await deps.bundle.manifest(bundleActor, input.nodeId)));
186
316
  }
187
317
  if (deps.authorization.can("intel", "admin")) {
188
- server.registerTool("knowledge_reindex", {
189
- title: "Reindex knowledge",
190
- description: "Queue every current canonical Knowledge version for derived index rebuild.",
318
+ registerWithAlias(server, { name: "node_reindex", deprecated: "knowledge_reindex" }, {
319
+ title: "Reindex nodes",
320
+ description: "Queue every current canonical node version for derived index rebuild.",
191
321
  inputSchema: EmptyInput,
192
322
  annotations: {
193
- title: "Reindex knowledge",
323
+ title: "Reindex nodes",
194
324
  readOnlyHint: false,
195
325
  destructiveHint: false,
196
326
  idempotentHint: true,
197
327
  openWorldHint: false,
198
328
  },
199
- }, async () => text(await deps.knowledge.reindex(actor)));
329
+ }, async () => text(await deps.nodes.reindex(actor)));
200
330
  }
201
331
  if (permits(deps.authorization, "knowledge", "create")) {
202
- server.registerTool("knowledge_create", {
203
- title: "Create knowledge",
332
+ registerWithAlias(server, { name: "node_create", deprecated: "knowledge_create" }, {
333
+ title: "Create node",
204
334
  description: "Create a governed folder, document, attachment, or table node.",
205
- inputSchema: CreateKnowledgeNodeInput,
335
+ inputSchema: CreateNodeInput,
206
336
  annotations: {
207
- title: "Create knowledge",
337
+ title: "Create node",
208
338
  readOnlyHint: false,
209
339
  destructiveHint: false,
210
340
  idempotentHint: true,
211
341
  openWorldHint: false,
212
342
  },
213
- }, async (input) => text(await deps.knowledge.create(actor, input)));
343
+ }, async (input) => text(await deps.nodes.create(actor, input)));
344
+ /**
345
+ * ⚠️ There is no longer ANY tool result in Intel that contains a secret, and that is the whole
346
+ * of D29 seen from here. Until #207 this one tool answered with the Application key in plain
347
+ * text — a deliberate exception, because the result WAS the credential and a redacted answer
348
+ * would have meant "an agent you can create but never run". The exception is gone rather than
349
+ * weakened: the key now travels from Gate into the agent runtime over Intel's service binding
350
+ * inside this call, so there is nothing left to hand to a model. `agent_create`, `agent_get`,
351
+ * `agent_list` and every node read answer with `applicationId` alone, which names the principal
352
+ * without authenticating it.
353
+ */
354
+ server.registerTool("agent_create", {
355
+ title: "Create agent",
356
+ 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.",
357
+ inputSchema: CreateAgentInput,
358
+ annotations: {
359
+ title: "Create agent",
360
+ readOnlyHint: false,
361
+ destructiveHint: false,
362
+ idempotentHint: true,
363
+ // Creating an agent creates a principal in Gate, which is a system outside Intel.
364
+ openWorldHint: true,
365
+ },
366
+ }, async (input) => text(await deps.nodes.createAgent(actor, input, { token: deps.bearer })));
214
367
  }
215
368
  if (permits(deps.authorization, "knowledge", "write")) {
216
- server.registerTool("knowledge_save", {
217
- title: "Save knowledge version",
369
+ server.registerTool("agent_update", {
370
+ title: "Update agent definition",
371
+ description: "Append an immutable definition version to an agent using an optimistic base version.",
372
+ inputSchema: SaveAgentDefinitionInput,
373
+ annotations: {
374
+ title: "Update agent definition",
375
+ readOnlyHint: false,
376
+ destructiveHint: false,
377
+ idempotentHint: true,
378
+ openWorldHint: false,
379
+ },
380
+ }, async (input) => text(await deps.nodes.saveAgentDefinition(actor, input, { token: deps.bearer })));
381
+ /**
382
+ * The MCP half of the "Replace key" action in the agent profile (D29, #207).
383
+ *
384
+ * ⚠️ `knowledge/write` like `agent_update`, not `agents/run`: which principal an agent acts as
385
+ * is a property of the agent, changed by whoever may change the agent. And the result names the
386
+ * principal and the moment — never the key, which went to the runtime and nowhere else.
387
+ */
388
+ server.registerTool("agent_rotate_key", {
389
+ title: "Replace an agent's application key",
390
+ 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.",
391
+ inputSchema: RotateAgentKeyInput,
392
+ annotations: {
393
+ title: "Replace an agent's application key",
394
+ readOnlyHint: false,
395
+ // The old key stops working the moment this runs, and anything still using it breaks.
396
+ destructiveHint: true,
397
+ // Asking twice issues twice, and the second answer invalidates the first key.
398
+ idempotentHint: false,
399
+ // The principal lives in Gate, which is a system outside Intel.
400
+ openWorldHint: true,
401
+ },
402
+ }, async (input) => text(await deps.nodes.rotateAgentKey(actor, input, { token: deps.bearer })));
403
+ registerWithAlias(server, { name: "node_save", deprecated: "knowledge_save" }, {
404
+ title: "Save node version",
218
405
  description: "Append an immutable content version using an optimistic base version.",
219
- inputSchema: SaveKnowledgeVersionInput,
406
+ inputSchema: SaveNodeVersionInput,
220
407
  annotations: {
221
- title: "Save knowledge version",
408
+ title: "Save node version",
222
409
  readOnlyHint: false,
223
410
  destructiveHint: false,
224
411
  idempotentHint: true,
225
412
  openWorldHint: false,
226
413
  },
227
- }, async (input) => text(await deps.knowledge.save(actor, input)));
228
- server.registerTool("knowledge_attachment_save", {
229
- title: "Save knowledge attachment",
414
+ }, async (input) => text(await deps.nodes.save(actor, input)));
415
+ registerWithAlias(server, { name: "node_attachment_save", deprecated: "knowledge_attachment_save" }, {
416
+ title: "Save node attachment",
230
417
  description: "Append immutable base64 file bytes to an attachment node.",
231
- inputSchema: SaveKnowledgeAttachmentInput,
418
+ inputSchema: SaveAttachmentInput,
419
+ annotations: {
420
+ title: "Save node attachment",
421
+ readOnlyHint: false,
422
+ destructiveHint: false,
423
+ idempotentHint: true,
424
+ openWorldHint: false,
425
+ },
426
+ }, async (input) => text(await deps.nodes.saveAttachment(actor, input)));
427
+ // The import half of the bundle round trip (#137). At minimum the manifest way: a zip made by
428
+ // the export — or a naked zipped folder — lands as a new subtree under the target. Same
429
+ // service as HTTP, so authorization, limits, remapping, and the all-or-nothing write cannot
430
+ // differ by surface; only the transport differs (inline base64 here, a streamed body there).
431
+ server.registerTool("node_import_bundle", {
432
+ title: "Import bundle",
433
+ 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.",
434
+ inputSchema: ImportBundleInput,
232
435
  annotations: {
233
- title: "Save knowledge attachment",
436
+ title: "Import bundle",
234
437
  readOnlyHint: false,
235
438
  destructiveHint: false,
236
439
  idempotentHint: true,
237
440
  openWorldHint: false,
238
441
  },
239
- }, async (input) => text(await deps.knowledge.saveAttachment(actor, input)));
240
- server.registerTool("knowledge_table_define", {
241
- title: "Define knowledge table columns",
442
+ }, async (input) => text(await deps.bundle.importBundle(bundleActor, {
443
+ targetNodeId: input.nodeId,
444
+ zip: decodeZipBase64(input.zipBase64),
445
+ idempotencyKey: input.idempotencyKey,
446
+ })));
447
+ registerWithAlias(server, { name: "node_table_define", deprecated: "knowledge_table_define" }, {
448
+ title: "Define node table columns",
242
449
  description: "Write the column names of an empty table. The header is the contract every append is checked against and cannot be rewritten.",
243
- inputSchema: DefineKnowledgeTableInput,
450
+ inputSchema: DefineTableInput,
244
451
  annotations: {
245
- title: "Define knowledge table columns",
452
+ title: "Define node table columns",
246
453
  readOnlyHint: false,
247
454
  destructiveHint: false,
248
455
  idempotentHint: true,
249
456
  openWorldHint: false,
250
457
  },
251
- }, async (input) => text(await deps.knowledge.defineTable(actor, input)));
458
+ }, async (input) => text(await deps.nodes.defineTable(actor, input)));
252
459
  // The tool #40 exists for: an agent collecting findings on a schedule appends them without
253
460
  // reading or resending what is already there, and two agents appending at once lose nothing.
254
- server.registerTool("knowledge_table_append", {
255
- title: "Append knowledge table rows",
461
+ registerWithAlias(server, { name: "node_table_append", deprecated: "knowledge_table_append" }, {
462
+ title: "Append node table rows",
256
463
  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.",
257
- inputSchema: AppendKnowledgeTableRowsInput,
464
+ inputSchema: AppendTableRowsInput,
258
465
  annotations: {
259
- title: "Append knowledge table rows",
466
+ title: "Append node table rows",
260
467
  readOnlyHint: false,
261
468
  destructiveHint: false,
262
469
  // The idempotency key makes a repeat of the same call a no-op; without one, appending
@@ -264,74 +471,120 @@ export async function handleMcp(request, deps) {
264
471
  idempotentHint: true,
265
472
  openWorldHint: false,
266
473
  },
267
- }, async (input) => text(await deps.knowledge.appendTableRows(actor, input)));
268
- server.registerTool("knowledge_update", {
269
- title: "Update knowledge",
270
- description: "Rename, move, describe, or change the context policy of Knowledge.",
271
- inputSchema: UpdateKnowledgeNodeInput,
474
+ }, async (input) => text(await deps.nodes.appendTableRows(actor, input)));
475
+ // The other half of #135: an agent that wrote a wrong finding can correct it. Positions address
476
+ // rows — they carry no IDs on purpose — so every mutation names the version the positions were
477
+ // read from and is refused with `version_conflict` when the table moved on. No `knowledge_*`
478
+ // alias: these tools are new under the post-#125 naming and never carried the old prefix.
479
+ server.registerTool("node_table_update_rows", {
480
+ title: "Update node table rows",
481
+ 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.",
482
+ inputSchema: UpdateTableRowsInput,
272
483
  annotations: {
273
- title: "Update knowledge",
484
+ title: "Update node table rows",
274
485
  readOnlyHint: false,
275
- destructiveHint: false,
486
+ // Replacing a cell discards what stood there; the history keeps it, the table does not.
487
+ destructiveHint: true,
488
+ idempotentHint: true,
489
+ openWorldHint: false,
490
+ },
491
+ }, async (input) => text(await deps.nodes.updateTableRows(actor, input)));
492
+ server.registerTool("node_table_delete_rows", {
493
+ title: "Delete node table rows",
494
+ 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.",
495
+ inputSchema: DeleteTableRowsInput,
496
+ annotations: {
497
+ title: "Delete node table rows",
498
+ readOnlyHint: false,
499
+ destructiveHint: true,
276
500
  idempotentHint: true,
277
501
  openWorldHint: false,
278
502
  },
279
- }, async (input) => text(await deps.knowledge.update(actor, input)));
280
- server.registerTool("knowledge_archive", {
281
- title: "Archive knowledge",
282
- description: "Archive or restore one Knowledge node using optimistic concurrency.",
283
- inputSchema: ArchiveKnowledgeNodeInput,
503
+ }, async (input) => text(await deps.nodes.deleteTableRows(actor, input)));
504
+ server.registerTool("node_table_redefine", {
505
+ title: "Redefine node table columns",
506
+ 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.",
507
+ inputSchema: RedefineTableInput,
284
508
  annotations: {
285
- title: "Archive knowledge",
509
+ title: "Redefine node table columns",
286
510
  readOnlyHint: false,
287
511
  destructiveHint: true,
288
512
  idempotentHint: true,
289
513
  openWorldHint: false,
290
514
  },
291
- }, async (input) => text(await deps.knowledge.archive(actor, input)));
515
+ }, async (input) => text(await deps.nodes.redefineTable(actor, input)));
516
+ registerWithAlias(server, { name: "node_update", deprecated: "knowledge_update" }, {
517
+ title: "Update node",
518
+ description: "Rename, move, or describe a node.",
519
+ inputSchema: UpdateNodeInput,
520
+ annotations: {
521
+ title: "Update node",
522
+ readOnlyHint: false,
523
+ destructiveHint: false,
524
+ idempotentHint: true,
525
+ openWorldHint: false,
526
+ },
527
+ }, async (input) => text(await deps.nodes.update(actor, input)));
528
+ registerWithAlias(server, { name: "node_archive", deprecated: "knowledge_archive" }, {
529
+ title: "Archive node",
530
+ description: "Archive or restore one node using optimistic concurrency. Archiving an agent also switches off its Gate application; restoring switches it back on.",
531
+ inputSchema: ArchiveNodeInput,
532
+ annotations: {
533
+ title: "Archive node",
534
+ readOnlyHint: false,
535
+ destructiveHint: true,
536
+ idempotentHint: true,
537
+ // The one node mutation that can reach a system outside Intel: archiving an agent
538
+ // switches its Gate Application (#182). Nothing else here leaves the installation.
539
+ openWorldHint: true,
540
+ },
541
+ },
542
+ // The caller's own bearer, exactly as the runtime proxy uses it and for the same reason:
543
+ // switching a machine principal is an act in Gate, performed by the person asking.
544
+ async (input) => text(await deps.nodes.archive(actor, input, { token: deps.bearer })));
292
545
  }
293
546
  if (permits(deps.authorization, "knowledge", "share")) {
294
- server.registerTool("knowledge_shares_list", {
295
- title: "List knowledge shares",
296
- description: "List direct grants for Knowledge the caller is allowed to manage.",
297
- inputSchema: ListKnowledgeGrantsInput,
547
+ registerWithAlias(server, { name: "node_shares_list", deprecated: "knowledge_shares_list" }, {
548
+ title: "List node shares",
549
+ description: "List direct grants for nodes the caller is allowed to manage.",
550
+ inputSchema: ListGrantsInput,
298
551
  annotations: {
299
- title: "List knowledge shares",
552
+ title: "List node shares",
300
553
  readOnlyHint: true,
301
554
  destructiveHint: false,
302
555
  idempotentHint: true,
303
556
  openWorldHint: false,
304
557
  },
305
- }, async (input) => text(await deps.knowledge.listGrants(actor, input.resourceId)));
306
- server.registerTool("knowledge_share", {
307
- title: "Share knowledge",
308
- description: "Grant inherited Knowledge access to a Gate user, verified email, or the organization.",
309
- inputSchema: ShareKnowledgeInput,
558
+ }, async (input) => text(await deps.nodes.listGrants(actor, input.resourceId)));
559
+ registerWithAlias(server, { name: "node_share", deprecated: "knowledge_share" }, {
560
+ title: "Share node",
561
+ description: "Grant inherited node access to a Gate user, verified email, or the organization.",
562
+ inputSchema: ShareInput,
310
563
  annotations: {
311
- title: "Share knowledge",
564
+ title: "Share node",
312
565
  readOnlyHint: false,
313
566
  destructiveHint: false,
314
567
  idempotentHint: true,
315
568
  openWorldHint: false,
316
569
  },
317
- }, async (input) => text(await deps.knowledge.share(actor, input)));
318
- server.registerTool("knowledge_revoke_share", {
319
- title: "Revoke knowledge share",
320
- description: "Revoke one direct Knowledge grant by ID.",
321
- inputSchema: RevokeKnowledgeGrantInput,
570
+ }, async (input) => text(await deps.nodes.share(actor, input)));
571
+ registerWithAlias(server, { name: "node_revoke_share", deprecated: "knowledge_revoke_share" }, {
572
+ title: "Revoke node share",
573
+ description: "Revoke one direct node grant by ID.",
574
+ inputSchema: RevokeGrantInput,
322
575
  annotations: {
323
- title: "Revoke knowledge share",
576
+ title: "Revoke node share",
324
577
  readOnlyHint: false,
325
578
  destructiveHint: true,
326
579
  idempotentHint: true,
327
580
  openWorldHint: false,
328
581
  },
329
- }, async (input) => text(await deps.knowledge.revokeGrant(actor, input)));
582
+ }, async (input) => text(await deps.nodes.revokeGrant(actor, input)));
330
583
  }
331
584
  if (permits(deps.authorization, "flows", "read")) {
332
585
  server.registerTool("flows_list", {
333
586
  title: "List flows",
334
- description: "List authorized flows, either all of them or the ones filed in one folder of the shared Knowledge tree.",
587
+ description: "List authorized flows, either all of them or the ones filed in one folder of the shared tree.",
335
588
  inputSchema: ListFlowsInput,
336
589
  annotations: {
337
590
  title: "List flows",
@@ -353,6 +606,33 @@ export async function handleMcp(request, deps) {
353
606
  openWorldHint: false,
354
607
  },
355
608
  }, async (input) => text(await deps.flows.get(flowActor, input.flowId)));
609
+ // Named like `knowledge_versions_list`, because it is the same question about the other kind of
610
+ // thing in the tree. This pair is what turns the `versionId` that `flow_publish` and
611
+ // `flow_publish_preview` demand into something an MCP client can actually obtain (#144).
612
+ server.registerTool("flow_versions_list", {
613
+ title: "List flow versions",
614
+ 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.",
615
+ inputSchema: GetFlowInput,
616
+ annotations: {
617
+ title: "List flow versions",
618
+ readOnlyHint: true,
619
+ destructiveHint: false,
620
+ idempotentHint: true,
621
+ openWorldHint: false,
622
+ },
623
+ }, async (input) => text(await deps.flows.listVersions(flowActor, input.flowId)));
624
+ server.registerTool("flow_version_get", {
625
+ title: "Get flow version",
626
+ description: "Get one immutable version of an authorized flow, including its graph.",
627
+ inputSchema: GetFlowVersionInput,
628
+ annotations: {
629
+ title: "Get flow version",
630
+ readOnlyHint: true,
631
+ destructiveHint: false,
632
+ idempotentHint: true,
633
+ openWorldHint: false,
634
+ },
635
+ }, async (input) => text(await deps.flows.getVersion(flowActor, input)));
356
636
  server.registerTool("flow_calls_list", {
357
637
  title: "List called flows",
358
638
  description: "List the flows one flow calls, read out of its graph rather than out of where it is filed.",
@@ -394,7 +674,7 @@ export async function handleMcp(request, deps) {
394
674
  }, async (input) => text(await deps.flows.validate(flowActor, input.flowId)));
395
675
  server.registerTool("flow_requirements_list", {
396
676
  title: "List what a flow needs",
397
- description: "List the Knowledge 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.",
677
+ 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.",
398
678
  inputSchema: GetFlowInput,
399
679
  annotations: {
400
680
  title: "List what a flow needs",
@@ -417,7 +697,7 @@ export async function handleMcp(request, deps) {
417
697
  },
418
698
  }, async (input) => text(await deps.flows.getRun(flowActor, input.runId)));
419
699
  // ⚠️ Model context like every other tool result: what a run did, never what it produced. The run
420
- // input and output are absent because a run reaches Knowledge and tools with the rights of
700
+ // input and output are absent because a run reaches nodes and tools with the rights of
421
701
  // whoever started it, and only their own runs are the calling user's to read out that way.
422
702
  server.registerTool("flow_runs_list", {
423
703
  title: "List flow runs",
@@ -445,7 +725,7 @@ export async function handleMcp(request, deps) {
445
725
  }, async (input) => text(await deps.flows.listRunSteps(flowActor, input.runId)));
446
726
  }
447
727
  // A flow has no share tool of its own. Sharing happens on the folder a flow is filed in, through
448
- // knowledge_share, so a narrower grant cannot sit beside the folder grant (ADR-0004 §2).
728
+ // node_share, so a narrower grant cannot sit beside the folder grant (ADR-0004 §2).
449
729
  if (permits(deps.authorization, "flows", "create")) {
450
730
  server.registerTool("flow_create", {
451
731
  title: "Create flow",
@@ -463,7 +743,7 @@ export async function handleMcp(request, deps) {
463
743
  if (permits(deps.authorization, "flows", "write")) {
464
744
  server.registerTool("flow_update", {
465
745
  title: "Rename or move flow",
466
- description: "Rename a flow, change its description, or move it to another folder of the shared Knowledge tree. Never changes what the flow does.",
746
+ description: "Rename a flow, change its description, or move it to another folder of the shared tree. Never changes what the flow does.",
467
747
  inputSchema: UpdateFlowInput,
468
748
  annotations: {
469
749
  title: "Rename or move flow",
@@ -518,7 +798,7 @@ export async function handleMcp(request, deps) {
518
798
  }, async (input) => text(await deps.flows.previewPublish(flowActor, input)));
519
799
  server.registerTool("flow_publish", {
520
800
  title: "Publish flow",
521
- description: "Publish a version after resolving its authorized Knowledge and tool references.",
801
+ description: "Publish a version after resolving its authorized node and tool references.",
522
802
  inputSchema: PublishFlowInput,
523
803
  annotations: {
524
804
  title: "Publish flow",
@@ -528,6 +808,20 @@ export async function handleMcp(request, deps) {
528
808
  openWorldHint: false,
529
809
  },
530
810
  }, async (input) => text(await deps.flows.publish(flowActor, input)));
811
+ server.registerTool("flow_unpublish", {
812
+ title: "Unpublish flow",
813
+ description: "Withdraw a flow's published version. Versions are kept and can be republished; new runs and calls from other flows stop resolving, runs in flight finish on the version they took.",
814
+ inputSchema: UnpublishFlowInput,
815
+ annotations: {
816
+ title: "Unpublish flow",
817
+ readOnlyHint: false,
818
+ // Nothing is deleted and republishing is one publish away — destructive all the same,
819
+ // because a flow another flow calls stops resolving, like flow_archive.
820
+ destructiveHint: true,
821
+ idempotentHint: true,
822
+ openWorldHint: false,
823
+ },
824
+ }, async (input) => text(await deps.flows.unpublish(flowActor, input)));
531
825
  }
532
826
  if (permits(deps.authorization, "flows", "run")) {
533
827
  server.registerTool("flow_run_start", {
@@ -554,6 +848,92 @@ export async function handleMcp(request, deps) {
554
848
  openWorldHint: true,
555
849
  },
556
850
  }, async (input) => text(await deps.flows.completeStep(flowActor, input)));
851
+ server.registerTool("flow_run_cancel", {
852
+ title: "Cancel flow run",
853
+ description: "Cancel a running flow run without recording a step failure. Running child runs it called are cancelled with it; a run that is already terminal is refused unchanged.",
854
+ inputSchema: CancelFlowRunInput,
855
+ annotations: {
856
+ title: "Cancel flow run",
857
+ readOnlyHint: false,
858
+ // Nothing is deleted, but work in flight is ended for the run and its children — a
859
+ // client deciding whether to ask first has to be told that.
860
+ destructiveHint: true,
861
+ idempotentHint: true,
862
+ openWorldHint: false,
863
+ },
864
+ }, async (input) => text(await deps.flows.cancelRun(flowActor, input)));
865
+ }
866
+ /**
867
+ * Driving an agent, over the same door the screen uses.
868
+ *
869
+ * ⚠️ The three below are the MCP half of the two head actions on the agent page — the repository's
870
+ * "one business layer, three thin surfaces" rule, kept in the same delivery. They call the runtime
871
+ * through `deps.agents`, which resolves the resource ACL and forwards the CALLER's bearer; a tool
872
+ * that reached the runtime with anything else would be Intel acting as somebody it is not.
873
+ */
874
+ if (permits(deps.authorization, "agents", "run")) {
875
+ server.registerTool("agent_pause", {
876
+ title: "Pause agent",
877
+ 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.",
878
+ inputSchema: PauseAgentInput,
879
+ annotations: {
880
+ title: "Pause agent",
881
+ readOnlyHint: false,
882
+ // Nothing is deleted, and yet work that would have happened does not — a client deciding
883
+ // whether to ask first has to be told that.
884
+ destructiveHint: true,
885
+ idempotentHint: true,
886
+ openWorldHint: false,
887
+ },
888
+ }, async (input) => text(await deps.agents.request({
889
+ actor: agentActor,
890
+ token: deps.bearer,
891
+ agentId: input.nodeId,
892
+ method: "POST",
893
+ path: "/pause",
894
+ body: "{}",
895
+ })));
896
+ server.registerTool("agent_resume", {
897
+ title: "Resume agent",
898
+ 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.",
899
+ inputSchema: PauseAgentInput,
900
+ annotations: {
901
+ title: "Resume agent",
902
+ readOnlyHint: false,
903
+ destructiveHint: false,
904
+ idempotentHint: true,
905
+ openWorldHint: false,
906
+ },
907
+ }, async (input) => text(await deps.agents.request({
908
+ actor: agentActor,
909
+ token: deps.bearer,
910
+ agentId: input.nodeId,
911
+ method: "POST",
912
+ path: "/resume",
913
+ body: "{}",
914
+ })));
915
+ server.registerTool("agent_run_now", {
916
+ title: "Run agent now",
917
+ 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.",
918
+ inputSchema: RunAgentNowInput,
919
+ annotations: {
920
+ title: "Run agent now",
921
+ readOnlyHint: false,
922
+ destructiveHint: false,
923
+ // Every run is a new run, so asking twice runs it twice.
924
+ idempotentHint: false,
925
+ // The agent may reach whatever its own definition grants it, so what a run touches is not
926
+ // knowable from this schema.
927
+ openWorldHint: true,
928
+ },
929
+ }, async (input) => text(await deps.agents.request({
930
+ actor: agentActor,
931
+ token: deps.bearer,
932
+ agentId: input.nodeId,
933
+ method: "POST",
934
+ path: "/run",
935
+ body: JSON.stringify({ target: input.target }),
936
+ })));
557
937
  }
558
938
  if (permits(deps.authorization, "tools", "read")) {
559
939
  server.registerTool("tools_catalog_list", {
@@ -568,6 +948,21 @@ export async function handleMcp(request, deps) {
568
948
  openWorldHint: false,
569
949
  },
570
950
  }, async () => text(await deps.tools.catalog(toolActor)));
951
+ // The MCP twin of the picker in the agent profile (D30): whole servers, so an agent can be
952
+ // given tools from a client instead of from the screen. Assigning and removing is
953
+ // `agent_update` — a delegation is a field of the definition, not a surface of its own.
954
+ server.registerTool("tools_servers_list", {
955
+ title: "List tool servers",
956
+ 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.",
957
+ inputSchema: EmptyInput,
958
+ annotations: {
959
+ title: "List tool servers",
960
+ readOnlyHint: true,
961
+ destructiveHint: false,
962
+ idempotentHint: true,
963
+ openWorldHint: false,
964
+ },
965
+ }, async () => text(await deps.tools.servers(toolActor)));
571
966
  }
572
967
  if (permits(deps.authorization, "tools", "test")) {
573
968
  server.registerTool("tools_test", {