@opengeni/api-router 0.12.5 → 0.12.12

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/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,9 +17,11 @@ 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,
24
+ type SessionAuthorizationSurface,
21
25
  type Session,
22
26
  UpdateScheduledTaskRequest,
23
27
  } from "@opengeni/contracts";
@@ -52,7 +56,6 @@ import {
52
56
  MEMORY_CORRECT_TOOL_DESCRIPTION,
53
57
  MEMORY_SAVE_TOOL_DESCRIPTION,
54
58
  MEMORY_SEARCH_TOOL_DESCRIPTION,
55
- requireFile,
56
59
  requireScheduledTask,
57
60
  requireSession,
58
61
  saveWorkspaceMemory,
@@ -73,7 +76,13 @@ import {
73
76
  GitHubAppConfigurationError,
74
77
  githubAppMissingSettings,
75
78
  } from "@opengeni/github";
76
- import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
79
+ import {
80
+ McpServer,
81
+ type RegisteredTool,
82
+ type ToolCallback,
83
+ } from "@modelcontextprotocol/sdk/server/mcp.js";
84
+ import type { AnySchema, ZodRawShapeCompat } from "@modelcontextprotocol/sdk/server/zod-compat.js";
85
+ import type { ToolAnnotations } from "@modelcontextprotocol/sdk/types.js";
77
86
  import * as z4 from "zod/v4";
78
87
  import {
79
88
  hasPermission,
@@ -157,15 +166,153 @@ export type McpServerOptions = {
157
166
  workspaceMemoryEnabled?: boolean | undefined;
158
167
  };
159
168
 
169
+ type FirstPartyToolAuthorization = {
170
+ sessionRequired?: true;
171
+ allOf?: readonly Permission[];
172
+ anyOf?: readonly Permission[];
173
+ };
174
+
175
+ /**
176
+ * Authorization is deliberately complete and separate from visibility. The
177
+ * `satisfies Record` check makes every catalog addition choose an explicit
178
+ * registration predicate before it can compile.
179
+ */
180
+ const FIRST_PARTY_TOOL_AUTHORIZATION = {
181
+ set_session_title: { sessionRequired: true, allOf: ["sessions:control"] },
182
+ goal_set: { sessionRequired: true, allOf: ["goals:manage"] },
183
+ goal_update: { sessionRequired: true, allOf: ["goals:manage"] },
184
+ goal_complete: { sessionRequired: true, allOf: ["goals:manage"] },
185
+ goal_pause: { sessionRequired: true, allOf: ["goals:manage"] },
186
+ memory_search: { sessionRequired: true, allOf: ["documents:search"] },
187
+ memory_save: { sessionRequired: true, allOf: ["documents:search"] },
188
+ memory_correct: { sessionRequired: true, allOf: ["documents:search"] },
189
+ sandboxes_list: { sessionRequired: true, allOf: ["sessions:read"] },
190
+ sandbox_attach: { sessionRequired: true, allOf: ["sessions:control"] },
191
+ sandbox_swap: { sessionRequired: true, allOf: ["sessions:control"] },
192
+ run_on: { sessionRequired: true, allOf: ["sessions:control"] },
193
+ sandbox_provision: { sessionRequired: true, allOf: ["sessions:control"] },
194
+ rig_list: { allOf: ["rigs:use"] },
195
+ rig_get: { allOf: ["rigs:use"] },
196
+ rig_propose_change: { allOf: ["rigs:use"] },
197
+ rig_verify: { allOf: ["rigs:use"] },
198
+ rig_promote: { allOf: ["rigs:manage"] },
199
+ sessions_list: { allOf: ["sessions:read"] },
200
+ session_get: { allOf: ["sessions:read"] },
201
+ session_events: { allOf: ["sessions:read"] },
202
+ session_create: { allOf: ["sessions:create"] },
203
+ session_send_message: { allOf: ["sessions:control"] },
204
+ session_pause: { allOf: ["sessions:control"] },
205
+ session_resume: { allOf: ["sessions:control"] },
206
+ session_steer: { sessionRequired: true, allOf: ["sessions:control"] },
207
+ set_other_session_title: { allOf: ["sessions:control"] },
208
+ variable_set_list: { allOf: ["variable-sets:use"] },
209
+ environment_list: { allOf: ["variable-sets:use"] },
210
+ variable_set_set_variable: { allOf: ["variable-sets:manage"] },
211
+ environment_set_variable: { allOf: ["variable-sets:manage"] },
212
+ github_connect_link: { allOf: ["github:use"] },
213
+ github_token: { sessionRequired: true, allOf: ["github:use"] },
214
+ github_repositories_list: { allOf: ["github:use"] },
215
+ social_connections_list: { allOf: ["connections:read"] },
216
+ social_posts_recent: { allOf: ["connections:read"] },
217
+ social_daily_analysis_context: { allOf: ["connections:read"] },
218
+ scheduled_tasks_list: { anyOf: ["scheduled_tasks:manage", "scheduled_tasks:run"] },
219
+ scheduled_tasks_get: { anyOf: ["scheduled_tasks:manage", "scheduled_tasks:run"] },
220
+ scheduled_tasks_create: { allOf: ["scheduled_tasks:manage"] },
221
+ scheduled_tasks_update: { allOf: ["scheduled_tasks:manage"] },
222
+ scheduled_tasks_pause: { allOf: ["scheduled_tasks:manage"] },
223
+ scheduled_tasks_resume: { allOf: ["scheduled_tasks:manage"] },
224
+ scheduled_tasks_trigger: { allOf: ["scheduled_tasks:run"] },
225
+ scheduled_tasks_delete: { allOf: ["scheduled_tasks:manage"] },
226
+ scheduled_task_runs_list: { anyOf: ["scheduled_tasks:manage", "scheduled_tasks:run"] },
227
+ slack_bot_list_channels: { allOf: ["connections:read"] },
228
+ slack_bot_channel_history: { allOf: ["connections:read"] },
229
+ slack_bot_list_users: { allOf: ["connections:read"] },
230
+ slack_bot_post_message: { allOf: ["connections:read"] },
231
+ } satisfies Record<FirstPartyMcpToolName, FirstPartyToolAuthorization>;
232
+
233
+ const FIRST_PARTY_MCP_TOOL_NAME_SET = new Set<string>(FIRST_PARTY_MCP_TOOL_NAMES);
234
+
235
+ class PolicyMcpServer extends McpServer {
236
+ private registeredToolCount = 0;
237
+
238
+ constructor(
239
+ private readonly grant: AccessGrant,
240
+ private readonly sessionId: string | null,
241
+ private readonly selectedTools: ReadonlySet<FirstPartyMcpToolName> | null,
242
+ private readonly allowUncatalogued: boolean,
243
+ ) {
244
+ super({ name: "opengeni", version: "1.0.0" });
245
+ }
246
+
247
+ override registerTool<
248
+ OutputArgs extends ZodRawShapeCompat | AnySchema,
249
+ InputArgs extends undefined | ZodRawShapeCompat | AnySchema = undefined,
250
+ >(
251
+ name: string,
252
+ config: {
253
+ title?: string;
254
+ description?: string;
255
+ inputSchema?: InputArgs;
256
+ outputSchema?: OutputArgs;
257
+ annotations?: ToolAnnotations;
258
+ _meta?: Record<string, unknown>;
259
+ },
260
+ cb: ToolCallback<InputArgs>,
261
+ ): RegisteredTool {
262
+ const catalogued = FIRST_PARTY_MCP_TOOL_NAME_SET.has(name);
263
+ let admitted = this.allowUncatalogued && !catalogued;
264
+ if (catalogued) {
265
+ const toolName = name as FirstPartyMcpToolName;
266
+ const policy: FirstPartyToolAuthorization = FIRST_PARTY_TOOL_AUTHORIZATION[toolName];
267
+ const authorized =
268
+ (!policy.sessionRequired || this.sessionId !== null) &&
269
+ (policy.allOf?.every((permission) => hasPermission(this.grant.permissions, permission)) ??
270
+ true) &&
271
+ (policy.anyOf?.some((permission) => hasPermission(this.grant.permissions, permission)) ??
272
+ true);
273
+ const selected = this.selectedTools === null || this.selectedTools.has(toolName);
274
+ admitted = authorized && selected;
275
+ }
276
+ if (!admitted) {
277
+ return {
278
+ ...(config.title ? { title: config.title } : {}),
279
+ ...(config.description ? { description: config.description } : {}),
280
+ ...(config.annotations ? { annotations: config.annotations } : {}),
281
+ ...(config._meta ? { _meta: config._meta } : {}),
282
+ handler: cb as RegisteredTool["handler"],
283
+ enabled: false,
284
+ enable() {},
285
+ disable() {},
286
+ update() {},
287
+ remove() {},
288
+ };
289
+ }
290
+ this.registeredToolCount += 1;
291
+ return super.registerTool(name, config, cb);
292
+ }
293
+
294
+ ensureToolsListHandler(): void {
295
+ if (this.registeredToolCount > 0) return;
296
+ super
297
+ .registerTool(
298
+ "__opengeni_empty_first_party_surface__",
299
+ {
300
+ description: "Internal disabled placeholder for an empty first-party surface.",
301
+ inputSchema: z4.object({}),
302
+ },
303
+ async () => ({
304
+ content: [{ type: "text" as const, text: '{"unavailable":true}' }],
305
+ }),
306
+ )
307
+ .disable();
308
+ }
309
+ }
310
+
160
311
  export function buildOpenGeniMcpServer(
161
312
  deps: ApiRouteDeps,
162
313
  grant: AccessGrant,
163
314
  options: McpServerOptions = {},
164
315
  ): McpServer {
165
- const server = new McpServer({
166
- name: "opengeni",
167
- version: "1.0.0",
168
- });
169
316
  const json = (value: unknown) => ({
170
317
  content: [{ type: "text" as const, text: JSON.stringify(value, null, 2) }],
171
318
  });
@@ -178,6 +325,14 @@ export function buildOpenGeniMcpServer(
178
325
  typeof grant.metadata?.["sessionId"] === "string"
179
326
  ? (grant.metadata["sessionId"] as string)
180
327
  : null;
328
+ const selectedTools =
329
+ sessionId !== null && !toolspaceMode
330
+ ? new Set(
331
+ (grant.metadata?.["firstPartyMcpTools"] as FirstPartyMcpToolName[] | undefined) ??
332
+ DEFAULT_FIRST_PARTY_MCP_TOOLS,
333
+ )
334
+ : null;
335
+ const server = new PolicyMcpServer(grant, sessionId, selectedTools, toolspaceMode);
181
336
  // set_session_title names the agent's OWN session — pure session metadata,
182
337
  // not a goal operation — so it is available on every session, gated only on
183
338
  // the signed sessionId (NOT goals:manage, and NOT on a goal existing).
@@ -248,45 +403,6 @@ export function buildOpenGeniMcpServer(
248
403
  }
249
404
  }
250
405
 
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
406
  if (!toolspaceMode || can("github:use")) {
291
407
  server.registerTool(
292
408
  "github_repositories_list",
@@ -604,6 +720,7 @@ export function buildOpenGeniMcpServer(
604
720
  task,
605
721
  agentRunUsageIdempotencyKey,
606
722
  triggerWorkflowId,
723
+ initiator: { kind: "subject", subjectId: grant.subjectId },
607
724
  });
608
725
  await recordWorkspaceUsage(deps, {
609
726
  accountId: grant.accountId,
@@ -653,6 +770,7 @@ export function buildOpenGeniMcpServer(
653
770
  }
654
771
 
655
772
  registerToolspaceProxyTools(server, options.toolspace ?? null);
773
+ server.ensureToolsListHandler();
656
774
 
657
775
  return server;
658
776
  }
@@ -1450,6 +1568,7 @@ function registerRigTools(
1450
1568
  function exactAgentCommandContext(
1451
1569
  grant: AccessGrant,
1452
1570
  callerSessionId: string,
1571
+ authorizationSurface?: SessionAuthorizationSurface,
1453
1572
  ): AgentSessionCommandContext {
1454
1573
  const turnId = grant.metadata?.["turnId"];
1455
1574
  const attemptId = grant.metadata?.["attemptId"];
@@ -1471,6 +1590,7 @@ function exactAgentCommandContext(
1471
1590
  callerTurnId: turnId,
1472
1591
  callerAttemptId: attemptId,
1473
1592
  callerExecutionGeneration: executionGeneration,
1593
+ ...(authorizationSurface ? { authorizationSurface } : {}),
1474
1594
  };
1475
1595
  }
1476
1596
 
@@ -1734,6 +1854,12 @@ function registerWorkspaceOrchestrationTools(
1734
1854
  .describe(
1735
1855
  "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
1856
  ),
1857
+ firstPartyMcpTools: z4
1858
+ .array(z4.enum(FIRST_PARTY_MCP_TOOL_NAMES))
1859
+ .optional()
1860
+ .describe(
1861
+ "Exact model-visible first-party tool selection for the child. Omit to inherit this session's effective selection. This does not grant permissions.",
1862
+ ),
1737
1863
  // Shared-sandbox placement (addendum 05 §D). OMIT (default) to SHARE the
1738
1864
  // creator's box — one filesystem/repo/desktop, N independent conversations;
1739
1865
  // this is the SAFE DEFAULT. Pass "new" for a fresh isolated box (a different
@@ -1792,7 +1918,7 @@ function registerWorkspaceOrchestrationTools(
1792
1918
  "session_send_message",
1793
1919
  {
1794
1920
  description:
1795
- "Send information to another session. From an OpenGeni worker this becomes a coalescible internal update, never a visible prompt-queue row; pending updates are delivered together on the target's next inference. A sessionless operator call appends one visible prompt.",
1921
+ "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
1922
  inputSchema: {
1797
1923
  sessionId: z4.string().uuid(),
1798
1924
  text: z4.string().min(1),
@@ -1830,7 +1956,6 @@ function registerWorkspaceOrchestrationTools(
1830
1956
  targetSessionId,
1831
1957
  {
1832
1958
  text,
1833
- toolsProvided: false,
1834
1959
  delivery: "send",
1835
1960
  origin: "operator",
1836
1961
  clientEventId: idempotencyKey,
@@ -1854,16 +1979,10 @@ function registerWorkspaceOrchestrationTools(
1854
1979
  },
1855
1980
  },
1856
1981
  async ({ sessionId, idempotencyKey, reason }) => {
1857
- const authorization = await authorizeFirstPartySession(
1858
- deps,
1859
- grant,
1860
- sessionId,
1861
- "session.control",
1862
- );
1863
1982
  if (callerSessionId !== null) {
1864
1983
  const controlled = await controlAgentSessionWorkstream(
1865
1984
  deps,
1866
- exactAgentCommandContext(grant, callerSessionId),
1985
+ exactAgentCommandContext(grant, callerSessionId, "first_party_mcp"),
1867
1986
  {
1868
1987
  targetSessionId: sessionId,
1869
1988
  action: "pause",
@@ -1876,7 +1995,7 @@ function registerWorkspaceOrchestrationTools(
1876
1995
  effectiveControl: projectEffectiveControlForRelatedAccess(
1877
1996
  serializeEffectiveSessionControl(controlled.control),
1878
1997
  sessionId,
1879
- authorization?.relatedSessionAccess ?? "root",
1998
+ controlled.authorization?.relatedSessionAccess ?? "root",
1880
1999
  ),
1881
2000
  interruptionCount: controlled.interruptionCount,
1882
2001
  replay: controlled.replay,
@@ -1889,6 +2008,7 @@ function registerWorkspaceOrchestrationTools(
1889
2008
  workspaceId: grant.workspaceId,
1890
2009
  sessionId,
1891
2010
  subjectId: grant.subjectId,
2011
+ authorizationSurface: "first_party_mcp",
1892
2012
  },
1893
2013
  {
1894
2014
  action: "pause",
@@ -1896,14 +2016,7 @@ function registerWorkspaceOrchestrationTools(
1896
2016
  ...(reason ? { reason } : {}),
1897
2017
  },
1898
2018
  );
1899
- return json({
1900
- ...controlled,
1901
- effectiveControl: projectEffectiveControlForRelatedAccess(
1902
- controlled.effectiveControl,
1903
- sessionId,
1904
- authorization?.relatedSessionAccess ?? "root",
1905
- ),
1906
- });
2019
+ return json(controlled);
1907
2020
  },
1908
2021
  );
1909
2022
 
@@ -1919,16 +2032,10 @@ function registerWorkspaceOrchestrationTools(
1919
2032
  },
1920
2033
  },
1921
2034
  async ({ sessionId, idempotencyKey, reason }) => {
1922
- const authorization = await authorizeFirstPartySession(
1923
- deps,
1924
- grant,
1925
- sessionId,
1926
- "session.control",
1927
- );
1928
2035
  if (callerSessionId !== null) {
1929
2036
  const controlled = await controlAgentSessionWorkstream(
1930
2037
  deps,
1931
- exactAgentCommandContext(grant, callerSessionId),
2038
+ exactAgentCommandContext(grant, callerSessionId, "first_party_mcp"),
1932
2039
  {
1933
2040
  targetSessionId: sessionId,
1934
2041
  action: "resume",
@@ -1941,7 +2048,7 @@ function registerWorkspaceOrchestrationTools(
1941
2048
  effectiveControl: projectEffectiveControlForRelatedAccess(
1942
2049
  serializeEffectiveSessionControl(controlled.control),
1943
2050
  sessionId,
1944
- authorization?.relatedSessionAccess ?? "root",
2051
+ controlled.authorization?.relatedSessionAccess ?? "root",
1945
2052
  ),
1946
2053
  interruptionCount: controlled.interruptionCount,
1947
2054
  replay: controlled.replay,
@@ -1954,6 +2061,7 @@ function registerWorkspaceOrchestrationTools(
1954
2061
  workspaceId: grant.workspaceId,
1955
2062
  sessionId,
1956
2063
  subjectId: grant.subjectId,
2064
+ authorizationSurface: "first_party_mcp",
1957
2065
  },
1958
2066
  {
1959
2067
  action: "resume",
@@ -1961,14 +2069,7 @@ function registerWorkspaceOrchestrationTools(
1961
2069
  ...(reason ? { reason } : {}),
1962
2070
  },
1963
2071
  );
1964
- return json({
1965
- ...controlled,
1966
- effectiveControl: projectEffectiveControlForRelatedAccess(
1967
- controlled.effectiveControl,
1968
- sessionId,
1969
- authorization?.relatedSessionAccess ?? "root",
1970
- ),
1971
- });
2072
+ return json(controlled);
1972
2073
  },
1973
2074
  );
1974
2075
 
@@ -1977,7 +2078,7 @@ function registerWorkspaceOrchestrationTools(
1977
2078
  "session_steer",
1978
2079
  {
1979
2080
  description:
1980
- "Atomically replace another session's current direction and resume it. The instruction is an internal update, never a human queue row.",
2081
+ "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
2082
  inputSchema: {
1982
2083
  sessionId: z4.string().uuid(),
1983
2084
  instruction: z4.string().min(1),
@@ -1985,10 +2086,9 @@ function registerWorkspaceOrchestrationTools(
1985
2086
  },
1986
2087
  },
1987
2088
  async ({ sessionId, instruction, idempotencyKey }) => {
1988
- await authorizeFirstPartySession(deps, grant, sessionId, "session.steer");
1989
2089
  const result = await steerAgentSession(
1990
2090
  deps,
1991
- exactAgentCommandContext(grant, callerSessionId),
2091
+ exactAgentCommandContext(grant, callerSessionId, "first_party_mcp"),
1992
2092
  { targetSessionId: sessionId, instruction, idempotencyKey },
1993
2093
  );
1994
2094
  return json({
@@ -2194,14 +2294,16 @@ function registerGitHubConnectTool(
2194
2294
  const { settings } = deps;
2195
2295
  const missing = githubAppMissingSettings(settings);
2196
2296
  const slug = settings.githubAppSlug?.trim() || null;
2297
+ const setupMode = settings.productAccessMode === "managed" ? "platform" : "operator";
2197
2298
  if (missing.length > 0 || !slug) {
2198
2299
  return json({
2199
2300
  configured: false,
2200
2301
  status: "disabled",
2201
- appSlug: slug,
2302
+ setupMode,
2303
+ appSlug: setupMode === "operator" ? slug : null,
2202
2304
  installUrl: null,
2203
2305
  linkUrl: null,
2204
- missing,
2306
+ missing: setupMode === "operator" ? missing : [],
2205
2307
  });
2206
2308
  }
2207
2309
  const installations = await listWorkspaceGitHubInstallationBindings(deps, grant.workspaceId);
@@ -2219,13 +2321,21 @@ function registerGitHubConnectTool(
2219
2321
  const connectUrl = state
2220
2322
  ? `${baseUrl}/v1/workspaces/${grant.workspaceId}/github/connect?state=${encodeURIComponent(state)}`
2221
2323
  : null;
2324
+ const installationViews = installations.map((installation) => ({
2325
+ ...installation,
2326
+ configureUrl:
2327
+ state && baseUrl
2328
+ ? `${baseUrl}/v1/workspaces/${grant.workspaceId}/github/installations/${installation.installationId}/configure?state=${encodeURIComponent(state)}`
2329
+ : null,
2330
+ }));
2222
2331
  return json({
2223
2332
  configured: true,
2224
2333
  status,
2225
- appSlug: slug,
2334
+ setupMode,
2335
+ appSlug: setupMode === "operator" ? slug : null,
2226
2336
  installUrl: connectUrl,
2227
2337
  linkUrl: connectUrl,
2228
- installations,
2338
+ installations: installationViews,
2229
2339
  missing: [],
2230
2340
  });
2231
2341
  },
@@ -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");