@anchrd/intel-api 0.3.0 → 0.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/adapters/cloudflare/cloudflare.js +12 -12
- package/dist/adapters/db/db-flows.js +417 -188
- package/dist/adapters/db/db-grants.d.ts +47 -0
- package/dist/adapters/db/db-grants.js +125 -0
- package/dist/adapters/db/db-indexing.js +14 -3
- package/dist/adapters/db/db.js +236 -202
- package/dist/flows/flows.d.ts +31 -1
- package/dist/flows/flows.js +988 -74
- package/dist/flows/flows.types.d.ts +104 -30
- package/dist/http/http.js +154 -46
- package/dist/indexing/indexing.js +14 -3
- package/dist/knowledge/document-links/document-links.d.ts +15 -0
- package/dist/knowledge/document-links/document-links.js +52 -0
- package/dist/knowledge/knowledge.js +357 -73
- package/dist/knowledge/knowledge.types.d.ts +60 -27
- package/dist/mcp/mcp.js +168 -74
- package/dist/shared/csv/csv.d.ts +13 -0
- package/dist/shared/csv/csv.js +85 -0
- package/dist/shared/gate-authorization/gate-authorization.d.ts +1 -0
- package/dist/shared/gate-authorization/gate-authorization.js +7 -0
- package/dist/tools/tools.js +14 -1
- package/migrations/0002_flows_in_the_knowledge_tree.sql +34 -0
- package/migrations/0003_folder_permissions.sql +211 -0
- package/migrations/0004_subflow_runs.sql +14 -0
- package/migrations/0005_flow_node_cleanup.sql +28 -0
- package/migrations/0005_tables_in_the_knowledge_tree.sql +39 -0
- package/migrations/0006_links_are_written_in_the_text.sql +20 -0
- package/package.json +1 -1
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { AppendKnowledgeTableRowsInput, AppendKnowledgeTableRowsResult, ArchiveKnowledgeNodeInput, CreateKnowledgeNodeInput, DefineKnowledgeTableInput, KnowledgeAttachment, KnowledgeCitation, KnowledgeDocument, KnowledgeGraph, KnowledgeGraphInput, KnowledgeLink, KnowledgeNode, KnowledgeTable, KnowledgeVersion, ListKnowledgeNodesInput, ResolveKnowledgeLinksInput, ResolveKnowledgeLinksResult, ResourceGrant, ResourceGrantList, ResourceVerb, RevokeKnowledgeGrantInput, SaveKnowledgeAttachmentInput, SaveKnowledgeVersionInput, SearchKnowledgeInput, ShareKnowledgeInput, ShareKnowledgeResult, UpdateKnowledgeNodeInput } from "@anchrd/intel-contract";
|
|
2
2
|
import type { SemanticIndex } from "../adapters/semantic-index/semantic-index.types.js";
|
|
3
3
|
export interface Actor {
|
|
4
4
|
id: string;
|
|
5
5
|
email: string;
|
|
6
|
-
|
|
6
|
+
isAdmin?: boolean;
|
|
7
7
|
}
|
|
8
8
|
export interface NewKnowledgeNode {
|
|
9
9
|
node: KnowledgeNode;
|
|
@@ -18,16 +18,28 @@ export interface NewKnowledgeVersion {
|
|
|
18
18
|
idempotencyKey: string;
|
|
19
19
|
auditId: string;
|
|
20
20
|
}
|
|
21
|
+
export interface NewKnowledgeTableVersion {
|
|
22
|
+
version: Omit<KnowledgeVersion, "sequence">;
|
|
23
|
+
actorId: string;
|
|
24
|
+
idempotencyKey: string;
|
|
25
|
+
auditId: string;
|
|
26
|
+
}
|
|
21
27
|
export interface KnowledgeRepository {
|
|
22
28
|
listVisible(actor: Actor, input: ListKnowledgeNodesInput): Promise<KnowledgeNode[]>;
|
|
29
|
+
listVisibleBounded(actor: Actor, input: {
|
|
30
|
+
parentId: string | null;
|
|
31
|
+
limit: number;
|
|
32
|
+
}): Promise<BoundedChildren>;
|
|
23
33
|
getVisible(actor: Actor, nodeId: string): Promise<KnowledgeNode | null>;
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
findIdempotentNode(actorId: string, operation: "knowledge.create" | "knowledge.save" | "knowledge.update" | "knowledge.archive" | "knowledge.link" | "knowledge.unlink" | "knowledge.share" | "knowledge.revoke", idempotencyKey: string): Promise<string | null>;
|
|
34
|
+
can(actor: Actor, nodeId: string, verb: ResourceVerb): Promise<boolean>;
|
|
35
|
+
findIdempotentNode(actorId: string, operation: "knowledge.create" | "knowledge.save" | "knowledge.append" | "knowledge.update" | "knowledge.archive" | "knowledge.share" | "knowledge.revoke", idempotencyKey: string): Promise<string | null>;
|
|
27
36
|
findIdempotentRevocation(actorId: string, idempotencyKey: string): Promise<boolean | null>;
|
|
28
37
|
insertNode(input: NewKnowledgeNode): Promise<KnowledgeNode>;
|
|
29
38
|
getVersion(versionId: string): Promise<KnowledgeVersion | null>;
|
|
30
39
|
appendVersion(input: NewKnowledgeVersion): Promise<"saved" | "conflict">;
|
|
40
|
+
appendTableVersion(input: NewKnowledgeTableVersion): Promise<KnowledgeVersion>;
|
|
41
|
+
listVersionContentKeys(nodeId: string): Promise<string[]>;
|
|
42
|
+
firstVersionContentKey(nodeId: string): Promise<string | null>;
|
|
31
43
|
listVersions(nodeId: string): Promise<KnowledgeVersion[]>;
|
|
32
44
|
updateNode(input: {
|
|
33
45
|
node: KnowledgeNode;
|
|
@@ -46,23 +58,22 @@ export interface KnowledgeRepository {
|
|
|
46
58
|
auditId: string;
|
|
47
59
|
}): Promise<"conflict" | KnowledgeNode>;
|
|
48
60
|
listGrants(resourceId: string): Promise<ResourceGrant[]>;
|
|
61
|
+
organizationExecuteReaches(nodeId: string, exceptGrantId: string): Promise<boolean>;
|
|
49
62
|
listLinksVisible(actor: Actor, nodeId: string): Promise<KnowledgeLink[]>;
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
idempotencyKey: string;
|
|
56
|
-
auditId: string;
|
|
57
|
-
}): Promise<KnowledgeLink>;
|
|
58
|
-
deleteLink(input: {
|
|
63
|
+
resolveVisibleTitles(actor: Actor, nodeIds: string[]): Promise<Array<{
|
|
64
|
+
nodeId: string;
|
|
65
|
+
title: string;
|
|
66
|
+
}>>;
|
|
67
|
+
replaceTextLinks(input: {
|
|
59
68
|
sourceNodeId: string;
|
|
60
|
-
|
|
69
|
+
links: Array<{
|
|
70
|
+
id: string;
|
|
71
|
+
targetNodeId: string;
|
|
72
|
+
}>;
|
|
61
73
|
actorId: string;
|
|
62
|
-
idempotencyKey: string;
|
|
63
74
|
auditId: string;
|
|
64
75
|
occurredAt: string;
|
|
65
|
-
}): Promise<
|
|
76
|
+
}): Promise<void>;
|
|
66
77
|
graphVisible(actor: Actor, input: KnowledgeGraphInput): Promise<KnowledgeGraph>;
|
|
67
78
|
setGrant(input: {
|
|
68
79
|
grant: ResourceGrant;
|
|
@@ -102,20 +113,46 @@ export interface KnowledgeDeps {
|
|
|
102
113
|
content: ContentStore;
|
|
103
114
|
id(): string;
|
|
104
115
|
now(): Date;
|
|
116
|
+
externalFlowCallers(actor: Actor, folderId: string): Promise<{
|
|
117
|
+
visible: string[];
|
|
118
|
+
hidden: number;
|
|
119
|
+
}>;
|
|
120
|
+
flowKnowledgeReferences(actor: Actor, folderId: string): Promise<string[]>;
|
|
105
121
|
hash(content: string | Uint8Array): Promise<string>;
|
|
106
122
|
indexing: {
|
|
107
123
|
enqueue(versionId: string): Promise<void>;
|
|
108
124
|
};
|
|
109
125
|
semantic?: SemanticIndex | undefined;
|
|
110
126
|
}
|
|
127
|
+
export type FolderAccess = "ok" | "missing" | "not-a-folder" | "forbidden";
|
|
128
|
+
export interface BoundedChildren {
|
|
129
|
+
items: KnowledgeNode[];
|
|
130
|
+
total: number;
|
|
131
|
+
}
|
|
111
132
|
export interface KnowledgeService {
|
|
112
133
|
list(actor: Actor, input: ListKnowledgeNodesInput): Promise<{
|
|
113
134
|
items: KnowledgeNode[];
|
|
114
135
|
}>;
|
|
136
|
+
/**
|
|
137
|
+
* One level of the tree as this actor may see it, bounded: at most `limit` current children, and
|
|
138
|
+
* `total` for how many there are. Its caller is the relation graph, which draws a bounded picture
|
|
139
|
+
* and must not read a folder of a thousand documents to do it (#30).
|
|
140
|
+
*
|
|
141
|
+
* ⚠️ `total` counts what passed the visibility predicate and nothing else, or the size a picture
|
|
142
|
+
* reports would become a way of learning that something is filed here.
|
|
143
|
+
*/
|
|
144
|
+
childrenBounded(actor: Actor, input: {
|
|
145
|
+
parentId: string | null;
|
|
146
|
+
limit: number;
|
|
147
|
+
}): Promise<BoundedChildren>;
|
|
115
148
|
get(actor: Actor, nodeId: string): Promise<KnowledgeDocument>;
|
|
149
|
+
folderAccess(actor: Actor, folderId: string): Promise<FolderAccess>;
|
|
116
150
|
create(actor: Actor, input: CreateKnowledgeNodeInput): Promise<KnowledgeNode>;
|
|
117
151
|
save(actor: Actor, input: SaveKnowledgeVersionInput): Promise<KnowledgeDocument>;
|
|
118
152
|
saveAttachment(actor: Actor, input: SaveKnowledgeAttachmentInput): Promise<KnowledgeDocument>;
|
|
153
|
+
getTable(actor: Actor, nodeId: string): Promise<KnowledgeTable>;
|
|
154
|
+
defineTable(actor: Actor, input: DefineKnowledgeTableInput): Promise<KnowledgeTable>;
|
|
155
|
+
appendTableRows(actor: Actor, input: AppendKnowledgeTableRowsInput): Promise<AppendKnowledgeTableRowsResult>;
|
|
119
156
|
getAttachment(actor: Actor, nodeId: string): Promise<KnowledgeAttachment>;
|
|
120
157
|
readAttachment(actor: Actor, nodeId: string): Promise<KnowledgeAttachmentBody>;
|
|
121
158
|
listVersions(actor: Actor, nodeId: string): Promise<{
|
|
@@ -123,18 +160,14 @@ export interface KnowledgeService {
|
|
|
123
160
|
}>;
|
|
124
161
|
update(actor: Actor, input: UpdateKnowledgeNodeInput): Promise<KnowledgeNode>;
|
|
125
162
|
archive(actor: Actor, input: ArchiveKnowledgeNodeInput): Promise<KnowledgeNode>;
|
|
126
|
-
listGrants(actor: Actor, resourceId: string): Promise<
|
|
127
|
-
items: ResourceGrant[];
|
|
128
|
-
}>;
|
|
163
|
+
listGrants(actor: Actor, resourceId: string): Promise<ResourceGrantList>;
|
|
129
164
|
listLinks(actor: Actor, nodeId: string): Promise<{
|
|
130
165
|
items: KnowledgeLink[];
|
|
131
166
|
}>;
|
|
132
|
-
|
|
133
|
-
deleteLink(actor: Actor, input: DeleteKnowledgeLinkInput): Promise<{
|
|
134
|
-
deleted: boolean;
|
|
135
|
-
}>;
|
|
167
|
+
resolveLinks(actor: Actor, input: ResolveKnowledgeLinksInput): Promise<ResolveKnowledgeLinksResult>;
|
|
136
168
|
graph(actor: Actor, input: KnowledgeGraphInput): Promise<KnowledgeGraph>;
|
|
137
|
-
|
|
169
|
+
visibleNode(actor: Actor, nodeId: string): Promise<KnowledgeNode | null>;
|
|
170
|
+
share(actor: Actor, input: ShareKnowledgeInput): Promise<ShareKnowledgeResult>;
|
|
138
171
|
revokeGrant(actor: Actor, input: RevokeKnowledgeGrantInput): Promise<{
|
|
139
172
|
revoked: boolean;
|
|
140
173
|
}>;
|
|
@@ -149,9 +182,9 @@ export interface KnowledgeIndexTarget {
|
|
|
149
182
|
nodeId: string;
|
|
150
183
|
versionId: string;
|
|
151
184
|
title: string;
|
|
152
|
-
|
|
185
|
+
contentKeys: string[];
|
|
153
186
|
mediaType: string;
|
|
154
|
-
kind: "document" | "attachment";
|
|
187
|
+
kind: "document" | "attachment" | "table";
|
|
155
188
|
updatedAt: string;
|
|
156
189
|
}
|
|
157
190
|
export interface KnowledgeIndexRepository {
|
package/dist/mcp/mcp.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import { ArchiveKnowledgeNodeInput, CompleteFlowRunStepInput, CreateFlowInput,
|
|
1
|
+
import { AppendKnowledgeTableRowsInput, 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
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
|
+
import { permits } from "../shared/gate-authorization/gate-authorization.js";
|
|
5
6
|
import { IntelError } from "../shared/intel-error/intel-error.js";
|
|
6
7
|
function text(value) {
|
|
7
8
|
return { content: [{ type: "text", text: JSON.stringify(value) }], isError: false };
|
|
@@ -27,25 +28,26 @@ export async function handleMcp(request, deps) {
|
|
|
27
28
|
const actor = {
|
|
28
29
|
id: deps.authorization.identity.id,
|
|
29
30
|
email: deps.authorization.identity.email,
|
|
30
|
-
|
|
31
|
+
isAdmin: deps.authorization.can("intel", "admin"),
|
|
31
32
|
};
|
|
32
33
|
const toolActor = {
|
|
33
34
|
id: deps.authorization.identity.id,
|
|
34
35
|
email: deps.authorization.identity.email,
|
|
35
|
-
canExecute: deps.authorization
|
|
36
|
+
canExecute: permits(deps.authorization, "tools", "execute"),
|
|
36
37
|
};
|
|
37
38
|
const flowActor = {
|
|
38
39
|
id: deps.authorization.identity.id,
|
|
39
40
|
email: deps.authorization.identity.email,
|
|
40
|
-
canRun: deps.authorization
|
|
41
|
-
canApprove: deps.authorization
|
|
41
|
+
canRun: permits(deps.authorization, "flows", "run"),
|
|
42
|
+
canApprove: permits(deps.authorization, "flows", "approve"),
|
|
43
|
+
isAdmin: deps.authorization.can("intel", "admin"),
|
|
42
44
|
};
|
|
43
45
|
const server = new McpServer({ name: "intel", version: "0.1.0" });
|
|
44
46
|
const EmptyInput = z.strictObject({});
|
|
45
|
-
if (deps.authorization
|
|
47
|
+
if (permits(deps.authorization, "knowledge", "read")) {
|
|
46
48
|
server.registerTool("knowledge_list", {
|
|
47
49
|
title: "List knowledge",
|
|
48
|
-
description: "List authorized folders, documents, and
|
|
50
|
+
description: "List authorized folders, documents, attachments, and tables under one parent.",
|
|
49
51
|
inputSchema: ListKnowledgeNodesInput,
|
|
50
52
|
annotations: {
|
|
51
53
|
title: "List knowledge",
|
|
@@ -127,6 +129,18 @@ export async function handleMcp(request, deps) {
|
|
|
127
129
|
openWorldHint: false,
|
|
128
130
|
},
|
|
129
131
|
}, async (input) => text(await deps.knowledge.search(actor, input)));
|
|
132
|
+
server.registerTool("knowledge_table_get", {
|
|
133
|
+
title: "Get knowledge table",
|
|
134
|
+
description: "Read one authorized table as column names and rows.",
|
|
135
|
+
inputSchema: GetKnowledgeTableInput,
|
|
136
|
+
annotations: {
|
|
137
|
+
title: "Get knowledge table",
|
|
138
|
+
readOnlyHint: true,
|
|
139
|
+
destructiveHint: false,
|
|
140
|
+
idempotentHint: true,
|
|
141
|
+
openWorldHint: false,
|
|
142
|
+
},
|
|
143
|
+
}, async (input) => text(await deps.knowledge.getTable(actor, input.nodeId)));
|
|
130
144
|
server.registerTool("knowledge_links_list", {
|
|
131
145
|
title: "List knowledge links",
|
|
132
146
|
description: "List authorized outgoing links and backlinks for one Knowledge node.",
|
|
@@ -139,6 +153,25 @@ export async function handleMcp(request, deps) {
|
|
|
139
153
|
openWorldHint: false,
|
|
140
154
|
},
|
|
141
155
|
}, async (input) => text(await deps.knowledge.listLinks(actor, input.nodeId)));
|
|
156
|
+
// The reader's half of a document link (#41): the titles of the linked documents this caller
|
|
157
|
+
// may see. There is no tool to create or delete a link — a relationship is written in the text
|
|
158
|
+
// and `knowledge_save` is what records it, so there is only one way to make one.
|
|
159
|
+
//
|
|
160
|
+
// ⚠️ A target this caller may not reach, or one that is gone, is simply absent from the answer.
|
|
161
|
+
// The two are indistinguishable on purpose: telling them apart would confirm that a document
|
|
162
|
+
// exists somewhere they cannot look.
|
|
163
|
+
server.registerTool("knowledge_links_resolve", {
|
|
164
|
+
title: "Resolve knowledge links",
|
|
165
|
+
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.",
|
|
166
|
+
inputSchema: ResolveKnowledgeLinksInput,
|
|
167
|
+
annotations: {
|
|
168
|
+
title: "Resolve knowledge links",
|
|
169
|
+
readOnlyHint: true,
|
|
170
|
+
destructiveHint: false,
|
|
171
|
+
idempotentHint: true,
|
|
172
|
+
openWorldHint: false,
|
|
173
|
+
},
|
|
174
|
+
}, async (input) => text(await deps.knowledge.resolveLinks(actor, input)));
|
|
142
175
|
server.registerTool("knowledge_graph", {
|
|
143
176
|
title: "Get knowledge graph",
|
|
144
177
|
description: "Get the authorized Knowledge nodes and explicit relationships for discovery.",
|
|
@@ -166,10 +199,10 @@ export async function handleMcp(request, deps) {
|
|
|
166
199
|
},
|
|
167
200
|
}, async () => text(await deps.knowledge.reindex(actor)));
|
|
168
201
|
}
|
|
169
|
-
if (deps.authorization
|
|
202
|
+
if (permits(deps.authorization, "knowledge", "create")) {
|
|
170
203
|
server.registerTool("knowledge_create", {
|
|
171
204
|
title: "Create knowledge",
|
|
172
|
-
description: "Create a governed folder, document, or
|
|
205
|
+
description: "Create a governed folder, document, attachment, or table node.",
|
|
173
206
|
inputSchema: CreateKnowledgeNodeInput,
|
|
174
207
|
annotations: {
|
|
175
208
|
title: "Create knowledge",
|
|
@@ -180,7 +213,7 @@ export async function handleMcp(request, deps) {
|
|
|
180
213
|
},
|
|
181
214
|
}, async (input) => text(await deps.knowledge.create(actor, input)));
|
|
182
215
|
}
|
|
183
|
-
if (deps.authorization
|
|
216
|
+
if (permits(deps.authorization, "knowledge", "write")) {
|
|
184
217
|
server.registerTool("knowledge_save", {
|
|
185
218
|
title: "Save knowledge version",
|
|
186
219
|
description: "Append an immutable content version using an optimistic base version.",
|
|
@@ -205,6 +238,34 @@ export async function handleMcp(request, deps) {
|
|
|
205
238
|
openWorldHint: false,
|
|
206
239
|
},
|
|
207
240
|
}, async (input) => text(await deps.knowledge.saveAttachment(actor, input)));
|
|
241
|
+
server.registerTool("knowledge_table_define", {
|
|
242
|
+
title: "Define knowledge table columns",
|
|
243
|
+
description: "Write the column names of an empty table. The header is the contract every append is checked against and cannot be rewritten.",
|
|
244
|
+
inputSchema: DefineKnowledgeTableInput,
|
|
245
|
+
annotations: {
|
|
246
|
+
title: "Define knowledge table columns",
|
|
247
|
+
readOnlyHint: false,
|
|
248
|
+
destructiveHint: false,
|
|
249
|
+
idempotentHint: true,
|
|
250
|
+
openWorldHint: false,
|
|
251
|
+
},
|
|
252
|
+
}, async (input) => text(await deps.knowledge.defineTable(actor, input)));
|
|
253
|
+
// The tool #40 exists for: an agent collecting findings on a schedule appends them without
|
|
254
|
+
// reading or resending what is already there, and two agents appending at once lose nothing.
|
|
255
|
+
server.registerTool("knowledge_table_append", {
|
|
256
|
+
title: "Append knowledge table rows",
|
|
257
|
+
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.",
|
|
258
|
+
inputSchema: AppendKnowledgeTableRowsInput,
|
|
259
|
+
annotations: {
|
|
260
|
+
title: "Append knowledge table rows",
|
|
261
|
+
readOnlyHint: false,
|
|
262
|
+
destructiveHint: false,
|
|
263
|
+
// The idempotency key makes a repeat of the same call a no-op; without one, appending
|
|
264
|
+
// twice appends twice, which is what appending means.
|
|
265
|
+
idempotentHint: true,
|
|
266
|
+
openWorldHint: false,
|
|
267
|
+
},
|
|
268
|
+
}, async (input) => text(await deps.knowledge.appendTableRows(actor, input)));
|
|
208
269
|
server.registerTool("knowledge_update", {
|
|
209
270
|
title: "Update knowledge",
|
|
210
271
|
description: "Rename, move, describe, or change the context policy of Knowledge.",
|
|
@@ -229,32 +290,8 @@ export async function handleMcp(request, deps) {
|
|
|
229
290
|
openWorldHint: false,
|
|
230
291
|
},
|
|
231
292
|
}, async (input) => text(await deps.knowledge.archive(actor, input)));
|
|
232
|
-
server.registerTool("knowledge_link_create", {
|
|
233
|
-
title: "Link knowledge",
|
|
234
|
-
description: "Create a governed relationship between two authorized Knowledge nodes.",
|
|
235
|
-
inputSchema: CreateKnowledgeLinkInput,
|
|
236
|
-
annotations: {
|
|
237
|
-
title: "Link knowledge",
|
|
238
|
-
readOnlyHint: false,
|
|
239
|
-
destructiveHint: false,
|
|
240
|
-
idempotentHint: true,
|
|
241
|
-
openWorldHint: false,
|
|
242
|
-
},
|
|
243
|
-
}, async (input) => text(await deps.knowledge.createLink(actor, input)));
|
|
244
|
-
server.registerTool("knowledge_link_delete", {
|
|
245
|
-
title: "Delete knowledge link",
|
|
246
|
-
description: "Delete one governed Knowledge relationship by ID.",
|
|
247
|
-
inputSchema: DeleteKnowledgeLinkInput,
|
|
248
|
-
annotations: {
|
|
249
|
-
title: "Delete knowledge link",
|
|
250
|
-
readOnlyHint: false,
|
|
251
|
-
destructiveHint: true,
|
|
252
|
-
idempotentHint: true,
|
|
253
|
-
openWorldHint: false,
|
|
254
|
-
},
|
|
255
|
-
}, async (input) => text(await deps.knowledge.deleteLink(actor, input)));
|
|
256
293
|
}
|
|
257
|
-
if (deps.authorization
|
|
294
|
+
if (permits(deps.authorization, "knowledge", "share")) {
|
|
258
295
|
server.registerTool("knowledge_shares_list", {
|
|
259
296
|
title: "List knowledge shares",
|
|
260
297
|
description: "List direct grants for Knowledge the caller is allowed to manage.",
|
|
@@ -292,11 +329,11 @@ export async function handleMcp(request, deps) {
|
|
|
292
329
|
},
|
|
293
330
|
}, async (input) => text(await deps.knowledge.revokeGrant(actor, input)));
|
|
294
331
|
}
|
|
295
|
-
if (deps.authorization
|
|
332
|
+
if (permits(deps.authorization, "flows", "read")) {
|
|
296
333
|
server.registerTool("flows_list", {
|
|
297
334
|
title: "List flows",
|
|
298
|
-
description: "List authorized
|
|
299
|
-
inputSchema:
|
|
335
|
+
description: "List authorized flows, either all of them or the ones filed in one folder of the shared Knowledge tree.",
|
|
336
|
+
inputSchema: ListFlowsInput,
|
|
300
337
|
annotations: {
|
|
301
338
|
title: "List flows",
|
|
302
339
|
readOnlyHint: true,
|
|
@@ -304,7 +341,7 @@ export async function handleMcp(request, deps) {
|
|
|
304
341
|
idempotentHint: true,
|
|
305
342
|
openWorldHint: false,
|
|
306
343
|
},
|
|
307
|
-
}, async () => text(await deps.flows.list(flowActor)));
|
|
344
|
+
}, async (input) => text(await deps.flows.list(flowActor, input)));
|
|
308
345
|
server.registerTool("flow_get", {
|
|
309
346
|
title: "Get flow",
|
|
310
347
|
description: "Load one authorized flow and its current immutable graph on demand.",
|
|
@@ -317,6 +354,45 @@ export async function handleMcp(request, deps) {
|
|
|
317
354
|
openWorldHint: false,
|
|
318
355
|
},
|
|
319
356
|
}, async (input) => text(await deps.flows.get(flowActor, input.flowId)));
|
|
357
|
+
server.registerTool("flow_calls_list", {
|
|
358
|
+
title: "List called flows",
|
|
359
|
+
description: "List the flows one flow calls, read out of its graph rather than out of where it is filed.",
|
|
360
|
+
inputSchema: GetFlowInput,
|
|
361
|
+
annotations: {
|
|
362
|
+
title: "List called flows",
|
|
363
|
+
readOnlyHint: true,
|
|
364
|
+
destructiveHint: false,
|
|
365
|
+
idempotentHint: true,
|
|
366
|
+
openWorldHint: false,
|
|
367
|
+
},
|
|
368
|
+
}, async (input) => text(await deps.flows.listCalls(flowActor, input.flowId)));
|
|
369
|
+
// ⚠️ Only nodes the requesting user may see are in the answer, placeholders included: an edge to
|
|
370
|
+
// a grey box would already say that something is there. The same rule the screen follows,
|
|
371
|
+
// because it is the same service (#19).
|
|
372
|
+
server.registerTool("flow_relation_graph", {
|
|
373
|
+
title: "Read the relation graph",
|
|
374
|
+
description: "Read what accesses what for one folder or one flow: which flow reads which document and which flow calls which flow.",
|
|
375
|
+
inputSchema: RelationGraphInput,
|
|
376
|
+
annotations: {
|
|
377
|
+
title: "Read the relation graph",
|
|
378
|
+
readOnlyHint: true,
|
|
379
|
+
destructiveHint: false,
|
|
380
|
+
idempotentHint: true,
|
|
381
|
+
openWorldHint: false,
|
|
382
|
+
},
|
|
383
|
+
}, async (input) => text(await deps.flows.relationGraph(flowActor, input)));
|
|
384
|
+
server.registerTool("flow_requirements_list", {
|
|
385
|
+
title: "List what a flow needs",
|
|
386
|
+
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.",
|
|
387
|
+
inputSchema: GetFlowInput,
|
|
388
|
+
annotations: {
|
|
389
|
+
title: "List what a flow needs",
|
|
390
|
+
readOnlyHint: true,
|
|
391
|
+
destructiveHint: false,
|
|
392
|
+
idempotentHint: true,
|
|
393
|
+
openWorldHint: false,
|
|
394
|
+
},
|
|
395
|
+
}, async (input) => text(await deps.flows.listRequirements(flowActor, input.flowId)));
|
|
320
396
|
server.registerTool("flow_run_get", {
|
|
321
397
|
title: "Get flow run",
|
|
322
398
|
description: "Get durable run state and the next node an AI or person must handle.",
|
|
@@ -329,46 +405,37 @@ export async function handleMcp(request, deps) {
|
|
|
329
405
|
openWorldHint: false,
|
|
330
406
|
},
|
|
331
407
|
}, async (input) => text(await deps.flows.getRun(flowActor, input.runId)));
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
408
|
+
// ⚠️ Model context like every other tool result: what a run did, never what it produced. The run
|
|
409
|
+
// input and output are absent because a run reaches Knowledge and tools with the rights of
|
|
410
|
+
// whoever started it, and only their own runs are the calling user's to read out that way.
|
|
411
|
+
server.registerTool("flow_runs_list", {
|
|
412
|
+
title: "List flow runs",
|
|
413
|
+
description: "List the runs of one flow, newest first, with status, start, duration, what triggered them, and for a failed run the step that ended it. Optionally only the failed ones. Runs the calling user may not see are absent, and a failure inside a called flow they may not see is named by the calling step alone.",
|
|
414
|
+
inputSchema: ListFlowRunsInput,
|
|
415
|
+
annotations: {
|
|
416
|
+
title: "List flow runs",
|
|
340
417
|
readOnlyHint: true,
|
|
341
418
|
destructiveHint: false,
|
|
342
419
|
idempotentHint: true,
|
|
343
420
|
openWorldHint: false,
|
|
344
421
|
},
|
|
345
|
-
}, async (input) => text(await deps.flows.
|
|
346
|
-
server.registerTool("
|
|
347
|
-
title: "
|
|
348
|
-
description: "
|
|
349
|
-
inputSchema:
|
|
422
|
+
}, async (input) => text(await deps.flows.listRuns(flowActor, input)));
|
|
423
|
+
server.registerTool("flow_run_steps_list", {
|
|
424
|
+
title: "List flow run steps",
|
|
425
|
+
description: "List every completed step of one run, oldest first, with its outcome, branch, and the reason a failed step gives, plus the call chain the run belongs to.",
|
|
426
|
+
inputSchema: GetFlowRunInput,
|
|
350
427
|
annotations: {
|
|
351
|
-
title: "
|
|
352
|
-
readOnlyHint:
|
|
428
|
+
title: "List flow run steps",
|
|
429
|
+
readOnlyHint: true,
|
|
353
430
|
destructiveHint: false,
|
|
354
431
|
idempotentHint: true,
|
|
355
432
|
openWorldHint: false,
|
|
356
433
|
},
|
|
357
|
-
}, async (input) => text(await deps.flows.
|
|
358
|
-
server.registerTool("flow_revoke_share", {
|
|
359
|
-
title: "Revoke flow share",
|
|
360
|
-
description: "Revoke one direct Flow grant by ID.",
|
|
361
|
-
inputSchema: RevokeFlowGrantInput,
|
|
362
|
-
annotations: {
|
|
363
|
-
title: "Revoke flow share",
|
|
364
|
-
readOnlyHint: false,
|
|
365
|
-
destructiveHint: true,
|
|
366
|
-
idempotentHint: true,
|
|
367
|
-
openWorldHint: false,
|
|
368
|
-
},
|
|
369
|
-
}, async (input) => text(await deps.flows.revokeGrant(flowActor, input)));
|
|
434
|
+
}, async (input) => text(await deps.flows.listRunSteps(flowActor, input.runId)));
|
|
370
435
|
}
|
|
371
|
-
|
|
436
|
+
// A flow has no share tool of its own. Sharing happens on the folder a flow is filed in, through
|
|
437
|
+
// knowledge_share, so a narrower grant cannot sit beside the folder grant (ADR-0004 §2).
|
|
438
|
+
if (permits(deps.authorization, "flows", "create")) {
|
|
372
439
|
server.registerTool("flow_create", {
|
|
373
440
|
title: "Create flow",
|
|
374
441
|
description: "Create governed flow metadata before adding an immutable graph version.",
|
|
@@ -382,7 +449,19 @@ export async function handleMcp(request, deps) {
|
|
|
382
449
|
},
|
|
383
450
|
}, async (input) => text(await deps.flows.create(flowActor, input)));
|
|
384
451
|
}
|
|
385
|
-
if (deps.authorization
|
|
452
|
+
if (permits(deps.authorization, "flows", "write")) {
|
|
453
|
+
server.registerTool("flow_update", {
|
|
454
|
+
title: "Rename or move flow",
|
|
455
|
+
description: "Rename a flow, change its description, or move it to another folder of the shared Knowledge tree. Never changes what the flow does.",
|
|
456
|
+
inputSchema: UpdateFlowInput,
|
|
457
|
+
annotations: {
|
|
458
|
+
title: "Rename or move flow",
|
|
459
|
+
readOnlyHint: false,
|
|
460
|
+
destructiveHint: false,
|
|
461
|
+
idempotentHint: true,
|
|
462
|
+
openWorldHint: false,
|
|
463
|
+
},
|
|
464
|
+
}, async (input) => text(await deps.flows.update(flowActor, input)));
|
|
386
465
|
server.registerTool("flow_save", {
|
|
387
466
|
title: "Save flow",
|
|
388
467
|
description: "Append a validated immutable flow graph version with optimistic concurrency.",
|
|
@@ -396,7 +475,22 @@ export async function handleMcp(request, deps) {
|
|
|
396
475
|
},
|
|
397
476
|
}, async (input) => text(await deps.flows.save(flowActor, input)));
|
|
398
477
|
}
|
|
399
|
-
if (deps.authorization
|
|
478
|
+
if (permits(deps.authorization, "flows", "publish")) {
|
|
479
|
+
// The same answer the screen shows the author before publishing. Every user-visible capability
|
|
480
|
+
// has its MCP equivalent, and a freeze nobody could inspect from here would be one behavior
|
|
481
|
+
// through two doors.
|
|
482
|
+
server.registerTool("flow_publish_preview", {
|
|
483
|
+
title: "Preview flow publication",
|
|
484
|
+
description: "Show which version each sub-flow call will take and which of them publishing freezes.",
|
|
485
|
+
inputSchema: PreviewFlowPublishInput,
|
|
486
|
+
annotations: {
|
|
487
|
+
title: "Preview flow publication",
|
|
488
|
+
readOnlyHint: true,
|
|
489
|
+
destructiveHint: false,
|
|
490
|
+
idempotentHint: true,
|
|
491
|
+
openWorldHint: false,
|
|
492
|
+
},
|
|
493
|
+
}, async (input) => text(await deps.flows.previewPublish(flowActor, input)));
|
|
400
494
|
server.registerTool("flow_publish", {
|
|
401
495
|
title: "Publish flow",
|
|
402
496
|
description: "Publish a version after resolving its authorized Knowledge and tool references.",
|
|
@@ -410,7 +504,7 @@ export async function handleMcp(request, deps) {
|
|
|
410
504
|
},
|
|
411
505
|
}, async (input) => text(await deps.flows.publish(flowActor, input)));
|
|
412
506
|
}
|
|
413
|
-
if (deps.authorization
|
|
507
|
+
if (permits(deps.authorization, "flows", "run")) {
|
|
414
508
|
server.registerTool("flow_run_start", {
|
|
415
509
|
title: "Start flow run",
|
|
416
510
|
description: "Start a durable run of the published graph and return its first actionable node.",
|
|
@@ -436,7 +530,7 @@ export async function handleMcp(request, deps) {
|
|
|
436
530
|
},
|
|
437
531
|
}, async (input) => text(await deps.flows.completeStep(flowActor, input)));
|
|
438
532
|
}
|
|
439
|
-
if (deps.authorization
|
|
533
|
+
if (permits(deps.authorization, "tools", "read")) {
|
|
440
534
|
server.registerTool("tools_catalog_list", {
|
|
441
535
|
title: "List tools",
|
|
442
536
|
description: "List the MCP tools the calling user can reach through the portal, live rather than cached.",
|
|
@@ -450,7 +544,7 @@ export async function handleMcp(request, deps) {
|
|
|
450
544
|
},
|
|
451
545
|
}, async () => text(await deps.tools.catalog(toolActor)));
|
|
452
546
|
}
|
|
453
|
-
if (deps.authorization
|
|
547
|
+
if (permits(deps.authorization, "tools", "test")) {
|
|
454
548
|
server.registerTool("tools_test", {
|
|
455
549
|
title: "Test tool",
|
|
456
550
|
description: "Validate input and execute one explicitly read-only cached MCP tool with the caller's Gate connection.",
|
|
@@ -464,7 +558,7 @@ export async function handleMcp(request, deps) {
|
|
|
464
558
|
},
|
|
465
559
|
}, async (input) => text(await deps.tools.test(toolActor, input)));
|
|
466
560
|
}
|
|
467
|
-
if (deps.authorization
|
|
561
|
+
if (permits(deps.authorization, "tools", "execute")) {
|
|
468
562
|
server.registerTool("tools_execute", {
|
|
469
563
|
title: "Execute tool",
|
|
470
564
|
description: "Validate and execute a discovered MCP tool with the caller's just-in-time Gate connection.",
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export declare function encodeCsvRow(cells: readonly string[]): string;
|
|
2
|
+
export declare function encodeCsv(rows: readonly (readonly string[])[]): string;
|
|
3
|
+
/**
|
|
4
|
+
* Every row of a CSV body, quoted fields included.
|
|
5
|
+
*
|
|
6
|
+
* ⚠️ A quoted field may contain a line break, so this cannot split on `\n` first. A row is closed
|
|
7
|
+
* by a line break that falls outside quotes and by nothing else — the reason a naive `split` reads
|
|
8
|
+
* a table with one multi-line cell as two broken rows.
|
|
9
|
+
*
|
|
10
|
+
* A trailing newline does not produce an empty final row; an empty line in the middle does produce
|
|
11
|
+
* a row with one empty cell, because that is what the file says.
|
|
12
|
+
*/
|
|
13
|
+
export declare function parseCsv(text: string): string[][];
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
// The one CSV reader and writer in Intel. A table's canonical body is CSV (#40), and every surface
|
|
2
|
+
// that has to look inside one — the grid, the column check, the search index — goes through here so
|
|
3
|
+
// none of them can grow a private opinion about quoting.
|
|
4
|
+
//
|
|
5
|
+
// RFC 4180 with two deliberate narrowings: rows are separated by `\n` when written (a reader
|
|
6
|
+
// accepts `\r\n` too), and a field is quoted only when it has to be. Nothing else is escaped,
|
|
7
|
+
// because CSV has no escapes — a quote inside a quoted field is written twice.
|
|
8
|
+
function needsQuoting(cell) {
|
|
9
|
+
return (cell.includes(",") ||
|
|
10
|
+
cell.includes('"') ||
|
|
11
|
+
cell.includes("\n") ||
|
|
12
|
+
cell.includes("\r") ||
|
|
13
|
+
cell !== cell.trim());
|
|
14
|
+
}
|
|
15
|
+
export function encodeCsvRow(cells) {
|
|
16
|
+
return cells
|
|
17
|
+
.map((cell) => (needsQuoting(cell) ? `"${cell.replaceAll('"', '""')}"` : cell))
|
|
18
|
+
.join(",");
|
|
19
|
+
}
|
|
20
|
+
export function encodeCsv(rows) {
|
|
21
|
+
return rows.length === 0 ? "" : `${rows.map(encodeCsvRow).join("\n")}\n`;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Every row of a CSV body, quoted fields included.
|
|
25
|
+
*
|
|
26
|
+
* ⚠️ A quoted field may contain a line break, so this cannot split on `\n` first. A row is closed
|
|
27
|
+
* by a line break that falls outside quotes and by nothing else — the reason a naive `split` reads
|
|
28
|
+
* a table with one multi-line cell as two broken rows.
|
|
29
|
+
*
|
|
30
|
+
* A trailing newline does not produce an empty final row; an empty line in the middle does produce
|
|
31
|
+
* a row with one empty cell, because that is what the file says.
|
|
32
|
+
*/
|
|
33
|
+
export function parseCsv(text) {
|
|
34
|
+
const rows = [];
|
|
35
|
+
let row = [];
|
|
36
|
+
let cell = "";
|
|
37
|
+
let quoted = false;
|
|
38
|
+
let started = false;
|
|
39
|
+
for (let index = 0; index < text.length; index += 1) {
|
|
40
|
+
const character = text[index];
|
|
41
|
+
if (quoted) {
|
|
42
|
+
if (character === '"') {
|
|
43
|
+
if (text[index + 1] === '"') {
|
|
44
|
+
cell += '"';
|
|
45
|
+
index += 1;
|
|
46
|
+
}
|
|
47
|
+
else {
|
|
48
|
+
quoted = false;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
else {
|
|
52
|
+
cell += character;
|
|
53
|
+
}
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
if (character === '"' && cell === "") {
|
|
57
|
+
quoted = true;
|
|
58
|
+
started = true;
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
if (character === ",") {
|
|
62
|
+
row.push(cell);
|
|
63
|
+
cell = "";
|
|
64
|
+
started = true;
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
if (character === "\n" || character === "\r") {
|
|
68
|
+
if (character === "\r" && text[index + 1] === "\n")
|
|
69
|
+
index += 1;
|
|
70
|
+
row.push(cell);
|
|
71
|
+
rows.push(row);
|
|
72
|
+
row = [];
|
|
73
|
+
cell = "";
|
|
74
|
+
started = false;
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
cell += character;
|
|
78
|
+
started = true;
|
|
79
|
+
}
|
|
80
|
+
if (started || cell !== "" || row.length > 0) {
|
|
81
|
+
row.push(cell);
|
|
82
|
+
rows.push(row);
|
|
83
|
+
}
|
|
84
|
+
return rows;
|
|
85
|
+
}
|