@anchrd/intel-api 0.6.7 → 0.7.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 (51) hide show
  1. package/README.md +44 -3
  2. package/dist/adapters/cloudflare/cloudflare.js +50 -16
  3. package/dist/adapters/cloudflare/cloudflare.types.d.ts +11 -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 +431 -118
  12. package/dist/adapters/gate-applications/gate-applications.d.ts +23 -0
  13. package/dist/adapters/gate-applications/gate-applications.js +66 -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/agent-runtime/agent-runtime.d.ts +16 -0
  18. package/dist/agent-runtime/agent-runtime.js +76 -0
  19. package/dist/agent-runtime/agent-runtime.types.d.ts +57 -0
  20. package/dist/bundle/bundle.d.ts +4 -0
  21. package/dist/bundle/bundle.js +1035 -0
  22. package/dist/bundle/bundle.types.d.ts +33 -0
  23. package/dist/bundle/bundle.types.js +1 -0
  24. package/dist/cli/cli.js +10 -1
  25. package/dist/flows/flows.d.ts +8 -8
  26. package/dist/flows/flows.js +158 -42
  27. package/dist/flows/flows.types.d.ts +40 -7
  28. package/dist/http/http.d.ts +1 -0
  29. package/dist/http/http.js +324 -61
  30. package/dist/http/http.types.d.ts +6 -2
  31. package/dist/indexing/indexing.js +14 -2
  32. package/dist/indexing/indexing.types.d.ts +2 -2
  33. package/dist/intel/intel.js +12 -3
  34. package/dist/intel/intel.types.d.ts +6 -2
  35. package/dist/mcp/mcp.js +483 -124
  36. package/dist/mcp/mcp.types.d.ts +11 -2
  37. package/dist/nodes/nodes.d.ts +2 -0
  38. package/dist/nodes/nodes.js +1337 -0
  39. package/dist/nodes/nodes.types.d.ts +314 -0
  40. package/dist/nodes/nodes.types.js +1 -0
  41. package/migrations/0011_one_name_for_the_tree.sql +53 -0
  42. package/migrations/0012_table_snapshots.sql +29 -0
  43. package/migrations/0013_agents_in_the_tree.sql +76 -0
  44. package/migrations/0014_agent_applications.sql +25 -0
  45. package/package.json +3 -2
  46. package/dist/knowledge/knowledge.d.ts +0 -2
  47. package/dist/knowledge/knowledge.js +0 -761
  48. package/dist/knowledge/knowledge.types.d.ts +0 -198
  49. /package/dist/{knowledge/knowledge.types.js → agent-runtime/agent-runtime.types.js} +0 -0
  50. /package/dist/{knowledge → nodes}/document-links/document-links.d.ts +0 -0
  51. /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, 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,258 @@ 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
+ * ⚠️ The ONE tool result in Intel that contains a secret, and it is a decision rather than an
346
+ * oversight (#182, D27). Everywhere else in this file a credential in a result would be a leak
347
+ * into model context; here the result IS the credential — Gate issues an Application key once
348
+ * and keeps only its hash, so a redacted answer would mean "an agent you can create but never
349
+ * run", and the same administrator would get less through MCP than through the browser. Gate
350
+ * made the identical exception for `application_create` (`anchrd/gate` GATE-75).
351
+ *
352
+ * The exception is this tool and nothing beside it: `agent_get`, `agent_list` and every node
353
+ * read answer with `applicationId` alone, which names the principal without authenticating it.
354
+ */
355
+ server.registerTool("agent_create", {
356
+ title: "Create agent",
357
+ 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 response carries the application key ONCE, in plain text: put it into the agent runtime's AGENT_APPLICATION_KEYS secret and store it nowhere else. A repeated idempotency key returns the existing agent with no key.",
358
+ inputSchema: CreateAgentInput,
359
+ annotations: {
360
+ title: "Create agent",
361
+ readOnlyHint: false,
362
+ destructiveHint: false,
363
+ idempotentHint: true,
364
+ // Creating an agent creates a principal in Gate, which is a system outside Intel.
365
+ openWorldHint: true,
366
+ },
367
+ }, async (input) => text(await deps.nodes.createAgent(actor, input, { token: deps.bearer })));
214
368
  }
215
369
  if (permits(deps.authorization, "knowledge", "write")) {
216
- server.registerTool("knowledge_save", {
217
- title: "Save knowledge version",
370
+ server.registerTool("agent_update", {
371
+ title: "Update agent definition",
372
+ description: "Append an immutable definition version to an agent using an optimistic base version.",
373
+ inputSchema: SaveAgentDefinitionInput,
374
+ annotations: {
375
+ title: "Update agent definition",
376
+ readOnlyHint: false,
377
+ destructiveHint: false,
378
+ idempotentHint: true,
379
+ openWorldHint: false,
380
+ },
381
+ }, async (input) => text(await deps.nodes.saveAgentDefinition(actor, input)));
382
+ registerWithAlias(server, { name: "node_save", deprecated: "knowledge_save" }, {
383
+ title: "Save node version",
218
384
  description: "Append an immutable content version using an optimistic base version.",
219
- inputSchema: SaveKnowledgeVersionInput,
385
+ inputSchema: SaveNodeVersionInput,
220
386
  annotations: {
221
- title: "Save knowledge version",
387
+ title: "Save node version",
222
388
  readOnlyHint: false,
223
389
  destructiveHint: false,
224
390
  idempotentHint: true,
225
391
  openWorldHint: false,
226
392
  },
227
- }, async (input) => text(await deps.knowledge.save(actor, input)));
228
- server.registerTool("knowledge_attachment_save", {
229
- title: "Save knowledge attachment",
393
+ }, async (input) => text(await deps.nodes.save(actor, input)));
394
+ registerWithAlias(server, { name: "node_attachment_save", deprecated: "knowledge_attachment_save" }, {
395
+ title: "Save node attachment",
230
396
  description: "Append immutable base64 file bytes to an attachment node.",
231
- inputSchema: SaveKnowledgeAttachmentInput,
397
+ inputSchema: SaveAttachmentInput,
232
398
  annotations: {
233
- title: "Save knowledge attachment",
399
+ title: "Save node attachment",
234
400
  readOnlyHint: false,
235
401
  destructiveHint: false,
236
402
  idempotentHint: true,
237
403
  openWorldHint: false,
238
404
  },
239
- }, async (input) => text(await deps.knowledge.saveAttachment(actor, input)));
240
- server.registerTool("knowledge_table_define", {
241
- title: "Define knowledge table columns",
405
+ }, async (input) => text(await deps.nodes.saveAttachment(actor, input)));
406
+ // The import half of the bundle round trip (#137). At minimum the manifest way: a zip made by
407
+ // the export or a naked zipped folder — lands as a new subtree under the target. Same
408
+ // service as HTTP, so authorization, limits, remapping, and the all-or-nothing write cannot
409
+ // differ by surface; only the transport differs (inline base64 here, a streamed body there).
410
+ server.registerTool("node_import_bundle", {
411
+ title: "Import bundle",
412
+ 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.",
413
+ inputSchema: ImportBundleInput,
414
+ annotations: {
415
+ title: "Import bundle",
416
+ readOnlyHint: false,
417
+ destructiveHint: false,
418
+ idempotentHint: true,
419
+ openWorldHint: false,
420
+ },
421
+ }, async (input) => text(await deps.bundle.importBundle(bundleActor, {
422
+ targetNodeId: input.nodeId,
423
+ zip: decodeZipBase64(input.zipBase64),
424
+ idempotencyKey: input.idempotencyKey,
425
+ })));
426
+ registerWithAlias(server, { name: "node_table_define", deprecated: "knowledge_table_define" }, {
427
+ title: "Define node table columns",
242
428
  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,
429
+ inputSchema: DefineTableInput,
244
430
  annotations: {
245
- title: "Define knowledge table columns",
431
+ title: "Define node table columns",
246
432
  readOnlyHint: false,
247
433
  destructiveHint: false,
248
434
  idempotentHint: true,
249
435
  openWorldHint: false,
250
436
  },
251
- }, async (input) => text(await deps.knowledge.defineTable(actor, input)));
437
+ }, async (input) => text(await deps.nodes.defineTable(actor, input)));
252
438
  // The tool #40 exists for: an agent collecting findings on a schedule appends them without
253
439
  // 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",
440
+ registerWithAlias(server, { name: "node_table_append", deprecated: "knowledge_table_append" }, {
441
+ title: "Append node table rows",
256
442
  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,
443
+ inputSchema: AppendTableRowsInput,
258
444
  annotations: {
259
- title: "Append knowledge table rows",
445
+ title: "Append node table rows",
260
446
  readOnlyHint: false,
261
447
  destructiveHint: false,
262
448
  // The idempotency key makes a repeat of the same call a no-op; without one, appending
@@ -264,74 +450,120 @@ export async function handleMcp(request, deps) {
264
450
  idempotentHint: true,
265
451
  openWorldHint: false,
266
452
  },
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,
453
+ }, async (input) => text(await deps.nodes.appendTableRows(actor, input)));
454
+ // The other half of #135: an agent that wrote a wrong finding can correct it. Positions address
455
+ // rows — they carry no IDs on purpose — so every mutation names the version the positions were
456
+ // read from and is refused with `version_conflict` when the table moved on. No `knowledge_*`
457
+ // alias: these tools are new under the post-#125 naming and never carried the old prefix.
458
+ server.registerTool("node_table_update_rows", {
459
+ title: "Update node table rows",
460
+ 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.",
461
+ inputSchema: UpdateTableRowsInput,
272
462
  annotations: {
273
- title: "Update knowledge",
463
+ title: "Update node table rows",
274
464
  readOnlyHint: false,
275
- destructiveHint: false,
465
+ // Replacing a cell discards what stood there; the history keeps it, the table does not.
466
+ destructiveHint: true,
276
467
  idempotentHint: true,
277
468
  openWorldHint: false,
278
469
  },
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,
470
+ }, async (input) => text(await deps.nodes.updateTableRows(actor, input)));
471
+ server.registerTool("node_table_delete_rows", {
472
+ title: "Delete node table rows",
473
+ 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.",
474
+ inputSchema: DeleteTableRowsInput,
284
475
  annotations: {
285
- title: "Archive knowledge",
476
+ title: "Delete node table rows",
286
477
  readOnlyHint: false,
287
478
  destructiveHint: true,
288
479
  idempotentHint: true,
289
480
  openWorldHint: false,
290
481
  },
291
- }, async (input) => text(await deps.knowledge.archive(actor, input)));
482
+ }, async (input) => text(await deps.nodes.deleteTableRows(actor, input)));
483
+ server.registerTool("node_table_redefine", {
484
+ title: "Redefine node table columns",
485
+ 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.",
486
+ inputSchema: RedefineTableInput,
487
+ annotations: {
488
+ title: "Redefine node table columns",
489
+ readOnlyHint: false,
490
+ destructiveHint: true,
491
+ idempotentHint: true,
492
+ openWorldHint: false,
493
+ },
494
+ }, async (input) => text(await deps.nodes.redefineTable(actor, input)));
495
+ registerWithAlias(server, { name: "node_update", deprecated: "knowledge_update" }, {
496
+ title: "Update node",
497
+ description: "Rename, move, or describe a node.",
498
+ inputSchema: UpdateNodeInput,
499
+ annotations: {
500
+ title: "Update node",
501
+ readOnlyHint: false,
502
+ destructiveHint: false,
503
+ idempotentHint: true,
504
+ openWorldHint: false,
505
+ },
506
+ }, async (input) => text(await deps.nodes.update(actor, input)));
507
+ registerWithAlias(server, { name: "node_archive", deprecated: "knowledge_archive" }, {
508
+ title: "Archive node",
509
+ description: "Archive or restore one node using optimistic concurrency. Archiving an agent also switches off its Gate application; restoring switches it back on.",
510
+ inputSchema: ArchiveNodeInput,
511
+ annotations: {
512
+ title: "Archive node",
513
+ readOnlyHint: false,
514
+ destructiveHint: true,
515
+ idempotentHint: true,
516
+ // The one node mutation that can reach a system outside Intel: archiving an agent
517
+ // switches its Gate Application (#182). Nothing else here leaves the installation.
518
+ openWorldHint: true,
519
+ },
520
+ },
521
+ // The caller's own bearer, exactly as the runtime proxy uses it and for the same reason:
522
+ // switching a machine principal is an act in Gate, performed by the person asking.
523
+ async (input) => text(await deps.nodes.archive(actor, input, { token: deps.bearer })));
292
524
  }
293
525
  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,
526
+ registerWithAlias(server, { name: "node_shares_list", deprecated: "knowledge_shares_list" }, {
527
+ title: "List node shares",
528
+ description: "List direct grants for nodes the caller is allowed to manage.",
529
+ inputSchema: ListGrantsInput,
298
530
  annotations: {
299
- title: "List knowledge shares",
531
+ title: "List node shares",
300
532
  readOnlyHint: true,
301
533
  destructiveHint: false,
302
534
  idempotentHint: true,
303
535
  openWorldHint: false,
304
536
  },
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,
537
+ }, async (input) => text(await deps.nodes.listGrants(actor, input.resourceId)));
538
+ registerWithAlias(server, { name: "node_share", deprecated: "knowledge_share" }, {
539
+ title: "Share node",
540
+ description: "Grant inherited node access to a Gate user, verified email, or the organization.",
541
+ inputSchema: ShareInput,
310
542
  annotations: {
311
- title: "Share knowledge",
543
+ title: "Share node",
312
544
  readOnlyHint: false,
313
545
  destructiveHint: false,
314
546
  idempotentHint: true,
315
547
  openWorldHint: false,
316
548
  },
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,
549
+ }, async (input) => text(await deps.nodes.share(actor, input)));
550
+ registerWithAlias(server, { name: "node_revoke_share", deprecated: "knowledge_revoke_share" }, {
551
+ title: "Revoke node share",
552
+ description: "Revoke one direct node grant by ID.",
553
+ inputSchema: RevokeGrantInput,
322
554
  annotations: {
323
- title: "Revoke knowledge share",
555
+ title: "Revoke node share",
324
556
  readOnlyHint: false,
325
557
  destructiveHint: true,
326
558
  idempotentHint: true,
327
559
  openWorldHint: false,
328
560
  },
329
- }, async (input) => text(await deps.knowledge.revokeGrant(actor, input)));
561
+ }, async (input) => text(await deps.nodes.revokeGrant(actor, input)));
330
562
  }
331
563
  if (permits(deps.authorization, "flows", "read")) {
332
564
  server.registerTool("flows_list", {
333
565
  title: "List flows",
334
- description: "List authorized flows, either all of them or the ones filed in one folder of the shared Knowledge tree.",
566
+ description: "List authorized flows, either all of them or the ones filed in one folder of the shared tree.",
335
567
  inputSchema: ListFlowsInput,
336
568
  annotations: {
337
569
  title: "List flows",
@@ -353,6 +585,33 @@ export async function handleMcp(request, deps) {
353
585
  openWorldHint: false,
354
586
  },
355
587
  }, async (input) => text(await deps.flows.get(flowActor, input.flowId)));
588
+ // Named like `knowledge_versions_list`, because it is the same question about the other kind of
589
+ // thing in the tree. This pair is what turns the `versionId` that `flow_publish` and
590
+ // `flow_publish_preview` demand into something an MCP client can actually obtain (#144).
591
+ server.registerTool("flow_versions_list", {
592
+ title: "List flow versions",
593
+ 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.",
594
+ inputSchema: GetFlowInput,
595
+ annotations: {
596
+ title: "List flow versions",
597
+ readOnlyHint: true,
598
+ destructiveHint: false,
599
+ idempotentHint: true,
600
+ openWorldHint: false,
601
+ },
602
+ }, async (input) => text(await deps.flows.listVersions(flowActor, input.flowId)));
603
+ server.registerTool("flow_version_get", {
604
+ title: "Get flow version",
605
+ description: "Get one immutable version of an authorized flow, including its graph.",
606
+ inputSchema: GetFlowVersionInput,
607
+ annotations: {
608
+ title: "Get flow version",
609
+ readOnlyHint: true,
610
+ destructiveHint: false,
611
+ idempotentHint: true,
612
+ openWorldHint: false,
613
+ },
614
+ }, async (input) => text(await deps.flows.getVersion(flowActor, input)));
356
615
  server.registerTool("flow_calls_list", {
357
616
  title: "List called flows",
358
617
  description: "List the flows one flow calls, read out of its graph rather than out of where it is filed.",
@@ -394,7 +653,7 @@ export async function handleMcp(request, deps) {
394
653
  }, async (input) => text(await deps.flows.validate(flowActor, input.flowId)));
395
654
  server.registerTool("flow_requirements_list", {
396
655
  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.",
656
+ 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
657
  inputSchema: GetFlowInput,
399
658
  annotations: {
400
659
  title: "List what a flow needs",
@@ -417,7 +676,7 @@ export async function handleMcp(request, deps) {
417
676
  },
418
677
  }, async (input) => text(await deps.flows.getRun(flowActor, input.runId)));
419
678
  // ⚠️ 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
679
+ // input and output are absent because a run reaches nodes and tools with the rights of
421
680
  // whoever started it, and only their own runs are the calling user's to read out that way.
422
681
  server.registerTool("flow_runs_list", {
423
682
  title: "List flow runs",
@@ -445,7 +704,7 @@ export async function handleMcp(request, deps) {
445
704
  }, async (input) => text(await deps.flows.listRunSteps(flowActor, input.runId)));
446
705
  }
447
706
  // 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).
707
+ // node_share, so a narrower grant cannot sit beside the folder grant (ADR-0004 §2).
449
708
  if (permits(deps.authorization, "flows", "create")) {
450
709
  server.registerTool("flow_create", {
451
710
  title: "Create flow",
@@ -463,7 +722,7 @@ export async function handleMcp(request, deps) {
463
722
  if (permits(deps.authorization, "flows", "write")) {
464
723
  server.registerTool("flow_update", {
465
724
  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.",
725
+ description: "Rename a flow, change its description, or move it to another folder of the shared tree. Never changes what the flow does.",
467
726
  inputSchema: UpdateFlowInput,
468
727
  annotations: {
469
728
  title: "Rename or move flow",
@@ -518,7 +777,7 @@ export async function handleMcp(request, deps) {
518
777
  }, async (input) => text(await deps.flows.previewPublish(flowActor, input)));
519
778
  server.registerTool("flow_publish", {
520
779
  title: "Publish flow",
521
- description: "Publish a version after resolving its authorized Knowledge and tool references.",
780
+ description: "Publish a version after resolving its authorized node and tool references.",
522
781
  inputSchema: PublishFlowInput,
523
782
  annotations: {
524
783
  title: "Publish flow",
@@ -528,6 +787,20 @@ export async function handleMcp(request, deps) {
528
787
  openWorldHint: false,
529
788
  },
530
789
  }, async (input) => text(await deps.flows.publish(flowActor, input)));
790
+ server.registerTool("flow_unpublish", {
791
+ title: "Unpublish flow",
792
+ 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.",
793
+ inputSchema: UnpublishFlowInput,
794
+ annotations: {
795
+ title: "Unpublish flow",
796
+ readOnlyHint: false,
797
+ // Nothing is deleted and republishing is one publish away — destructive all the same,
798
+ // because a flow another flow calls stops resolving, like flow_archive.
799
+ destructiveHint: true,
800
+ idempotentHint: true,
801
+ openWorldHint: false,
802
+ },
803
+ }, async (input) => text(await deps.flows.unpublish(flowActor, input)));
531
804
  }
532
805
  if (permits(deps.authorization, "flows", "run")) {
533
806
  server.registerTool("flow_run_start", {
@@ -554,6 +827,92 @@ export async function handleMcp(request, deps) {
554
827
  openWorldHint: true,
555
828
  },
556
829
  }, async (input) => text(await deps.flows.completeStep(flowActor, input)));
830
+ server.registerTool("flow_run_cancel", {
831
+ title: "Cancel flow run",
832
+ 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.",
833
+ inputSchema: CancelFlowRunInput,
834
+ annotations: {
835
+ title: "Cancel flow run",
836
+ readOnlyHint: false,
837
+ // Nothing is deleted, but work in flight is ended for the run and its children — a
838
+ // client deciding whether to ask first has to be told that.
839
+ destructiveHint: true,
840
+ idempotentHint: true,
841
+ openWorldHint: false,
842
+ },
843
+ }, async (input) => text(await deps.flows.cancelRun(flowActor, input)));
844
+ }
845
+ /**
846
+ * Driving an agent, over the same door the screen uses.
847
+ *
848
+ * ⚠️ The three below are the MCP half of the two head actions on the agent page — the repository's
849
+ * "one business layer, three thin surfaces" rule, kept in the same delivery. They call the runtime
850
+ * through `deps.agents`, which resolves the resource ACL and forwards the CALLER's bearer; a tool
851
+ * that reached the runtime with anything else would be Intel acting as somebody it is not.
852
+ */
853
+ if (permits(deps.authorization, "agents", "run")) {
854
+ server.registerTool("agent_pause", {
855
+ title: "Pause agent",
856
+ 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.",
857
+ inputSchema: PauseAgentInput,
858
+ annotations: {
859
+ title: "Pause agent",
860
+ readOnlyHint: false,
861
+ // Nothing is deleted, and yet work that would have happened does not — a client deciding
862
+ // whether to ask first has to be told that.
863
+ destructiveHint: true,
864
+ idempotentHint: true,
865
+ openWorldHint: false,
866
+ },
867
+ }, async (input) => text(await deps.agents.request({
868
+ actor: agentActor,
869
+ token: deps.bearer,
870
+ agentId: input.nodeId,
871
+ method: "POST",
872
+ path: "/pause",
873
+ body: "{}",
874
+ })));
875
+ server.registerTool("agent_resume", {
876
+ title: "Resume agent",
877
+ 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.",
878
+ inputSchema: PauseAgentInput,
879
+ annotations: {
880
+ title: "Resume agent",
881
+ readOnlyHint: false,
882
+ destructiveHint: false,
883
+ idempotentHint: true,
884
+ openWorldHint: false,
885
+ },
886
+ }, async (input) => text(await deps.agents.request({
887
+ actor: agentActor,
888
+ token: deps.bearer,
889
+ agentId: input.nodeId,
890
+ method: "POST",
891
+ path: "/resume",
892
+ body: "{}",
893
+ })));
894
+ server.registerTool("agent_run_now", {
895
+ title: "Run agent now",
896
+ 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.",
897
+ inputSchema: RunAgentNowInput,
898
+ annotations: {
899
+ title: "Run agent now",
900
+ readOnlyHint: false,
901
+ destructiveHint: false,
902
+ // Every run is a new run, so asking twice runs it twice.
903
+ idempotentHint: false,
904
+ // The agent may reach whatever its own definition grants it, so what a run touches is not
905
+ // knowable from this schema.
906
+ openWorldHint: true,
907
+ },
908
+ }, async (input) => text(await deps.agents.request({
909
+ actor: agentActor,
910
+ token: deps.bearer,
911
+ agentId: input.nodeId,
912
+ method: "POST",
913
+ path: "/run",
914
+ body: JSON.stringify({ target: input.target }),
915
+ })));
557
916
  }
558
917
  if (permits(deps.authorization, "tools", "read")) {
559
918
  server.registerTool("tools_catalog_list", {