@opengeni/api-router 0.12.5 → 0.12.7
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-3BHOMOSD.js → chunk-SD5GJTZ4.js} +234 -70
- package/dist/chunk-SD5GJTZ4.js.map +1 -0
- package/dist/index.js +10 -4
- package/dist/index.js.map +1 -1
- package/package.json +10 -10
- package/src/app.ts +4 -0
- package/src/index.ts +8 -3
- package/src/mcp/files.ts +77 -0
- package/src/mcp/server.ts +170 -47
- package/src/routes/files.ts +11 -0
- package/src/routes/scheduled-tasks.ts +1 -0
- package/dist/chunk-3BHOMOSD.js.map +0 -1
package/src/mcp/server.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
CreateScheduledTaskRequest,
|
|
3
|
+
DEFAULT_FIRST_PARTY_MCP_TOOLS,
|
|
4
|
+
FIRST_PARTY_MCP_TOOL_NAMES,
|
|
3
5
|
defaultRepositoryMountPath,
|
|
4
6
|
SESSION_EVENT_RAW_DELTA_TYPES,
|
|
5
7
|
SessionEventLatestClass,
|
|
@@ -15,6 +17,7 @@ import {
|
|
|
15
17
|
VariableSetVariableName,
|
|
16
18
|
type AccessGrant,
|
|
17
19
|
type GitHubRepository,
|
|
20
|
+
type FirstPartyMcpToolName,
|
|
18
21
|
type Permission,
|
|
19
22
|
type ResourceRef,
|
|
20
23
|
type SessionAuthorizationOperation,
|
|
@@ -52,7 +55,6 @@ import {
|
|
|
52
55
|
MEMORY_CORRECT_TOOL_DESCRIPTION,
|
|
53
56
|
MEMORY_SAVE_TOOL_DESCRIPTION,
|
|
54
57
|
MEMORY_SEARCH_TOOL_DESCRIPTION,
|
|
55
|
-
requireFile,
|
|
56
58
|
requireScheduledTask,
|
|
57
59
|
requireSession,
|
|
58
60
|
saveWorkspaceMemory,
|
|
@@ -73,7 +75,13 @@ import {
|
|
|
73
75
|
GitHubAppConfigurationError,
|
|
74
76
|
githubAppMissingSettings,
|
|
75
77
|
} from "@opengeni/github";
|
|
76
|
-
import {
|
|
78
|
+
import {
|
|
79
|
+
McpServer,
|
|
80
|
+
type RegisteredTool,
|
|
81
|
+
type ToolCallback,
|
|
82
|
+
} from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
83
|
+
import type { AnySchema, ZodRawShapeCompat } from "@modelcontextprotocol/sdk/server/zod-compat.js";
|
|
84
|
+
import type { ToolAnnotations } from "@modelcontextprotocol/sdk/types.js";
|
|
77
85
|
import * as z4 from "zod/v4";
|
|
78
86
|
import {
|
|
79
87
|
hasPermission,
|
|
@@ -157,15 +165,153 @@ export type McpServerOptions = {
|
|
|
157
165
|
workspaceMemoryEnabled?: boolean | undefined;
|
|
158
166
|
};
|
|
159
167
|
|
|
168
|
+
type FirstPartyToolAuthorization = {
|
|
169
|
+
sessionRequired?: true;
|
|
170
|
+
allOf?: readonly Permission[];
|
|
171
|
+
anyOf?: readonly Permission[];
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Authorization is deliberately complete and separate from visibility. The
|
|
176
|
+
* `satisfies Record` check makes every catalog addition choose an explicit
|
|
177
|
+
* registration predicate before it can compile.
|
|
178
|
+
*/
|
|
179
|
+
const FIRST_PARTY_TOOL_AUTHORIZATION = {
|
|
180
|
+
set_session_title: { sessionRequired: true, allOf: ["sessions:control"] },
|
|
181
|
+
goal_set: { sessionRequired: true, allOf: ["goals:manage"] },
|
|
182
|
+
goal_update: { sessionRequired: true, allOf: ["goals:manage"] },
|
|
183
|
+
goal_complete: { sessionRequired: true, allOf: ["goals:manage"] },
|
|
184
|
+
goal_pause: { sessionRequired: true, allOf: ["goals:manage"] },
|
|
185
|
+
memory_search: { sessionRequired: true, allOf: ["documents:search"] },
|
|
186
|
+
memory_save: { sessionRequired: true, allOf: ["documents:search"] },
|
|
187
|
+
memory_correct: { sessionRequired: true, allOf: ["documents:search"] },
|
|
188
|
+
sandboxes_list: { sessionRequired: true, allOf: ["sessions:read"] },
|
|
189
|
+
sandbox_attach: { sessionRequired: true, allOf: ["sessions:control"] },
|
|
190
|
+
sandbox_swap: { sessionRequired: true, allOf: ["sessions:control"] },
|
|
191
|
+
run_on: { sessionRequired: true, allOf: ["sessions:control"] },
|
|
192
|
+
sandbox_provision: { sessionRequired: true, allOf: ["sessions:control"] },
|
|
193
|
+
rig_list: { allOf: ["rigs:use"] },
|
|
194
|
+
rig_get: { allOf: ["rigs:use"] },
|
|
195
|
+
rig_propose_change: { allOf: ["rigs:use"] },
|
|
196
|
+
rig_verify: { allOf: ["rigs:use"] },
|
|
197
|
+
rig_promote: { allOf: ["rigs:manage"] },
|
|
198
|
+
sessions_list: { allOf: ["sessions:read"] },
|
|
199
|
+
session_get: { allOf: ["sessions:read"] },
|
|
200
|
+
session_events: { allOf: ["sessions:read"] },
|
|
201
|
+
session_create: { allOf: ["sessions:create"] },
|
|
202
|
+
session_send_message: { allOf: ["sessions:control"] },
|
|
203
|
+
session_pause: { allOf: ["sessions:control"] },
|
|
204
|
+
session_resume: { allOf: ["sessions:control"] },
|
|
205
|
+
session_steer: { sessionRequired: true, allOf: ["sessions:control"] },
|
|
206
|
+
set_other_session_title: { allOf: ["sessions:control"] },
|
|
207
|
+
variable_set_list: { allOf: ["variable-sets:use"] },
|
|
208
|
+
environment_list: { allOf: ["variable-sets:use"] },
|
|
209
|
+
variable_set_set_variable: { allOf: ["variable-sets:manage"] },
|
|
210
|
+
environment_set_variable: { allOf: ["variable-sets:manage"] },
|
|
211
|
+
github_connect_link: { allOf: ["github:use"] },
|
|
212
|
+
github_token: { sessionRequired: true, allOf: ["github:use"] },
|
|
213
|
+
github_repositories_list: { allOf: ["github:use"] },
|
|
214
|
+
social_connections_list: { allOf: ["connections:read"] },
|
|
215
|
+
social_posts_recent: { allOf: ["connections:read"] },
|
|
216
|
+
social_daily_analysis_context: { allOf: ["connections:read"] },
|
|
217
|
+
scheduled_tasks_list: { anyOf: ["scheduled_tasks:manage", "scheduled_tasks:run"] },
|
|
218
|
+
scheduled_tasks_get: { anyOf: ["scheduled_tasks:manage", "scheduled_tasks:run"] },
|
|
219
|
+
scheduled_tasks_create: { allOf: ["scheduled_tasks:manage"] },
|
|
220
|
+
scheduled_tasks_update: { allOf: ["scheduled_tasks:manage"] },
|
|
221
|
+
scheduled_tasks_pause: { allOf: ["scheduled_tasks:manage"] },
|
|
222
|
+
scheduled_tasks_resume: { allOf: ["scheduled_tasks:manage"] },
|
|
223
|
+
scheduled_tasks_trigger: { allOf: ["scheduled_tasks:run"] },
|
|
224
|
+
scheduled_tasks_delete: { allOf: ["scheduled_tasks:manage"] },
|
|
225
|
+
scheduled_task_runs_list: { anyOf: ["scheduled_tasks:manage", "scheduled_tasks:run"] },
|
|
226
|
+
slack_bot_list_channels: { allOf: ["connections:read"] },
|
|
227
|
+
slack_bot_channel_history: { allOf: ["connections:read"] },
|
|
228
|
+
slack_bot_list_users: { allOf: ["connections:read"] },
|
|
229
|
+
slack_bot_post_message: { allOf: ["connections:read"] },
|
|
230
|
+
} satisfies Record<FirstPartyMcpToolName, FirstPartyToolAuthorization>;
|
|
231
|
+
|
|
232
|
+
const FIRST_PARTY_MCP_TOOL_NAME_SET = new Set<string>(FIRST_PARTY_MCP_TOOL_NAMES);
|
|
233
|
+
|
|
234
|
+
class PolicyMcpServer extends McpServer {
|
|
235
|
+
private registeredToolCount = 0;
|
|
236
|
+
|
|
237
|
+
constructor(
|
|
238
|
+
private readonly grant: AccessGrant,
|
|
239
|
+
private readonly sessionId: string | null,
|
|
240
|
+
private readonly selectedTools: ReadonlySet<FirstPartyMcpToolName> | null,
|
|
241
|
+
private readonly allowUncatalogued: boolean,
|
|
242
|
+
) {
|
|
243
|
+
super({ name: "opengeni", version: "1.0.0" });
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
override registerTool<
|
|
247
|
+
OutputArgs extends ZodRawShapeCompat | AnySchema,
|
|
248
|
+
InputArgs extends undefined | ZodRawShapeCompat | AnySchema = undefined,
|
|
249
|
+
>(
|
|
250
|
+
name: string,
|
|
251
|
+
config: {
|
|
252
|
+
title?: string;
|
|
253
|
+
description?: string;
|
|
254
|
+
inputSchema?: InputArgs;
|
|
255
|
+
outputSchema?: OutputArgs;
|
|
256
|
+
annotations?: ToolAnnotations;
|
|
257
|
+
_meta?: Record<string, unknown>;
|
|
258
|
+
},
|
|
259
|
+
cb: ToolCallback<InputArgs>,
|
|
260
|
+
): RegisteredTool {
|
|
261
|
+
const catalogued = FIRST_PARTY_MCP_TOOL_NAME_SET.has(name);
|
|
262
|
+
let admitted = this.allowUncatalogued && !catalogued;
|
|
263
|
+
if (catalogued) {
|
|
264
|
+
const toolName = name as FirstPartyMcpToolName;
|
|
265
|
+
const policy: FirstPartyToolAuthorization = FIRST_PARTY_TOOL_AUTHORIZATION[toolName];
|
|
266
|
+
const authorized =
|
|
267
|
+
(!policy.sessionRequired || this.sessionId !== null) &&
|
|
268
|
+
(policy.allOf?.every((permission) => hasPermission(this.grant.permissions, permission)) ??
|
|
269
|
+
true) &&
|
|
270
|
+
(policy.anyOf?.some((permission) => hasPermission(this.grant.permissions, permission)) ??
|
|
271
|
+
true);
|
|
272
|
+
const selected = this.selectedTools === null || this.selectedTools.has(toolName);
|
|
273
|
+
admitted = authorized && selected;
|
|
274
|
+
}
|
|
275
|
+
if (!admitted) {
|
|
276
|
+
return {
|
|
277
|
+
...(config.title ? { title: config.title } : {}),
|
|
278
|
+
...(config.description ? { description: config.description } : {}),
|
|
279
|
+
...(config.annotations ? { annotations: config.annotations } : {}),
|
|
280
|
+
...(config._meta ? { _meta: config._meta } : {}),
|
|
281
|
+
handler: cb as RegisteredTool["handler"],
|
|
282
|
+
enabled: false,
|
|
283
|
+
enable() {},
|
|
284
|
+
disable() {},
|
|
285
|
+
update() {},
|
|
286
|
+
remove() {},
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
this.registeredToolCount += 1;
|
|
290
|
+
return super.registerTool(name, config, cb);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
ensureToolsListHandler(): void {
|
|
294
|
+
if (this.registeredToolCount > 0) return;
|
|
295
|
+
super
|
|
296
|
+
.registerTool(
|
|
297
|
+
"__opengeni_empty_first_party_surface__",
|
|
298
|
+
{
|
|
299
|
+
description: "Internal disabled placeholder for an empty first-party surface.",
|
|
300
|
+
inputSchema: z4.object({}),
|
|
301
|
+
},
|
|
302
|
+
async () => ({
|
|
303
|
+
content: [{ type: "text" as const, text: '{"unavailable":true}' }],
|
|
304
|
+
}),
|
|
305
|
+
)
|
|
306
|
+
.disable();
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
160
310
|
export function buildOpenGeniMcpServer(
|
|
161
311
|
deps: ApiRouteDeps,
|
|
162
312
|
grant: AccessGrant,
|
|
163
313
|
options: McpServerOptions = {},
|
|
164
314
|
): McpServer {
|
|
165
|
-
const server = new McpServer({
|
|
166
|
-
name: "opengeni",
|
|
167
|
-
version: "1.0.0",
|
|
168
|
-
});
|
|
169
315
|
const json = (value: unknown) => ({
|
|
170
316
|
content: [{ type: "text" as const, text: JSON.stringify(value, null, 2) }],
|
|
171
317
|
});
|
|
@@ -178,6 +324,14 @@ export function buildOpenGeniMcpServer(
|
|
|
178
324
|
typeof grant.metadata?.["sessionId"] === "string"
|
|
179
325
|
? (grant.metadata["sessionId"] as string)
|
|
180
326
|
: null;
|
|
327
|
+
const selectedTools =
|
|
328
|
+
sessionId !== null && !toolspaceMode
|
|
329
|
+
? new Set(
|
|
330
|
+
(grant.metadata?.["firstPartyMcpTools"] as FirstPartyMcpToolName[] | undefined) ??
|
|
331
|
+
DEFAULT_FIRST_PARTY_MCP_TOOLS,
|
|
332
|
+
)
|
|
333
|
+
: null;
|
|
334
|
+
const server = new PolicyMcpServer(grant, sessionId, selectedTools, toolspaceMode);
|
|
181
335
|
// set_session_title names the agent's OWN session — pure session metadata,
|
|
182
336
|
// not a goal operation — so it is available on every session, gated only on
|
|
183
337
|
// the signed sessionId (NOT goals:manage, and NOT on a goal existing).
|
|
@@ -248,45 +402,6 @@ export function buildOpenGeniMcpServer(
|
|
|
248
402
|
}
|
|
249
403
|
}
|
|
250
404
|
|
|
251
|
-
if (!toolspaceMode || can("files:read")) {
|
|
252
|
-
server.registerTool(
|
|
253
|
-
"files_get_download_url",
|
|
254
|
-
{
|
|
255
|
-
description: "Create a short-lived download URL for a ready file asset.",
|
|
256
|
-
inputSchema: { fileId: z4.string().uuid() },
|
|
257
|
-
},
|
|
258
|
-
async ({ fileId }) => {
|
|
259
|
-
if (!deps.objectStorage) {
|
|
260
|
-
throw new Error("object storage is not configured");
|
|
261
|
-
}
|
|
262
|
-
const file = await requireFile(deps.db, grant.workspaceId, fileId);
|
|
263
|
-
if (file.status !== "ready") {
|
|
264
|
-
throw new Error(`file is ${file.status}`);
|
|
265
|
-
}
|
|
266
|
-
const signed = await deps.objectStorage.createGetUrl({
|
|
267
|
-
key: file.objectKey,
|
|
268
|
-
});
|
|
269
|
-
return json({
|
|
270
|
-
file: {
|
|
271
|
-
id: file.id,
|
|
272
|
-
filename: file.filename,
|
|
273
|
-
safeFilename: file.safeFilename,
|
|
274
|
-
contentType: file.contentType,
|
|
275
|
-
sizeBytes: file.sizeBytes,
|
|
276
|
-
sha256: file.sha256,
|
|
277
|
-
status: file.status,
|
|
278
|
-
createdAt: file.createdAt,
|
|
279
|
-
updatedAt: file.updatedAt,
|
|
280
|
-
},
|
|
281
|
-
downloadUrl: {
|
|
282
|
-
url: signed.url,
|
|
283
|
-
expiresAt: signed.expiresAt.toISOString(),
|
|
284
|
-
},
|
|
285
|
-
});
|
|
286
|
-
},
|
|
287
|
-
);
|
|
288
|
-
}
|
|
289
|
-
|
|
290
405
|
if (!toolspaceMode || can("github:use")) {
|
|
291
406
|
server.registerTool(
|
|
292
407
|
"github_repositories_list",
|
|
@@ -604,6 +719,7 @@ export function buildOpenGeniMcpServer(
|
|
|
604
719
|
task,
|
|
605
720
|
agentRunUsageIdempotencyKey,
|
|
606
721
|
triggerWorkflowId,
|
|
722
|
+
initiator: { kind: "subject", subjectId: grant.subjectId },
|
|
607
723
|
});
|
|
608
724
|
await recordWorkspaceUsage(deps, {
|
|
609
725
|
accountId: grant.accountId,
|
|
@@ -653,6 +769,7 @@ export function buildOpenGeniMcpServer(
|
|
|
653
769
|
}
|
|
654
770
|
|
|
655
771
|
registerToolspaceProxyTools(server, options.toolspace ?? null);
|
|
772
|
+
server.ensureToolsListHandler();
|
|
656
773
|
|
|
657
774
|
return server;
|
|
658
775
|
}
|
|
@@ -1734,6 +1851,12 @@ function registerWorkspaceOrchestrationTools(
|
|
|
1734
1851
|
.describe(
|
|
1735
1852
|
"Optional first-party capability set for the child. Omit to inherit this session's effective permissions. An explicit set may only narrow capabilities held by this session. A goal-bearing child requires goals:manage in the resulting set; creation fails rather than adding it implicitly.",
|
|
1736
1853
|
),
|
|
1854
|
+
firstPartyMcpTools: z4
|
|
1855
|
+
.array(z4.enum(FIRST_PARTY_MCP_TOOL_NAMES))
|
|
1856
|
+
.optional()
|
|
1857
|
+
.describe(
|
|
1858
|
+
"Exact model-visible first-party tool selection for the child. Omit to inherit this session's effective selection. This does not grant permissions.",
|
|
1859
|
+
),
|
|
1737
1860
|
// Shared-sandbox placement (addendum 05 §D). OMIT (default) to SHARE the
|
|
1738
1861
|
// creator's box — one filesystem/repo/desktop, N independent conversations;
|
|
1739
1862
|
// this is the SAFE DEFAULT. Pass "new" for a fresh isolated box (a different
|
|
@@ -1792,7 +1915,7 @@ function registerWorkspaceOrchestrationTools(
|
|
|
1792
1915
|
"session_send_message",
|
|
1793
1916
|
{
|
|
1794
1917
|
description:
|
|
1795
|
-
"Send information to another session. From an OpenGeni worker this becomes a coalescible
|
|
1918
|
+
"Send information to another session. From an OpenGeni worker this becomes a canonical coalescible machine input: it appears in the target's compact incoming queue group, is durably added to model history when claimed, and remains visible in the timeline. A sessionless operator call appends one human/API prompt.",
|
|
1796
1919
|
inputSchema: {
|
|
1797
1920
|
sessionId: z4.string().uuid(),
|
|
1798
1921
|
text: z4.string().min(1),
|
|
@@ -1977,7 +2100,7 @@ function registerWorkspaceOrchestrationTools(
|
|
|
1977
2100
|
"session_steer",
|
|
1978
2101
|
{
|
|
1979
2102
|
description:
|
|
1980
|
-
"Atomically replace another session's current direction and resume it. The
|
|
2103
|
+
"Atomically replace another session's current direction and resume it. The Agent Steer is a typed machine input shown in the target queue/timeline and durably retained in model history; it never impersonates a human prompt.",
|
|
1981
2104
|
inputSchema: {
|
|
1982
2105
|
sessionId: z4.string().uuid(),
|
|
1983
2106
|
instruction: z4.string().min(1),
|
package/src/routes/files.ts
CHANGED
|
@@ -27,10 +27,21 @@ import { HTTPException } from "hono/http-exception";
|
|
|
27
27
|
import { requireAccessGrant } from "@opengeni/core";
|
|
28
28
|
import { recordWorkspaceUsage, requireLimit } from "@opengeni/core";
|
|
29
29
|
import type { ApiRouteDeps } from "@opengeni/core";
|
|
30
|
+
import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js";
|
|
31
|
+
import { buildFilesMcpServer } from "../mcp/files";
|
|
30
32
|
|
|
31
33
|
export function registerFileRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
32
34
|
const { db, objectStorage } = deps;
|
|
33
35
|
|
|
36
|
+
app.all("/v1/workspaces/:workspaceId/mcp/files", async (c) => {
|
|
37
|
+
const workspaceId = c.req.param("workspaceId");
|
|
38
|
+
const grant = await requireAccessGrant(c, deps, workspaceId, "files:read");
|
|
39
|
+
const transport = new WebStandardStreamableHTTPServerTransport({ enableJsonResponse: true });
|
|
40
|
+
const server = buildFilesMcpServer(deps, grant);
|
|
41
|
+
await server.connect(transport);
|
|
42
|
+
return await transport.handleRequest(c.req.raw);
|
|
43
|
+
});
|
|
44
|
+
|
|
34
45
|
app.post("/v1/workspaces/:workspaceId/files/uploads", async (c) => {
|
|
35
46
|
const workspaceId = c.req.param("workspaceId");
|
|
36
47
|
const grant = await requireAccessGrant(c, deps, workspaceId, "files:upload");
|
|
@@ -131,6 +131,7 @@ export function registerScheduledTaskRoutes(app: Hono, deps: ApiRouteDeps): void
|
|
|
131
131
|
task,
|
|
132
132
|
agentRunUsageIdempotencyKey,
|
|
133
133
|
triggerWorkflowId,
|
|
134
|
+
initiator: { kind: "subject", subjectId: grant.subjectId },
|
|
134
135
|
});
|
|
135
136
|
await recordWorkspaceUsage(deps, {
|
|
136
137
|
accountId: grant.accountId,
|