@opengeni/api-router 2.13.0-canary.0 → 2.13.0-canary.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/app.js +1 -1
- package/dist/{chunk-BJWHVMOL.js → chunk-K2NU4KRK.js} +3165 -3662
- package/dist/{chunk-BJWHVMOL.js.map → chunk-K2NU4KRK.js.map} +1 -1
- package/dist/index.js +5 -4
- package/dist/index.js.map +1 -1
- package/dist/integrations/github-lens-connect.d.ts +2 -2
- package/dist/integrations/slack-interactions.d.ts +1 -1
- package/dist/mcp/documents.d.ts +5 -13
- package/dist/mcp/files.d.ts +2 -2
- package/dist/mcp/first-party-tool-permissions.d.ts +36 -0
- package/dist/mcp/knowledge-entries.d.ts +5 -0
- package/dist/mcp/server.d.ts +4 -4
- package/dist/routes/host-mcp-resolvers.d.ts +3 -0
- package/dist/routes/knowledge.d.ts +3 -0
- package/dist/sandbox-file-artifacts.d.ts +6 -2
- package/dist/workspace-state-projection.d.ts +3 -5
- package/dist/workspace-tool-gateway.d.ts +2 -1
- package/package.json +19 -19
- package/src/access-grant-rls.ts +13 -1
- package/src/app.ts +4 -11
- package/src/codemode.ts +4 -8
- package/src/index.ts +4 -3
- package/src/integrations/atlassian.ts +31 -28
- package/src/integrations/google-drive.ts +33 -30
- package/src/integrations/marketplace-plugin.ts +1 -1
- package/src/integrations/provider-oauth.ts +11 -2
- package/src/mcp/documents.ts +57 -428
- package/src/mcp/files.ts +76 -58
- package/src/mcp/first-party-tool-permissions.ts +9 -0
- package/src/mcp/knowledge-entries.ts +276 -0
- package/src/mcp/server.ts +7 -361
- package/src/routes/company-brain.ts +58 -107
- package/src/routes/documents.ts +78 -294
- package/src/routes/files.ts +68 -3
- package/src/routes/host-mcp-resolvers.ts +41 -0
- package/src/routes/knowledge.ts +386 -0
- package/src/routes/organization-memberships.ts +33 -0
- package/src/routes/scheduled-tasks.ts +9 -1
- package/src/routes/sessions.ts +20 -3
- package/src/routes/skill-content.ts +8 -2
- package/src/routes/video-generation.ts +7 -5
- package/src/routes/workspace-learning.ts +12 -82
- package/src/routes/workspace-state.ts +34 -25
- package/src/routes/workspaces.ts +20 -10
- package/src/sandbox-file-artifacts.ts +54 -2
- package/src/scheduled-task-deletion.ts +3 -2
- package/src/workspace-state-projection.ts +18 -148
- package/src/workspace-tool-gateway.ts +14 -25
- package/dist/mcp/company-brain-governed-writes.d.ts +0 -26
- package/dist/mcp/remember.d.ts +0 -28
- package/src/mcp/company-brain-governed-writes.ts +0 -271
- package/src/mcp/remember.ts +0 -191
package/src/mcp/documents.ts
CHANGED
|
@@ -1,485 +1,114 @@
|
|
|
1
|
+
import { searchKnowledgeEntries } from "@opengeni/core";
|
|
2
|
+
import { KnowledgeEntryListRequest } from "@opengeni/contracts";
|
|
3
|
+
import type { DocumentServices } from "@opengeni/documents";
|
|
1
4
|
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
KnowledgeRecordId,
|
|
6
|
-
KnowledgeSearchResponse,
|
|
7
|
-
ListIndexedDocumentsResponse,
|
|
8
|
-
} from "@opengeni/contracts";
|
|
9
|
-
import {
|
|
10
|
-
browseEffectiveKnowledge,
|
|
11
|
-
getEffectiveKnowledgeRecord,
|
|
12
|
-
getDocumentChunk,
|
|
13
|
-
listDocumentBases,
|
|
14
|
-
listEffectiveIndexedDocuments,
|
|
15
|
-
resolveEffectiveDocumentAccess,
|
|
16
|
-
searchEffectiveKnowledge,
|
|
17
|
-
searchEffectiveDocuments,
|
|
18
|
-
type DocumentAccessFilter,
|
|
19
|
-
type DocumentServices,
|
|
20
|
-
} from "@opengeni/documents";
|
|
21
|
-
import {
|
|
22
|
-
createKnowledgeMemory,
|
|
23
|
-
listKnowledgeMemories,
|
|
5
|
+
getKnowledgeEntry,
|
|
6
|
+
listKnowledgeEntries,
|
|
7
|
+
type KnowledgeContext,
|
|
24
8
|
type Database,
|
|
25
|
-
type MemoryAgentScope,
|
|
26
9
|
} from "@opengeni/db";
|
|
27
10
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
28
11
|
import * as z from "zod/v4";
|
|
29
|
-
import { mcpMutationReceipt } from "./receipts";
|
|
30
|
-
|
|
31
|
-
const SearchInputSchema = {
|
|
32
|
-
query: z.string().min(1),
|
|
33
|
-
baseIds: z.array(z.string().uuid()).optional(),
|
|
34
|
-
limit: z.number().int().positive().max(50).optional(),
|
|
35
|
-
mode: z.enum(["hybrid", "vector", "keyword"]).optional(),
|
|
36
|
-
sourceKinds: z
|
|
37
|
-
.array(
|
|
38
|
-
z.enum([
|
|
39
|
-
"manual_upload",
|
|
40
|
-
"meeting_transcript",
|
|
41
|
-
"repository",
|
|
42
|
-
"email",
|
|
43
|
-
"chat",
|
|
44
|
-
"document",
|
|
45
|
-
"web",
|
|
46
|
-
"other",
|
|
47
|
-
]),
|
|
48
|
-
)
|
|
49
|
-
.optional(),
|
|
50
|
-
aclTags: z.array(z.string().min(1)).optional(),
|
|
51
|
-
};
|
|
52
|
-
|
|
53
|
-
const MemoryKindSchema = z.enum(["semantic", "episodic", "procedural", "decision", "preference"]);
|
|
54
|
-
const SourceRefSchema = z.object({
|
|
55
|
-
kind: z.enum(["document_chunk", "document", "session_event", "memory", "external"]),
|
|
56
|
-
id: z.string().min(1),
|
|
57
|
-
uri: z.string().min(1).optional(),
|
|
58
|
-
title: z.string().min(1).optional(),
|
|
59
|
-
metadata: z.record(z.string(), z.unknown()).optional(),
|
|
60
|
-
});
|
|
61
12
|
|
|
13
|
+
/** The docs endpoint is a transport for canonical Knowledge retrieval. Legacy
|
|
14
|
+
* chunk/base retrieval is retired: it cannot bypass pending Knowledge review. */
|
|
62
15
|
export function buildDocumentsMcpServer(
|
|
63
16
|
db: Database,
|
|
64
17
|
accountId: string,
|
|
65
18
|
workspaceId: string,
|
|
66
19
|
documentServices: DocumentServices,
|
|
67
|
-
options: {
|
|
68
|
-
createdBySessionId?: string | undefined;
|
|
69
|
-
attemptId?: string | undefined;
|
|
70
|
-
/** Immutable human subject whose agent is making this retrieval request. */
|
|
71
|
-
initiatingSubjectId: string;
|
|
72
|
-
/**
|
|
73
|
-
* The bound session's typed Memory selector (migration 0427), resolved by
|
|
74
|
-
* the caller from the session row. `off` registers neither memory tool;
|
|
75
|
-
* `user`/`session` search the workspace layer plus the own private layer.
|
|
76
|
-
* Omitted keeps the workspace layer.
|
|
77
|
-
*/
|
|
78
|
-
memory?: MemoryAgentScope | null | undefined;
|
|
79
|
-
},
|
|
20
|
+
options: { knowledge: KnowledgeContext },
|
|
80
21
|
): McpServer {
|
|
81
|
-
const
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
});
|
|
85
|
-
// This server is the agent retrieval surface. Agent-disabled documents are
|
|
86
|
-
// never reachable. Workspace-visible documents are shared; private
|
|
87
|
-
// documents are available only to the creating subject's agent.
|
|
88
|
-
const agentAuthority =
|
|
89
|
-
options.createdBySessionId && options.attemptId
|
|
90
|
-
? { sessionId: options.createdBySessionId, attemptId: options.attemptId }
|
|
91
|
-
: undefined;
|
|
92
|
-
const resolveAgentAccess = async (): Promise<DocumentAccessFilter> =>
|
|
93
|
-
await resolveEffectiveDocumentAccess(db, {
|
|
94
|
-
accountId,
|
|
95
|
-
workspaceId,
|
|
96
|
-
initiatingSubjectId: options.initiatingSubjectId,
|
|
97
|
-
surface: "agent",
|
|
98
|
-
agentAuthority,
|
|
99
|
-
});
|
|
100
|
-
|
|
101
|
-
server.registerTool(
|
|
102
|
-
"list_document_bases",
|
|
103
|
-
{
|
|
104
|
-
description: "List document bases available for retrieval.",
|
|
105
|
-
inputSchema: {},
|
|
106
|
-
},
|
|
107
|
-
async () => ({
|
|
108
|
-
content: [{ type: "text", text: JSON.stringify(await listDocumentBases(db, workspaceId)) }],
|
|
109
|
-
}),
|
|
110
|
-
);
|
|
111
|
-
|
|
112
|
-
server.registerTool(
|
|
113
|
-
"search_documents",
|
|
114
|
-
{
|
|
115
|
-
description: "Search indexed documents with hybrid, vector, or keyword retrieval.",
|
|
116
|
-
inputSchema: SearchInputSchema,
|
|
117
|
-
},
|
|
118
|
-
async (input) =>
|
|
119
|
-
searchContent(
|
|
120
|
-
db,
|
|
121
|
-
accountId,
|
|
122
|
-
workspaceId,
|
|
123
|
-
documentServices,
|
|
124
|
-
input,
|
|
125
|
-
options.initiatingSubjectId,
|
|
126
|
-
agentAuthority,
|
|
127
|
-
),
|
|
128
|
-
);
|
|
129
|
-
|
|
22
|
+
const context = options.knowledge;
|
|
23
|
+
if (context.accountId !== accountId || context.workspaceId !== workspaceId)
|
|
24
|
+
throw new Error("Knowledge gateway authority mismatch");
|
|
25
|
+
const server = new McpServer({ name: "opengeni-knowledge", version: "2.0.0" });
|
|
130
26
|
server.registerTool(
|
|
131
27
|
"knowledge_search",
|
|
132
28
|
{
|
|
133
29
|
description:
|
|
134
|
-
"Search the
|
|
135
|
-
inputSchema:
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
documentServices,
|
|
143
|
-
input,
|
|
144
|
-
options.initiatingSubjectId,
|
|
145
|
-
agentAuthority,
|
|
146
|
-
),
|
|
147
|
-
);
|
|
148
|
-
|
|
149
|
-
server.registerTool(
|
|
150
|
-
"knowledge_get",
|
|
151
|
-
{
|
|
152
|
-
description:
|
|
153
|
-
"Fetch one stable Knowledge record by the id returned from knowledge_search or knowledge_browse. Authorization and agent access are rechecked before content or links are returned.",
|
|
154
|
-
inputSchema: { id: KnowledgeRecordId },
|
|
155
|
-
},
|
|
156
|
-
async ({ id }) => {
|
|
157
|
-
const record = await getEffectiveKnowledgeRecord(db, {
|
|
158
|
-
accountId,
|
|
159
|
-
workspaceId,
|
|
160
|
-
initiatingSubjectId: options.initiatingSubjectId,
|
|
161
|
-
agentAuthority,
|
|
162
|
-
id,
|
|
163
|
-
});
|
|
164
|
-
return {
|
|
165
|
-
content: [
|
|
166
|
-
{
|
|
167
|
-
type: "text",
|
|
168
|
-
text: record
|
|
169
|
-
? JSON.stringify(KnowledgeGetResponse.parse({ record }))
|
|
170
|
-
: `knowledge record not found: ${id}`,
|
|
171
|
-
},
|
|
172
|
-
],
|
|
173
|
-
isError: !record,
|
|
174
|
-
};
|
|
175
|
-
},
|
|
176
|
-
);
|
|
177
|
-
|
|
178
|
-
server.registerTool(
|
|
179
|
-
"knowledge_browse",
|
|
180
|
-
{
|
|
181
|
-
description:
|
|
182
|
-
"Browse authorized Knowledge without crawling folders. Omit parentId to list source documents; pass a document record id to inspect its chunks. Opaque cursors are bound to this user, workspace, parent, and filters.",
|
|
183
|
-
inputSchema: {
|
|
184
|
-
parentId: KnowledgeRecordId.optional(),
|
|
185
|
-
topic: z.string().min(1).max(256).optional(),
|
|
186
|
-
sourceKinds: SearchInputSchema.sourceKinds,
|
|
187
|
-
cursor: z.string().min(1).max(KNOWLEDGE_BROWSE_CURSOR_MAX_CHARS).optional(),
|
|
188
|
-
limit: z.number().int().positive().max(50).optional(),
|
|
189
|
-
},
|
|
190
|
-
},
|
|
191
|
-
async ({ parentId, topic, sourceKinds, cursor, limit }) => ({
|
|
30
|
+
"Search published structured Knowledge and retained source text in the current authorized scope. Use a concise subject or entity name first (for example Acme); omit scope to search all authorized scopes. If a query returns no entries, retry the key name alone in the same scope or browse groups before concluding the information is absent.",
|
|
31
|
+
inputSchema: KnowledgeEntryListRequest.omit({
|
|
32
|
+
view: true,
|
|
33
|
+
sessionId: true,
|
|
34
|
+
reviewBatchId: true,
|
|
35
|
+
}).shape,
|
|
36
|
+
},
|
|
37
|
+
async (input) => ({
|
|
192
38
|
content: [
|
|
193
39
|
{
|
|
194
40
|
type: "text",
|
|
195
41
|
text: JSON.stringify(
|
|
196
|
-
|
|
197
|
-
await browseEffectiveKnowledge(db, {
|
|
198
|
-
accountId,
|
|
199
|
-
workspaceId,
|
|
200
|
-
initiatingSubjectId: options.initiatingSubjectId,
|
|
201
|
-
agentAuthority,
|
|
202
|
-
...(parentId ? { parentId } : {}),
|
|
203
|
-
...(topic ? { topic } : {}),
|
|
204
|
-
...(sourceKinds ? { sourceKinds } : {}),
|
|
205
|
-
...(cursor ? { cursor } : {}),
|
|
206
|
-
...(limit ? { limit } : {}),
|
|
207
|
-
}),
|
|
208
|
-
),
|
|
42
|
+
await searchKnowledgeEntries(db, context, input, () => documentServices.embedder),
|
|
209
43
|
),
|
|
210
44
|
},
|
|
211
45
|
],
|
|
212
46
|
}),
|
|
213
47
|
);
|
|
214
|
-
|
|
215
48
|
server.registerTool(
|
|
216
|
-
"
|
|
217
|
-
{
|
|
218
|
-
description:
|
|
219
|
-
"List agent-authorized documents that became ready after an opaque checkpoint, ordered by durable indexing completion. Results include source and authority/ingestion provenance. Persist nextCheckpoint only after successfully processing the returned page, then pass it on the next scheduled run to avoid repeats.",
|
|
220
|
-
inputSchema: {
|
|
221
|
-
checkpoint: z.string().min(1).max(1_024).optional(),
|
|
222
|
-
limit: z.number().int().positive().max(100).optional(),
|
|
223
|
-
},
|
|
224
|
-
},
|
|
225
|
-
async ({ checkpoint, limit }) => ({
|
|
226
|
-
content: [
|
|
227
|
-
{
|
|
228
|
-
type: "text",
|
|
229
|
-
text: JSON.stringify(
|
|
230
|
-
ListIndexedDocumentsResponse.parse(
|
|
231
|
-
await listEffectiveIndexedDocuments(db, {
|
|
232
|
-
accountId,
|
|
233
|
-
workspaceId,
|
|
234
|
-
initiatingSubjectId: options.initiatingSubjectId,
|
|
235
|
-
agentAuthority,
|
|
236
|
-
...(checkpoint ? { checkpoint } : {}),
|
|
237
|
-
...(limit ? { limit } : {}),
|
|
238
|
-
}),
|
|
239
|
-
),
|
|
240
|
-
),
|
|
241
|
-
},
|
|
242
|
-
],
|
|
243
|
-
}),
|
|
244
|
-
);
|
|
245
|
-
|
|
246
|
-
server.registerTool(
|
|
247
|
-
"fetch_document_chunk",
|
|
248
|
-
{
|
|
249
|
-
description: "Fetch one indexed document chunk by id.",
|
|
250
|
-
inputSchema: {
|
|
251
|
-
chunkId: z.string().uuid(),
|
|
252
|
-
},
|
|
253
|
-
},
|
|
254
|
-
async ({ chunkId }) => {
|
|
255
|
-
const found = await getDocumentChunk(
|
|
256
|
-
db,
|
|
257
|
-
accountId,
|
|
258
|
-
workspaceId,
|
|
259
|
-
chunkId,
|
|
260
|
-
await resolveAgentAccess(),
|
|
261
|
-
);
|
|
262
|
-
return {
|
|
263
|
-
content: [
|
|
264
|
-
{ type: "text", text: found ? JSON.stringify(found) : `chunk not found: ${chunkId}` },
|
|
265
|
-
],
|
|
266
|
-
isError: !found,
|
|
267
|
-
};
|
|
268
|
-
},
|
|
269
|
-
);
|
|
270
|
-
|
|
271
|
-
server.registerTool(
|
|
272
|
-
"knowledge_fetch",
|
|
273
|
-
{
|
|
274
|
-
description: "Fetch one knowledge source chunk by id.",
|
|
275
|
-
inputSchema: {
|
|
276
|
-
chunkId: z.string().uuid(),
|
|
277
|
-
},
|
|
278
|
-
},
|
|
279
|
-
async ({ chunkId }) => {
|
|
280
|
-
const found = await getDocumentChunk(
|
|
281
|
-
db,
|
|
282
|
-
accountId,
|
|
283
|
-
workspaceId,
|
|
284
|
-
chunkId,
|
|
285
|
-
await resolveAgentAccess(),
|
|
286
|
-
);
|
|
287
|
-
return {
|
|
288
|
-
content: [
|
|
289
|
-
{ type: "text", text: found ? JSON.stringify(found) : `chunk not found: ${chunkId}` },
|
|
290
|
-
],
|
|
291
|
-
isError: !found,
|
|
292
|
-
};
|
|
293
|
-
},
|
|
294
|
-
);
|
|
295
|
-
|
|
296
|
-
const memoryScope = options.memory ?? null;
|
|
297
|
-
if (memoryScope?.mode === "off") return server;
|
|
298
|
-
const memoryReadScope: MemoryAgentScope = memoryScope ?? {
|
|
299
|
-
mode: "workspace",
|
|
300
|
-
userSubjectId: null,
|
|
301
|
-
rootSessionId: null,
|
|
302
|
-
};
|
|
303
|
-
|
|
304
|
-
server.registerTool(
|
|
305
|
-
"memory_search",
|
|
49
|
+
"knowledge_browse",
|
|
306
50
|
{
|
|
307
|
-
description: "
|
|
51
|
+
description: "Browse Knowledge groups or entries in one group, across sources.",
|
|
308
52
|
inputSchema: {
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
limit: z.number().int().positive().max(100).optional(),
|
|
53
|
+
groupId: z.uuid().optional(),
|
|
54
|
+
cursor: z.string().optional(),
|
|
55
|
+
limit: z.number().int().positive().max(50).optional(),
|
|
313
56
|
},
|
|
314
57
|
},
|
|
315
|
-
async (
|
|
58
|
+
async (input) => ({
|
|
316
59
|
content: [
|
|
317
60
|
{
|
|
318
61
|
type: "text",
|
|
319
62
|
text: JSON.stringify(
|
|
320
|
-
await
|
|
321
|
-
...
|
|
322
|
-
|
|
323
|
-
...(kind ? { kind } : {}),
|
|
324
|
-
...(scope ? { scope } : {}),
|
|
325
|
-
...(limit ? { limit } : {}),
|
|
326
|
-
agentScope: memoryReadScope,
|
|
63
|
+
await listKnowledgeEntries(db, context, {
|
|
64
|
+
...input,
|
|
65
|
+
...(input.groupId ? {} : { kind: "group" }),
|
|
327
66
|
}),
|
|
328
67
|
),
|
|
329
68
|
},
|
|
330
69
|
],
|
|
331
70
|
}),
|
|
332
71
|
);
|
|
333
|
-
|
|
334
72
|
server.registerTool(
|
|
335
|
-
"
|
|
73
|
+
"knowledge_get",
|
|
336
74
|
{
|
|
337
|
-
description:
|
|
75
|
+
description:
|
|
76
|
+
"Read an exact published Knowledge revision and its evidence. Long source text is paginated.",
|
|
338
77
|
inputSchema: {
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
confidence: z.number().min(0).max(1).optional(),
|
|
344
|
-
metadata: z.record(z.string(), z.unknown()).optional(),
|
|
78
|
+
entryId: z.uuid(),
|
|
79
|
+
revisionId: z.uuid().optional(),
|
|
80
|
+
offset: z.number().int().nonnegative().default(0),
|
|
81
|
+
maxChars: z.number().int().positive().max(16000).default(8000),
|
|
345
82
|
},
|
|
346
83
|
},
|
|
347
|
-
async (
|
|
348
|
-
const
|
|
349
|
-
|
|
350
|
-
workspaceId,
|
|
351
|
-
status: "proposed",
|
|
352
|
-
kind: kind ?? "semantic",
|
|
353
|
-
scope: scope ?? "workspace",
|
|
354
|
-
text,
|
|
355
|
-
sourceRefs:
|
|
356
|
-
sourceRefs?.map((sourceRef) => ({
|
|
357
|
-
...sourceRef,
|
|
358
|
-
metadata: sourceRef.metadata ?? {},
|
|
359
|
-
})) ?? [],
|
|
360
|
-
confidence: confidence ?? 0.5,
|
|
361
|
-
metadata: metadata ?? {},
|
|
362
|
-
createdBySessionId: options.createdBySessionId,
|
|
84
|
+
async (input) => {
|
|
85
|
+
const record = await getKnowledgeEntry(db, context, input.entryId, {
|
|
86
|
+
revisionId: input.revisionId,
|
|
363
87
|
});
|
|
88
|
+
if (!record) return { content: [{ type: "text", text: JSON.stringify({ found: false }) }] };
|
|
89
|
+
const text = record.revision.entry.content;
|
|
90
|
+
const end = Math.min(text.length, input.offset + input.maxChars);
|
|
364
91
|
return {
|
|
365
92
|
content: [
|
|
366
93
|
{
|
|
367
94
|
type: "text",
|
|
368
|
-
text: JSON.stringify(
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
nextAction: { tool: "memory_search", arguments: {} },
|
|
382
|
-
}),
|
|
383
|
-
),
|
|
95
|
+
text: JSON.stringify({
|
|
96
|
+
...record,
|
|
97
|
+
revision: {
|
|
98
|
+
...record.revision,
|
|
99
|
+
entry: { ...record.revision.entry, content: text.slice(input.offset, end) },
|
|
100
|
+
},
|
|
101
|
+
contentRange: {
|
|
102
|
+
start: input.offset,
|
|
103
|
+
end,
|
|
104
|
+
total: text.length,
|
|
105
|
+
nextOffset: end < text.length ? end : null,
|
|
106
|
+
},
|
|
107
|
+
}),
|
|
384
108
|
},
|
|
385
109
|
],
|
|
386
110
|
};
|
|
387
111
|
},
|
|
388
112
|
);
|
|
389
|
-
|
|
390
113
|
return server;
|
|
391
114
|
}
|
|
392
|
-
|
|
393
|
-
async function searchContent(
|
|
394
|
-
db: Database,
|
|
395
|
-
accountId: string,
|
|
396
|
-
workspaceId: string,
|
|
397
|
-
documentServices: DocumentServices,
|
|
398
|
-
input: {
|
|
399
|
-
query: string;
|
|
400
|
-
baseIds?: string[] | undefined;
|
|
401
|
-
limit?: number | undefined;
|
|
402
|
-
mode?: "hybrid" | "vector" | "keyword" | undefined;
|
|
403
|
-
sourceKinds?:
|
|
404
|
-
| Array<
|
|
405
|
-
| "manual_upload"
|
|
406
|
-
| "meeting_transcript"
|
|
407
|
-
| "repository"
|
|
408
|
-
| "email"
|
|
409
|
-
| "chat"
|
|
410
|
-
| "document"
|
|
411
|
-
| "web"
|
|
412
|
-
| "other"
|
|
413
|
-
>
|
|
414
|
-
| undefined;
|
|
415
|
-
aclTags?: string[] | undefined;
|
|
416
|
-
},
|
|
417
|
-
initiatingSubjectId: string,
|
|
418
|
-
agentAuthority: { sessionId: string; attemptId: string } | undefined,
|
|
419
|
-
) {
|
|
420
|
-
const results = await searchEffectiveDocuments(
|
|
421
|
-
db,
|
|
422
|
-
{
|
|
423
|
-
accountId,
|
|
424
|
-
workspaceId,
|
|
425
|
-
query: input.query,
|
|
426
|
-
...(input.baseIds ? { baseIds: input.baseIds } : {}),
|
|
427
|
-
...(input.limit ? { limit: input.limit } : {}),
|
|
428
|
-
...(input.mode ? { mode: input.mode } : {}),
|
|
429
|
-
...(input.sourceKinds ? { sourceKinds: input.sourceKinds } : {}),
|
|
430
|
-
...(input.aclTags ? { aclTags: input.aclTags } : {}),
|
|
431
|
-
initiatingSubjectId,
|
|
432
|
-
agentAuthority,
|
|
433
|
-
surface: "agent",
|
|
434
|
-
},
|
|
435
|
-
documentServices,
|
|
436
|
-
);
|
|
437
|
-
return {
|
|
438
|
-
content: [
|
|
439
|
-
{
|
|
440
|
-
type: "text" as const,
|
|
441
|
-
text: JSON.stringify(results),
|
|
442
|
-
},
|
|
443
|
-
],
|
|
444
|
-
};
|
|
445
|
-
}
|
|
446
|
-
|
|
447
|
-
async function searchKnowledge(
|
|
448
|
-
db: Database,
|
|
449
|
-
accountId: string,
|
|
450
|
-
workspaceId: string,
|
|
451
|
-
documentServices: DocumentServices,
|
|
452
|
-
input: {
|
|
453
|
-
query: string;
|
|
454
|
-
baseIds?: string[] | undefined;
|
|
455
|
-
limit?: number | undefined;
|
|
456
|
-
mode?: "hybrid" | "vector" | "keyword" | undefined;
|
|
457
|
-
sourceKinds?: Parameters<typeof searchContent>[4]["sourceKinds"];
|
|
458
|
-
aclTags?: string[] | undefined;
|
|
459
|
-
},
|
|
460
|
-
initiatingSubjectId: string,
|
|
461
|
-
agentAuthority: { sessionId: string; attemptId: string } | undefined,
|
|
462
|
-
) {
|
|
463
|
-
const response = await searchEffectiveKnowledge(
|
|
464
|
-
db,
|
|
465
|
-
{
|
|
466
|
-
accountId,
|
|
467
|
-
workspaceId,
|
|
468
|
-
query: input.query,
|
|
469
|
-
...(input.baseIds ? { baseIds: input.baseIds } : {}),
|
|
470
|
-
...(input.limit ? { limit: input.limit } : {}),
|
|
471
|
-
...(input.mode ? { mode: input.mode } : {}),
|
|
472
|
-
...(input.sourceKinds ? { sourceKinds: input.sourceKinds } : {}),
|
|
473
|
-
...(input.aclTags ? { aclTags: input.aclTags } : {}),
|
|
474
|
-
initiatingSubjectId,
|
|
475
|
-
agentAuthority,
|
|
476
|
-
surface: "agent",
|
|
477
|
-
},
|
|
478
|
-
documentServices,
|
|
479
|
-
);
|
|
480
|
-
return {
|
|
481
|
-
content: [
|
|
482
|
-
{ type: "text" as const, text: JSON.stringify(KnowledgeSearchResponse.parse(response)) },
|
|
483
|
-
],
|
|
484
|
-
};
|
|
485
|
-
}
|
package/src/mcp/files.ts
CHANGED
|
@@ -1,11 +1,22 @@
|
|
|
1
1
|
import type { AccessGrant } from "@opengeni/contracts";
|
|
2
|
-
import { recordAuditEvent, requireFileForSubject } from "@opengeni/db";
|
|
3
|
-
import {
|
|
2
|
+
import { recordAuditEvent, requireFileForSubject, withSessionRlsActorContext } from "@opengeni/db";
|
|
3
|
+
import {
|
|
4
|
+
hasPermission,
|
|
5
|
+
fileOwnerContextForAccess,
|
|
6
|
+
fileOwnerContextForAgent,
|
|
7
|
+
type AccessGrantAuthorization,
|
|
8
|
+
type ApiRouteDeps,
|
|
9
|
+
} from "@opengeni/core";
|
|
4
10
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
5
11
|
import * as z from "zod/v4";
|
|
6
12
|
|
|
7
13
|
/** Dedicated file MCP surface. The broad `opengeni` server never registers it. */
|
|
8
|
-
export function buildFilesMcpServer(
|
|
14
|
+
export function buildFilesMcpServer(
|
|
15
|
+
deps: ApiRouteDeps,
|
|
16
|
+
authority: AccessGrant | AccessGrantAuthorization,
|
|
17
|
+
): McpServer {
|
|
18
|
+
const access = "grant" in authority ? authority : undefined;
|
|
19
|
+
const grant = access ? access.grant : (authority as AccessGrant);
|
|
9
20
|
const server = new McpServer({
|
|
10
21
|
name: "opengeni-files",
|
|
11
22
|
version: "1.0.0",
|
|
@@ -34,62 +45,69 @@ export function buildFilesMcpServer(deps: ApiRouteDeps, grant: AccessGrant): Mcp
|
|
|
34
45
|
inputSchema: { fileId: z.string().uuid() },
|
|
35
46
|
},
|
|
36
47
|
async ({ fileId }) => {
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
text: JSON.stringify(
|
|
70
|
-
{
|
|
71
|
-
file: {
|
|
72
|
-
id: file.id,
|
|
73
|
-
filename: file.filename,
|
|
74
|
-
safeFilename: file.safeFilename,
|
|
75
|
-
contentType: file.contentType,
|
|
76
|
-
sizeBytes: file.sizeBytes,
|
|
77
|
-
sha256: file.sha256,
|
|
78
|
-
status: file.status,
|
|
79
|
-
createdAt: file.createdAt,
|
|
80
|
-
updatedAt: file.updatedAt,
|
|
81
|
-
},
|
|
82
|
-
downloadUrl: {
|
|
83
|
-
url: signed.url,
|
|
84
|
-
expiresAt: signed.expiresAt.toISOString(),
|
|
85
|
-
},
|
|
86
|
-
},
|
|
87
|
-
null,
|
|
88
|
-
2,
|
|
89
|
-
),
|
|
48
|
+
const actor = access
|
|
49
|
+
? await fileOwnerContextForAccess(deps, access, "files:read")
|
|
50
|
+
: grant.principalKind === "agent_attempt"
|
|
51
|
+
? await fileOwnerContextForAgent(deps, grant, "files:read")
|
|
52
|
+
: { subjectId: grant.subjectId, privateFileOwnerSubjectId: null };
|
|
53
|
+
return withSessionRlsActorContext(actor, async () => {
|
|
54
|
+
if (!deps.objectStorage) {
|
|
55
|
+
throw new Error("object storage is not configured");
|
|
56
|
+
}
|
|
57
|
+
const file = await requireFileForSubject(deps.db, {
|
|
58
|
+
accountId: grant.accountId,
|
|
59
|
+
workspaceId: grant.workspaceId,
|
|
60
|
+
subjectId: grant.subjectId,
|
|
61
|
+
fileId,
|
|
62
|
+
});
|
|
63
|
+
if (file.status !== "ready") {
|
|
64
|
+
throw new Error(`file is ${file.status}`);
|
|
65
|
+
}
|
|
66
|
+
const signed = await deps.objectStorage.createGetUrl({ key: file.objectKey });
|
|
67
|
+
// Principal-facing signed URL issuance is a metadata-only audit
|
|
68
|
+
// fact, awaited before the URL leaves the platform. Never the URL/key.
|
|
69
|
+
await recordAuditEvent(deps.db, {
|
|
70
|
+
accountId: grant.accountId,
|
|
71
|
+
workspaceId: grant.workspaceId,
|
|
72
|
+
subjectId: grant.subjectId,
|
|
73
|
+
action: "file.signed_url.issued",
|
|
74
|
+
targetType: "workspace_file",
|
|
75
|
+
targetId: file.id,
|
|
76
|
+
metadata: {
|
|
77
|
+
fileId: file.id,
|
|
78
|
+
kind: "mcp_download",
|
|
79
|
+
expiresAt: signed.expiresAt.toISOString(),
|
|
90
80
|
},
|
|
91
|
-
|
|
92
|
-
|
|
81
|
+
});
|
|
82
|
+
return {
|
|
83
|
+
content: [
|
|
84
|
+
{
|
|
85
|
+
type: "text" as const,
|
|
86
|
+
text: JSON.stringify(
|
|
87
|
+
{
|
|
88
|
+
file: {
|
|
89
|
+
id: file.id,
|
|
90
|
+
filename: file.filename,
|
|
91
|
+
safeFilename: file.safeFilename,
|
|
92
|
+
contentType: file.contentType,
|
|
93
|
+
sizeBytes: file.sizeBytes,
|
|
94
|
+
sha256: file.sha256,
|
|
95
|
+
status: file.status,
|
|
96
|
+
createdAt: file.createdAt,
|
|
97
|
+
updatedAt: file.updatedAt,
|
|
98
|
+
},
|
|
99
|
+
downloadUrl: {
|
|
100
|
+
url: signed.url,
|
|
101
|
+
expiresAt: signed.expiresAt.toISOString(),
|
|
102
|
+
},
|
|
103
|
+
},
|
|
104
|
+
null,
|
|
105
|
+
2,
|
|
106
|
+
),
|
|
107
|
+
},
|
|
108
|
+
],
|
|
109
|
+
};
|
|
110
|
+
});
|
|
93
111
|
},
|
|
94
112
|
);
|
|
95
113
|
|