@opengeni/api-router 0.4.1 → 0.5.1

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.
@@ -4,11 +4,39 @@ import {
4
4
  searchDocuments,
5
5
  type DocumentServices,
6
6
  } from "@opengeni/documents";
7
- import type { Database } from "@opengeni/db";
7
+ import {
8
+ createKnowledgeMemory,
9
+ listKnowledgeMemories,
10
+ type Database,
11
+ } from "@opengeni/db";
8
12
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
9
13
  import * as z from "zod/v4";
10
14
 
11
- export function buildDocumentsMcpServer(db: Database, workspaceId: string, documentServices: DocumentServices): McpServer {
15
+ const SearchInputSchema = {
16
+ query: z.string().min(1),
17
+ baseIds: z.array(z.string().uuid()).optional(),
18
+ limit: z.number().int().positive().max(50).optional(),
19
+ mode: z.enum(["hybrid", "vector", "keyword"]).optional(),
20
+ sourceKinds: z.array(z.enum(["manual_upload", "meeting_transcript", "repository", "email", "chat", "document", "web", "other"])).optional(),
21
+ aclTags: z.array(z.string().min(1)).optional(),
22
+ };
23
+
24
+ const MemoryKindSchema = z.enum(["semantic", "episodic", "procedural", "decision", "preference"]);
25
+ const SourceRefSchema = z.object({
26
+ kind: z.enum(["document_chunk", "document", "session_event", "memory", "external"]),
27
+ id: z.string().min(1),
28
+ uri: z.string().min(1).optional(),
29
+ title: z.string().min(1).optional(),
30
+ metadata: z.record(z.string(), z.unknown()).optional(),
31
+ });
32
+
33
+ export function buildDocumentsMcpServer(
34
+ db: Database,
35
+ accountId: string,
36
+ workspaceId: string,
37
+ documentServices: DocumentServices,
38
+ options: { createdBySessionId?: string | undefined } = {},
39
+ ): McpServer {
12
40
  const server = new McpServer({
13
41
  name: "opengeni-documents",
14
42
  version: "1.0.0",
@@ -22,28 +50,19 @@ export function buildDocumentsMcpServer(db: Database, workspaceId: string, docum
22
50
  }));
23
51
 
24
52
  server.registerTool("search_documents", {
25
- description: "Search indexed documents.",
26
- inputSchema: {
27
- query: z.string(),
28
- baseIds: z.array(z.string()).optional(),
29
- limit: z.number().optional(),
30
- },
31
- }, async ({ query, baseIds, limit }) => ({
32
- content: [{
33
- type: "text",
34
- text: JSON.stringify(await searchDocuments(db, {
35
- workspaceId,
36
- query,
37
- ...(baseIds ? { baseIds } : {}),
38
- ...(limit ? { limit } : {}),
39
- }, documentServices)),
40
- }],
41
- }));
53
+ description: "Search indexed documents with hybrid, vector, or keyword retrieval.",
54
+ inputSchema: SearchInputSchema,
55
+ }, async (input) => searchContent(db, workspaceId, documentServices, input));
56
+
57
+ server.registerTool("knowledge_search", {
58
+ description: "Search company knowledge sources with optional base, source-kind, ACL, and retrieval-mode filters.",
59
+ inputSchema: SearchInputSchema,
60
+ }, async (input) => searchContent(db, workspaceId, documentServices, input));
42
61
 
43
62
  server.registerTool("fetch_document_chunk", {
44
63
  description: "Fetch one indexed document chunk by id.",
45
64
  inputSchema: {
46
- chunkId: z.string(),
65
+ chunkId: z.string().uuid(),
47
66
  },
48
67
  }, async ({ chunkId }) => {
49
68
  const found = await getDocumentChunk(db, workspaceId, chunkId);
@@ -53,5 +72,90 @@ export function buildDocumentsMcpServer(db: Database, workspaceId: string, docum
53
72
  };
54
73
  });
55
74
 
75
+ server.registerTool("knowledge_fetch", {
76
+ description: "Fetch one knowledge source chunk by id.",
77
+ inputSchema: {
78
+ chunkId: z.string().uuid(),
79
+ },
80
+ }, async ({ chunkId }) => {
81
+ const found = await getDocumentChunk(db, workspaceId, chunkId);
82
+ return {
83
+ content: [{ type: "text", text: found ? JSON.stringify(found) : `chunk not found: ${chunkId}` }],
84
+ isError: !found,
85
+ };
86
+ });
87
+
88
+ server.registerTool("memory_search", {
89
+ description: "Search approved company memory records.",
90
+ inputSchema: {
91
+ query: z.string().min(1).optional(),
92
+ kind: MemoryKindSchema.optional(),
93
+ scope: z.string().min(1).optional(),
94
+ limit: z.number().int().positive().max(100).optional(),
95
+ },
96
+ }, async ({ query, kind, scope, limit }) => ({
97
+ content: [{ type: "text", text: JSON.stringify(await listKnowledgeMemories(db, workspaceId, {
98
+ ...(query ? { query } : {}),
99
+ status: "approved",
100
+ ...(kind ? { kind } : {}),
101
+ ...(scope ? { scope } : {}),
102
+ ...(limit ? { limit } : {}),
103
+ })) }],
104
+ }));
105
+
106
+ server.registerTool("memory_propose", {
107
+ description: "Propose a company memory record for human review.",
108
+ inputSchema: {
109
+ text: z.string().min(1),
110
+ kind: MemoryKindSchema.optional(),
111
+ scope: z.string().min(1).optional(),
112
+ sourceRefs: z.array(SourceRefSchema).optional(),
113
+ confidence: z.number().min(0).max(1).optional(),
114
+ metadata: z.record(z.string(), z.unknown()).optional(),
115
+ },
116
+ }, async ({ text, kind, scope, sourceRefs, confidence, metadata }) => ({
117
+ content: [{ type: "text", text: JSON.stringify(await createKnowledgeMemory(db, {
118
+ accountId,
119
+ workspaceId,
120
+ status: "proposed",
121
+ kind: kind ?? "semantic",
122
+ scope: scope ?? "workspace",
123
+ text,
124
+ sourceRefs: sourceRefs?.map((sourceRef) => ({ ...sourceRef, metadata: sourceRef.metadata ?? {} })) ?? [],
125
+ confidence: confidence ?? 0.5,
126
+ metadata: metadata ?? {},
127
+ createdBySessionId: options.createdBySessionId,
128
+ })) }],
129
+ }));
130
+
56
131
  return server;
57
132
  }
133
+
134
+ async function searchContent(
135
+ db: Database,
136
+ workspaceId: string,
137
+ documentServices: DocumentServices,
138
+ input: {
139
+ query: string;
140
+ baseIds?: string[] | undefined;
141
+ limit?: number | undefined;
142
+ mode?: "hybrid" | "vector" | "keyword" | undefined;
143
+ sourceKinds?: Array<"manual_upload" | "meeting_transcript" | "repository" | "email" | "chat" | "document" | "web" | "other"> | undefined;
144
+ aclTags?: string[] | undefined;
145
+ },
146
+ ) {
147
+ return {
148
+ content: [{
149
+ type: "text" as const,
150
+ text: JSON.stringify(await searchDocuments(db, {
151
+ workspaceId,
152
+ query: input.query,
153
+ ...(input.baseIds ? { baseIds: input.baseIds } : {}),
154
+ ...(input.limit ? { limit: input.limit } : {}),
155
+ ...(input.mode ? { mode: input.mode } : {}),
156
+ ...(input.sourceKinds ? { sourceKinds: input.sourceKinds } : {}),
157
+ ...(input.aclTags ? { aclTags: input.aclTags } : {}),
158
+ }, documentServices)),
159
+ }],
160
+ };
161
+ }
package/src/mcp/server.ts CHANGED
@@ -77,12 +77,14 @@ import {
77
77
  type RunOnOp,
78
78
  } from "@opengeni/core";
79
79
  import { capEventPage, capSessionDetail } from "./session-view";
80
+ import type { ToolspaceMcpSurface } from "./toolspace";
80
81
 
81
82
  export type McpServerOptions = {
82
83
  // Origin of the HTTP request that reached the MCP route; last-resort base
83
84
  // for links the server mints (github_connect_link) when neither
84
85
  // OPENGENI_PUBLIC_BASE_URL nor the manifest base URL is configured.
85
86
  requestOrigin?: string | null;
87
+ toolspace?: ToolspaceMcpSurface | null;
86
88
  };
87
89
 
88
90
  export function buildOpenGeniMcpServer(deps: ApiRouteDeps, grant: AccessGrant, options: McpServerOptions = {}): McpServer {
@@ -92,6 +94,7 @@ export function buildOpenGeniMcpServer(deps: ApiRouteDeps, grant: AccessGrant, o
92
94
  });
93
95
  const json = (value: unknown) => ({ content: [{ type: "text" as const, text: JSON.stringify(value, null, 2) }] });
94
96
  const can = (permission: Permission) => hasPermission(grant.permissions, permission);
97
+ const toolspaceMode = options.toolspace != null;
95
98
 
96
99
  // Session-scoped tools key off the worker-asserted sessionId claim (signed
97
100
  // into the delegated token by the worker, never agent-controlled).
@@ -99,7 +102,7 @@ export function buildOpenGeniMcpServer(deps: ApiRouteDeps, grant: AccessGrant, o
99
102
  // set_session_title names the agent's OWN session — pure session metadata,
100
103
  // not a goal operation — so it is available on every session, gated only on
101
104
  // the signed sessionId (NOT goals:manage, and NOT on a goal existing).
102
- if (sessionId !== null) {
105
+ if (sessionId !== null && (!toolspaceMode || can("sessions:control"))) {
103
106
  server.registerTool("set_session_title", {
104
107
  description: "Set this session's display title to a concise 3-7 word summary. Call once early to name the session; calling again replaces it unless a human has manually set the title.",
105
108
  inputSchema: { title: z4.string().min(1).max(200) },
@@ -119,7 +122,7 @@ export function buildOpenGeniMcpServer(deps: ApiRouteDeps, grant: AccessGrant, o
119
122
  // so they register only when the grant carries the worker-signed sessionId claim
120
123
  // (never agent-controlled). Gated on the selfhosted feature flag: the active
121
124
  // pointer + swap are only meaningful when bring-your-own-compute is enabled.
122
- if (sessionId !== null && deps.settings.sandboxSelfhostedEnabled) {
125
+ if (!toolspaceMode && sessionId !== null && deps.settings.sandboxSelfhostedEnabled) {
123
126
  registerFleetTools(server, deps, grant, sessionId, json);
124
127
  }
125
128
 
@@ -144,6 +147,7 @@ export function buildOpenGeniMcpServer(deps: ApiRouteDeps, grant: AccessGrant, o
144
147
  }
145
148
  }
146
149
 
150
+ if (!toolspaceMode || can("files:read")) {
147
151
  server.registerTool("files_get_download_url", {
148
152
  description: "Create a short-lived download URL for a ready file asset.",
149
153
  inputSchema: { fileId: z4.string().uuid() },
@@ -174,7 +178,9 @@ export function buildOpenGeniMcpServer(deps: ApiRouteDeps, grant: AccessGrant, o
174
178
  },
175
179
  });
176
180
  });
181
+ }
177
182
 
183
+ if (!toolspaceMode || can("github:use")) {
178
184
  server.registerTool("github_repositories_list", {
179
185
  description: "List GitHub App repositories available as scheduled task repository resources. Use the returned resource object in scheduled task agentConfig.resources.",
180
186
  inputSchema: { limit: z4.number().int().positive().optional() },
@@ -191,7 +197,9 @@ export function buildOpenGeniMcpServer(deps: ApiRouteDeps, grant: AccessGrant, o
191
197
  throw error;
192
198
  }
193
199
  });
200
+ }
194
201
 
202
+ if (!toolspaceMode || can("connections:read")) {
195
203
  server.registerTool("social_connections_list", {
196
204
  description: "List connected social media accounts available to social media analysis packs.",
197
205
  inputSchema: { limit: z4.number().int().positive().optional() },
@@ -266,6 +274,9 @@ export function buildOpenGeniMcpServer(deps: ApiRouteDeps, grant: AccessGrant, o
266
274
  });
267
275
  });
268
276
 
277
+ }
278
+
279
+ if (!toolspaceMode || can("scheduled_tasks:manage") || can("scheduled_tasks:run")) {
269
280
  server.registerTool("scheduled_tasks_list", {
270
281
  description: "List scheduled tasks.",
271
282
  inputSchema: { limit: z4.number().int().positive().optional() },
@@ -379,9 +390,33 @@ export function buildOpenGeniMcpServer(deps: ApiRouteDeps, grant: AccessGrant, o
379
390
  inputSchema: { taskId: z4.string().uuid(), limit: z4.number().int().positive().optional() },
380
391
  }, async ({ taskId, limit }) => json({ runs: await listScheduledTaskRuns(deps.db, grant.workspaceId, taskId, limit ?? 100) }));
381
392
 
393
+ }
394
+
395
+ registerToolspaceProxyTools(server, options.toolspace ?? null);
396
+
382
397
  return server;
383
398
  }
384
399
 
400
+ function registerToolspaceProxyTools(server: McpServer, surface: ToolspaceMcpSurface | null): void {
401
+ if (!surface) {
402
+ return;
403
+ }
404
+ for (const tool of surface.tools) {
405
+ server.registerTool(tool.name, {
406
+ ...(tool.description ? { description: tool.description } : {}),
407
+ inputSchema: z4.object({}).passthrough(),
408
+ _meta: {
409
+ opengeni: {
410
+ origin: "toolspace",
411
+ subjectId: surface.subjectId,
412
+ sessionId: surface.sessionId,
413
+ ...(tool.inputSchema ? { inputSchema: tool.inputSchema } : {}),
414
+ },
415
+ },
416
+ }, async (args) => await tool.call(args));
417
+ }
418
+ }
419
+
385
420
  function registerGoalTools(
386
421
  server: McpServer,
387
422
  deps: ApiRouteDeps,