@opengeni/api-router 0.5.7 → 0.9.0
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.d.ts +21 -3
- package/dist/app.js +3 -1
- package/dist/{chunk-HBEJMWD3.js → chunk-QOYQBYHM.js} +4302 -926
- package/dist/chunk-QOYQBYHM.js.map +1 -0
- package/dist/index.js +4 -3
- package/dist/index.js.map +1 -1
- package/package.json +12 -11
- package/src/app.ts +83 -18
- package/src/codex-redemption-security.ts +96 -0
- package/src/github-access.ts +46 -0
- package/src/github-browser-flow.ts +83 -0
- package/src/http/auth.ts +12 -4
- package/src/http/sse.ts +526 -92
- package/src/index.ts +2 -1
- package/src/integrations/oauth-client.ts +147 -61
- package/src/mcp/server.ts +828 -146
- package/src/mcp/session-view.ts +721 -201
- package/src/mcp/toolspace.ts +482 -132
- package/src/model-catalog.ts +337 -0
- package/src/routes/codex.ts +883 -28
- package/src/routes/enrollments.ts +2 -2
- package/src/routes/files.ts +153 -0
- package/src/routes/github.ts +63 -202
- package/src/routes/install.ts +1 -1
- package/src/routes/machines.ts +2 -2
- package/src/routes/sessions.ts +789 -81
- package/src/routes/workspace-capture.ts +56 -38
- package/src/routes/workspaces.ts +66 -13
- package/src/sandbox/access.ts +1 -1
- package/src/sandbox/auth-callout.ts +1 -1
- package/src/sandbox/channel-a.ts +14 -1
- package/src/sandbox/enrollment.ts +4 -4
- package/src/sandbox/machines.ts +1 -1
- package/src/sandbox/metrics-ingestion.ts +1 -1
- package/src/sandbox/viewer.ts +8 -2
- package/dist/chunk-HBEJMWD3.js.map +0 -1
package/src/mcp/toolspace.ts
CHANGED
|
@@ -1,9 +1,14 @@
|
|
|
1
1
|
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
2
2
|
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
|
|
3
|
-
import type {
|
|
3
|
+
import type { Transport } from "@modelcontextprotocol/sdk/shared/transport.js";
|
|
4
4
|
import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
|
|
5
5
|
import { environmentsEncryptionKeyBytes, type McpServerConfig } from "@opengeni/config";
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
prefixedMcpToolName,
|
|
8
|
+
type AccessGrant,
|
|
9
|
+
type SessionTurn,
|
|
10
|
+
type ToolRef,
|
|
11
|
+
} from "@opengeni/contracts";
|
|
7
12
|
import {
|
|
8
13
|
hasPermission,
|
|
9
14
|
settingsWithEnabledCapabilityMcpServers,
|
|
@@ -11,13 +16,36 @@ import {
|
|
|
11
16
|
} from "@opengeni/core";
|
|
12
17
|
import {
|
|
13
18
|
buildConnectionTokenResolver,
|
|
19
|
+
buildHostConnectionTokenResolver,
|
|
20
|
+
clearPendingSessionToolspaceCall,
|
|
21
|
+
getActiveSessionTurnForExecution,
|
|
22
|
+
getSessionRootId,
|
|
14
23
|
listSessionMcpServerMetadata,
|
|
15
24
|
listSessionMcpServersForRun,
|
|
25
|
+
registerPendingSessionToolCall,
|
|
16
26
|
requireSession,
|
|
17
|
-
|
|
27
|
+
reserveToolspaceCallForAttempt,
|
|
18
28
|
type ResolveConnectionCredentialResult,
|
|
19
29
|
} from "@opengeni/db";
|
|
20
|
-
import { appendAndPublishEvents } from "@opengeni/events";
|
|
30
|
+
import { appendAndPublishEvents, appendAndPublishTurnEventsFenced } from "@opengeni/events";
|
|
31
|
+
import { undiciFetch, type FetchLike } from "@opengeni/network";
|
|
32
|
+
import {
|
|
33
|
+
MCP_MAX_AGGREGATE_TOOL_LIST_BYTES,
|
|
34
|
+
MCP_MAX_AGGREGATE_TOOL_LIST_ENTRIES,
|
|
35
|
+
MCP_MAX_CONCURRENT_SERVER_OPERATIONS,
|
|
36
|
+
MCP_MAX_TOOL_RESULT_BYTES,
|
|
37
|
+
McpAggregateToolListBudget,
|
|
38
|
+
McpPayloadTooLargeError,
|
|
39
|
+
assertMcpPayloadWithinBytes,
|
|
40
|
+
assertMcpServerSelectionWithinBounds,
|
|
41
|
+
assertMcpToolListWithinBounds,
|
|
42
|
+
boundedParallelMap,
|
|
43
|
+
cancelMcpResponseBody,
|
|
44
|
+
guardedMcpFetch,
|
|
45
|
+
mcpSerializedSizeBytes,
|
|
46
|
+
} from "@opengeni/runtime/mcp-network";
|
|
47
|
+
import { Buffer } from "node:buffer";
|
|
48
|
+
import { createHash } from "node:crypto";
|
|
21
49
|
|
|
22
50
|
export type ToolspaceCallResult = CallToolResult;
|
|
23
51
|
|
|
@@ -65,19 +93,109 @@ const FIRST_PARTY_PROXY_IDS = new Set(["files", "docs"]);
|
|
|
65
93
|
// to every upstream on every call.
|
|
66
94
|
const TOOLSPACE_TOOL_LIST_TTL_MS = 30_000;
|
|
67
95
|
const TOOLSPACE_TOOL_LIST_CACHE_MAX_ENTRIES = 2_000;
|
|
68
|
-
const
|
|
96
|
+
const TOOLSPACE_TOOL_LIST_CACHE_MAX_BYTES = 64 * 1024 * 1024;
|
|
69
97
|
|
|
70
|
-
type ToolListingEntry = {
|
|
98
|
+
export type ToolListingEntry = {
|
|
71
99
|
serverId: string;
|
|
72
100
|
tool: McpTool;
|
|
73
101
|
requireApproval: McpServerConfig["requireApproval"];
|
|
74
102
|
};
|
|
75
103
|
|
|
104
|
+
type ToolListCacheValue = {
|
|
105
|
+
expiresAt: number;
|
|
106
|
+
entries: ToolListingEntry[];
|
|
107
|
+
sizeBytes: number;
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
/** Deterministic LRU bounded by both key count and serialized retained bytes. */
|
|
111
|
+
export class ToolspaceToolListCache {
|
|
112
|
+
private readonly values = new Map<string, ToolListCacheValue>();
|
|
113
|
+
private retainedBytes = 0;
|
|
114
|
+
|
|
115
|
+
constructor(
|
|
116
|
+
private readonly maxEntries = TOOLSPACE_TOOL_LIST_CACHE_MAX_ENTRIES,
|
|
117
|
+
private readonly maxBytes = TOOLSPACE_TOOL_LIST_CACHE_MAX_BYTES,
|
|
118
|
+
private readonly ttlMs = TOOLSPACE_TOOL_LIST_TTL_MS,
|
|
119
|
+
) {
|
|
120
|
+
if (maxEntries < 1 || maxBytes < 1 || ttlMs < 1) {
|
|
121
|
+
throw new Error("toolspace cache limits must be positive");
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
read(key: string, now = Date.now()): ToolListingEntry[] | null {
|
|
126
|
+
const hit = this.values.get(key);
|
|
127
|
+
if (!hit) return null;
|
|
128
|
+
if (hit.expiresAt <= now) {
|
|
129
|
+
this.delete(key);
|
|
130
|
+
return null;
|
|
131
|
+
}
|
|
132
|
+
this.values.delete(key);
|
|
133
|
+
this.values.set(key, hit);
|
|
134
|
+
return hit.entries;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
write(key: string, entries: ToolListingEntry[], now = Date.now()): boolean {
|
|
138
|
+
const sizeBytes = Buffer.byteLength(key) + mcpSerializedSizeBytes(entries);
|
|
139
|
+
if (sizeBytes > this.maxBytes) return false;
|
|
140
|
+
// A rejected replacement is a no-op: preserve the current safe value
|
|
141
|
+
// until the candidate has passed its own size check.
|
|
142
|
+
this.delete(key);
|
|
143
|
+
for (const [existingKey, value] of this.values) {
|
|
144
|
+
if (value.expiresAt <= now) this.delete(existingKey);
|
|
145
|
+
}
|
|
146
|
+
while (this.values.size >= this.maxEntries || this.retainedBytes + sizeBytes > this.maxBytes) {
|
|
147
|
+
const oldestKey = this.values.keys().next().value as string | undefined;
|
|
148
|
+
if (oldestKey === undefined) break;
|
|
149
|
+
this.delete(oldestKey);
|
|
150
|
+
}
|
|
151
|
+
this.values.set(key, { expiresAt: now + this.ttlMs, entries, sizeBytes });
|
|
152
|
+
this.retainedBytes += sizeBytes;
|
|
153
|
+
return true;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
clear(): void {
|
|
157
|
+
this.values.clear();
|
|
158
|
+
this.retainedBytes = 0;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
snapshot(): { entries: number; bytes: number; keys: string[] } {
|
|
162
|
+
return {
|
|
163
|
+
entries: this.values.size,
|
|
164
|
+
bytes: this.retainedBytes,
|
|
165
|
+
keys: [...this.values.keys()],
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
private delete(key: string): void {
|
|
170
|
+
const existing = this.values.get(key);
|
|
171
|
+
if (!existing) return;
|
|
172
|
+
this.values.delete(key);
|
|
173
|
+
this.retainedBytes -= existing.sizeBytes;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const toolListCache = new ToolspaceToolListCache();
|
|
178
|
+
|
|
179
|
+
type ToolspaceAuthority = {
|
|
180
|
+
sessionId: string;
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
type ToolspaceAttemptAuthority = ToolspaceAuthority & {
|
|
184
|
+
turnId: string;
|
|
185
|
+
attemptId: string;
|
|
186
|
+
executionGeneration: number;
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
function toolspaceAuthorityForGrant(grant: AccessGrant): ToolspaceAuthority | null {
|
|
190
|
+
const sessionId = grant.metadata?.sessionId;
|
|
191
|
+
return typeof sessionId === "string" ? { sessionId } : null;
|
|
192
|
+
}
|
|
193
|
+
|
|
76
194
|
export function isToolspaceGrant(settings: ApiRouteDeps["settings"], grant: AccessGrant): boolean {
|
|
77
195
|
return (
|
|
78
196
|
settings.toolspaceEnabled &&
|
|
79
197
|
hasPermission(grant.permissions, "toolspace:call") &&
|
|
80
|
-
|
|
198
|
+
toolspaceAuthorityForGrant(grant) !== null
|
|
81
199
|
);
|
|
82
200
|
}
|
|
83
201
|
|
|
@@ -89,8 +207,33 @@ export async function prepareToolspaceMcpSurface(input: {
|
|
|
89
207
|
if (!isToolspaceGrant(deps.settings, grant)) {
|
|
90
208
|
return null;
|
|
91
209
|
}
|
|
92
|
-
const
|
|
210
|
+
const authority = toolspaceAuthorityForGrant(grant);
|
|
211
|
+
if (!authority) {
|
|
212
|
+
return null;
|
|
213
|
+
}
|
|
214
|
+
const { sessionId } = authority;
|
|
215
|
+
const activeTurn = await getActiveSessionTurnForExecution(deps.db, grant.workspaceId, sessionId);
|
|
216
|
+
// Recovering/waiting-capacity attempts retain ownership pointers, but they
|
|
217
|
+
// are not currently executing model code. Do not even enumerate upstream
|
|
218
|
+
// tools until the turn has returned to the running state.
|
|
219
|
+
if (!activeTurn?.activeAttemptId || activeTurn.status !== "running") {
|
|
220
|
+
return emptyToolspaceSurface(sessionId, grant.subjectId);
|
|
221
|
+
}
|
|
222
|
+
const attemptAuthority: ToolspaceAttemptAuthority = {
|
|
223
|
+
sessionId,
|
|
224
|
+
turnId: activeTurn.id,
|
|
225
|
+
attemptId: activeTurn.activeAttemptId,
|
|
226
|
+
executionGeneration: activeTurn.executionGeneration,
|
|
227
|
+
};
|
|
93
228
|
const session = await requireSession(deps.db, grant.workspaceId, sessionId);
|
|
229
|
+
let rootSessionId = sessionId;
|
|
230
|
+
if (deps.connectionCredentials?.mcpCredentials) {
|
|
231
|
+
const resolvedRootSessionId = await getSessionRootId(deps.db, grant.workspaceId, sessionId);
|
|
232
|
+
if (!resolvedRootSessionId) {
|
|
233
|
+
throw new Error(`cannot resolve host MCP credentials for missing session ${sessionId}`);
|
|
234
|
+
}
|
|
235
|
+
rootSessionId = resolvedRootSessionId;
|
|
236
|
+
}
|
|
94
237
|
const selectedIds = selectedMcpServerIds(
|
|
95
238
|
session.tools,
|
|
96
239
|
session.mcpServers.map((server) => server.id),
|
|
@@ -98,28 +241,43 @@ export async function prepareToolspaceMcpSurface(input: {
|
|
|
98
241
|
// Proxyable ids: everything selected except the first-party OpenGeni tool
|
|
99
242
|
// server and the first-party MCP proxies, both of which would re-enter /mcp.
|
|
100
243
|
const proxyableIds = [...selectedIds].filter((id) => toolspaceCanProxyServerId(id));
|
|
244
|
+
assertMcpServerSelectionWithinBounds(proxyableIds);
|
|
101
245
|
if (proxyableIds.length === 0) {
|
|
102
246
|
return emptyToolspaceSurface(sessionId, grant.subjectId);
|
|
103
247
|
}
|
|
104
|
-
|
|
105
248
|
// The registry (decrypted session servers + capability/pack expansion) is a
|
|
106
249
|
// handful of DB reads with no upstream dials. Build it at most once per
|
|
107
250
|
// request, and only when we actually need it (a cache-miss listing or a real
|
|
108
251
|
// tools/call), so a cache-hit request does no registry work.
|
|
109
|
-
|
|
110
|
-
const getRegistry = () =>
|
|
111
|
-
|
|
252
|
+
const registryPromises = new Map<string, Promise<Map<string, McpServerConfig>>>();
|
|
253
|
+
const getRegistry = (attemptId: string) => {
|
|
254
|
+
const existing = registryPromises.get(attemptId);
|
|
255
|
+
if (existing) {
|
|
256
|
+
return existing;
|
|
257
|
+
}
|
|
258
|
+
const created = buildToolspaceRegistry(deps, grant.workspaceId, sessionId, attemptId);
|
|
259
|
+
registryPromises.set(attemptId, created);
|
|
260
|
+
return created;
|
|
261
|
+
};
|
|
112
262
|
|
|
113
263
|
const listing = await resolveToolListing({
|
|
114
264
|
deps,
|
|
115
265
|
grant,
|
|
116
266
|
sessionId,
|
|
267
|
+
rootSessionId,
|
|
117
268
|
proxyableIds,
|
|
118
|
-
|
|
119
|
-
getRegistry,
|
|
269
|
+
activeTurn,
|
|
270
|
+
getRegistry: () => getRegistry(attemptAuthority.attemptId),
|
|
120
271
|
});
|
|
121
272
|
const tools = listing.map((entry) =>
|
|
122
|
-
toolspaceToolFor({
|
|
273
|
+
toolspaceToolFor({
|
|
274
|
+
deps,
|
|
275
|
+
grant,
|
|
276
|
+
authority: attemptAuthority,
|
|
277
|
+
rootSessionId,
|
|
278
|
+
entry,
|
|
279
|
+
getRegistry,
|
|
280
|
+
}),
|
|
123
281
|
);
|
|
124
282
|
|
|
125
283
|
return {
|
|
@@ -140,6 +298,7 @@ async function buildToolspaceRegistry(
|
|
|
140
298
|
deps: ApiRouteDeps,
|
|
141
299
|
workspaceId: string,
|
|
142
300
|
sessionId: string,
|
|
301
|
+
attemptId: string,
|
|
143
302
|
): Promise<Map<string, McpServerConfig>> {
|
|
144
303
|
const runtimeSettings = await settingsWithEnabledCapabilityMcpServers(
|
|
145
304
|
deps.db,
|
|
@@ -150,6 +309,7 @@ async function buildToolspaceRegistry(
|
|
|
150
309
|
deps,
|
|
151
310
|
workspaceId,
|
|
152
311
|
sessionId,
|
|
312
|
+
attemptId,
|
|
153
313
|
runtimeSettings,
|
|
154
314
|
);
|
|
155
315
|
return new Map(withSessionServers.mcpServers.map((server) => [server.id, server]));
|
|
@@ -164,46 +324,82 @@ async function resolveToolListing(input: {
|
|
|
164
324
|
deps: ApiRouteDeps;
|
|
165
325
|
grant: AccessGrant;
|
|
166
326
|
sessionId: string;
|
|
327
|
+
rootSessionId: string;
|
|
167
328
|
proxyableIds: string[];
|
|
168
|
-
|
|
329
|
+
activeTurn: SessionTurn;
|
|
169
330
|
getRegistry: () => Promise<Map<string, McpServerConfig>>;
|
|
170
331
|
}): Promise<ToolListingEntry[]> {
|
|
171
|
-
const { deps, grant, sessionId, proxyableIds,
|
|
172
|
-
|
|
332
|
+
const { deps, grant, sessionId, rootSessionId, proxyableIds, activeTurn, getRegistry } = input;
|
|
333
|
+
// Host credentials can be initiator-specific. A prior turn's tool list must
|
|
334
|
+
// never be reused under a different frozen authority in the same session.
|
|
335
|
+
const cacheKey = await toolListCacheKey(
|
|
336
|
+
deps,
|
|
337
|
+
grant.workspaceId,
|
|
338
|
+
sessionId,
|
|
339
|
+
proxyableIds,
|
|
340
|
+
activeTurn,
|
|
341
|
+
);
|
|
173
342
|
const cached = readToolListCache(cacheKey);
|
|
174
343
|
if (cached) {
|
|
175
344
|
return cached;
|
|
176
345
|
}
|
|
177
|
-
if (!activeTurnId) {
|
|
178
|
-
return [];
|
|
179
|
-
}
|
|
180
346
|
const registry = await getRegistry();
|
|
347
|
+
const aggregateBudget = new McpAggregateToolListBudget(
|
|
348
|
+
"aggregate Toolspace tool list",
|
|
349
|
+
MCP_MAX_AGGREGATE_TOOL_LIST_ENTRIES,
|
|
350
|
+
MCP_MAX_AGGREGATE_TOOL_LIST_BYTES,
|
|
351
|
+
);
|
|
181
352
|
const entries: ToolListingEntry[] = [];
|
|
182
|
-
|
|
353
|
+
// Each mapper commits its source contribution synchronously immediately
|
|
354
|
+
// after the upstream result is bounded. No per-provider result arrays are
|
|
355
|
+
// retained by boundedParallelMap, and a failed aggregate replacement never
|
|
356
|
+
// reaches the cache or the exposed MCP surface.
|
|
357
|
+
await boundedParallelMap(proxyableIds, MCP_MAX_CONCURRENT_SERVER_OPERATIONS, async (serverId) => {
|
|
183
358
|
const config = registry.get(serverId);
|
|
184
359
|
if (!config || !toolspaceCanProxyServer(config)) {
|
|
185
|
-
|
|
360
|
+
aggregateBudget.replace(serverId, []);
|
|
361
|
+
return;
|
|
186
362
|
}
|
|
187
|
-
const connection = await connectToolspaceServer({
|
|
188
|
-
|
|
189
|
-
|
|
363
|
+
const connection = await connectToolspaceServer({
|
|
364
|
+
deps,
|
|
365
|
+
grant,
|
|
366
|
+
config,
|
|
367
|
+
sessionId,
|
|
368
|
+
rootSessionId,
|
|
369
|
+
turn: activeTurn,
|
|
370
|
+
}).catch(() => null);
|
|
190
371
|
if (!connection) {
|
|
191
|
-
|
|
372
|
+
aggregateBudget.replace(serverId, []);
|
|
373
|
+
return;
|
|
192
374
|
}
|
|
193
375
|
try {
|
|
194
376
|
const listed = await connection.client
|
|
195
377
|
.listTools(undefined, toolspaceRequestOptions(config))
|
|
196
378
|
.catch(() => ({ tools: [] }));
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
379
|
+
let boundedTools: readonly McpTool[];
|
|
380
|
+
try {
|
|
381
|
+
boundedTools = assertMcpToolListWithinBounds(listed.tools as McpTool[]) as McpTool[];
|
|
382
|
+
} catch (error) {
|
|
383
|
+
deps.observability?.warn("toolspace upstream tool list exceeded safety limit", {
|
|
384
|
+
serverId,
|
|
385
|
+
errorClass: error instanceof Error ? error.name : typeof error,
|
|
386
|
+
});
|
|
387
|
+
aggregateBudget.replace(serverId, []);
|
|
388
|
+
return;
|
|
202
389
|
}
|
|
390
|
+
const sourceEntries = boundedTools
|
|
391
|
+
.filter((tool) => Boolean(tool?.name) && allowedByConfig(config, tool.name))
|
|
392
|
+
.map((tool) => ({
|
|
393
|
+
serverId,
|
|
394
|
+
tool,
|
|
395
|
+
requireApproval: config.requireApproval,
|
|
396
|
+
}));
|
|
397
|
+
aggregateBudget.replace(serverId, sourceEntries);
|
|
398
|
+
entries.push(...sourceEntries);
|
|
203
399
|
} finally {
|
|
204
400
|
await connection.close();
|
|
205
401
|
}
|
|
206
|
-
}
|
|
402
|
+
});
|
|
207
403
|
writeToolListCache(cacheKey, entries);
|
|
208
404
|
return entries;
|
|
209
405
|
}
|
|
@@ -213,6 +409,7 @@ async function toolListCacheKey(
|
|
|
213
409
|
workspaceId: string,
|
|
214
410
|
sessionId: string,
|
|
215
411
|
proxyableIds: string[],
|
|
412
|
+
turn: SessionTurn,
|
|
216
413
|
): Promise<string> {
|
|
217
414
|
const metadata = await listSessionMcpServerMetadata(deps.db, workspaceId, sessionId);
|
|
218
415
|
const versions = new Map(metadata.map((server) => [server.id, server.credentialVersion]));
|
|
@@ -221,40 +418,28 @@ async function toolListCacheKey(
|
|
|
221
418
|
.sort()
|
|
222
419
|
.map((id) => `${id}@${versions.get(id) ?? 0}`)
|
|
223
420
|
.join(",");
|
|
224
|
-
|
|
421
|
+
const authority = JSON.stringify({
|
|
422
|
+
turnId: turn.id,
|
|
423
|
+
executionGeneration: turn.executionGeneration,
|
|
424
|
+
attemptId: turn.activeAttemptId,
|
|
425
|
+
initiator: turn.initiator,
|
|
426
|
+
});
|
|
427
|
+
return `${workspaceId}:${sessionId}:${signature}:${authority}`;
|
|
225
428
|
}
|
|
226
429
|
|
|
227
430
|
function readToolListCache(key: string): ToolListingEntry[] | null {
|
|
228
|
-
|
|
229
|
-
if (!hit) {
|
|
230
|
-
return null;
|
|
231
|
-
}
|
|
232
|
-
if (hit.expiresAt <= Date.now()) {
|
|
233
|
-
toolListCache.delete(key);
|
|
234
|
-
return null;
|
|
235
|
-
}
|
|
236
|
-
return hit.entries;
|
|
431
|
+
return toolListCache.read(key);
|
|
237
432
|
}
|
|
238
433
|
|
|
239
434
|
function writeToolListCache(key: string, entries: ToolListingEntry[]): void {
|
|
240
|
-
|
|
241
|
-
const now = Date.now();
|
|
242
|
-
for (const [existingKey, value] of toolListCache) {
|
|
243
|
-
if (value.expiresAt <= now) {
|
|
244
|
-
toolListCache.delete(existingKey);
|
|
245
|
-
}
|
|
246
|
-
}
|
|
247
|
-
if (toolListCache.size >= TOOLSPACE_TOOL_LIST_CACHE_MAX_ENTRIES) {
|
|
248
|
-
toolListCache.clear();
|
|
249
|
-
}
|
|
250
|
-
}
|
|
251
|
-
toolListCache.set(key, { expiresAt: Date.now() + TOOLSPACE_TOOL_LIST_TTL_MS, entries });
|
|
435
|
+
toolListCache.write(key, entries);
|
|
252
436
|
}
|
|
253
437
|
|
|
254
438
|
async function settingsWithSessionMcpServersForToolspace(
|
|
255
439
|
deps: ApiRouteDeps,
|
|
256
440
|
workspaceId: string,
|
|
257
441
|
sessionId: string,
|
|
442
|
+
attemptId: string,
|
|
258
443
|
settings: ApiRouteDeps["settings"],
|
|
259
444
|
): Promise<ApiRouteDeps["settings"]> {
|
|
260
445
|
const encryptionKey = environmentsEncryptionKeyBytes(settings);
|
|
@@ -263,9 +448,19 @@ async function settingsWithSessionMcpServersForToolspace(
|
|
|
263
448
|
if (metadata.length === 0) {
|
|
264
449
|
return settings;
|
|
265
450
|
}
|
|
266
|
-
|
|
451
|
+
if (metadata.some((server) => server.headerNames.length > 0)) {
|
|
452
|
+
throw new Error(
|
|
453
|
+
"session MCP server credentials require OPENGENI_ENVIRONMENTS_ENCRYPTION_KEY",
|
|
454
|
+
);
|
|
455
|
+
}
|
|
267
456
|
}
|
|
268
|
-
const servers = await listSessionMcpServersForRun(
|
|
457
|
+
const servers = await listSessionMcpServersForRun(
|
|
458
|
+
deps.db,
|
|
459
|
+
workspaceId,
|
|
460
|
+
sessionId,
|
|
461
|
+
attemptId,
|
|
462
|
+
encryptionKey ?? null,
|
|
463
|
+
);
|
|
269
464
|
if (servers.length === 0) {
|
|
270
465
|
return settings;
|
|
271
466
|
}
|
|
@@ -284,6 +479,7 @@ async function settingsWithSessionMcpServersForToolspace(
|
|
|
284
479
|
...(server.requireApproval !== undefined
|
|
285
480
|
? { requireApproval: server.requireApproval }
|
|
286
481
|
: {}),
|
|
482
|
+
...(server.connectionRef ? { connectionRef: server.connectionRef } : {}),
|
|
287
483
|
headers: server.headers,
|
|
288
484
|
})),
|
|
289
485
|
],
|
|
@@ -295,10 +491,13 @@ async function connectToolspaceServer(input: {
|
|
|
295
491
|
grant: AccessGrant;
|
|
296
492
|
config: McpServerConfig;
|
|
297
493
|
sessionId: string;
|
|
494
|
+
rootSessionId: string;
|
|
495
|
+
turn: SessionTurn;
|
|
298
496
|
}): Promise<ConnectedToolspaceServer> {
|
|
497
|
+
const guardedFetch = guardedMcpFetch(input.deps.settings, undiciFetch);
|
|
299
498
|
const baseFetch: FetchLike = input.config.connectionRef
|
|
300
|
-
? connectionBrokerFetch(
|
|
301
|
-
:
|
|
499
|
+
? connectionBrokerFetch(guardedFetch, input)
|
|
500
|
+
: guardedFetch;
|
|
302
501
|
const client = new Client(
|
|
303
502
|
{ name: `opengeni-toolspace-${input.config.id}`, version: "1.0.0" },
|
|
304
503
|
{ capabilities: {} },
|
|
@@ -309,7 +508,12 @@ async function connectToolspaceServer(input: {
|
|
|
309
508
|
headers: toolspaceServerHeaders(input.config),
|
|
310
509
|
},
|
|
311
510
|
});
|
|
312
|
-
|
|
511
|
+
try {
|
|
512
|
+
await client.connect(transport as unknown as Transport, toolspaceRequestOptions(input.config));
|
|
513
|
+
} catch (error) {
|
|
514
|
+
await client.close().catch(() => undefined);
|
|
515
|
+
throw error;
|
|
516
|
+
}
|
|
313
517
|
return {
|
|
314
518
|
config: input.config,
|
|
315
519
|
client,
|
|
@@ -322,11 +526,13 @@ async function connectToolspaceServer(input: {
|
|
|
322
526
|
function toolspaceToolFor(input: {
|
|
323
527
|
deps: ApiRouteDeps;
|
|
324
528
|
grant: AccessGrant;
|
|
325
|
-
|
|
529
|
+
authority: ToolspaceAttemptAuthority;
|
|
530
|
+
rootSessionId: string;
|
|
326
531
|
entry: ToolListingEntry;
|
|
327
|
-
getRegistry: () => Promise<Map<string, McpServerConfig>>;
|
|
532
|
+
getRegistry: (attemptId: string) => Promise<Map<string, McpServerConfig>>;
|
|
328
533
|
}): ToolspaceRegisteredTool {
|
|
329
|
-
const { deps, grant,
|
|
534
|
+
const { deps, grant, authority, rootSessionId, entry, getRegistry } = input;
|
|
535
|
+
const { sessionId } = authority;
|
|
330
536
|
const { serverId, tool } = entry;
|
|
331
537
|
const name = prefixedMcpToolName(serverId, tool.name);
|
|
332
538
|
const approvalRequired = mcpToolRequiresApproval(entry.requireApproval, tool.name);
|
|
@@ -338,10 +544,7 @@ function toolspaceToolFor(input: {
|
|
|
338
544
|
...(description ? { description } : {}),
|
|
339
545
|
...(tool.inputSchema ? { inputSchema: tool.inputSchema } : {}),
|
|
340
546
|
call: async (args) => {
|
|
341
|
-
|
|
342
|
-
return mcpError(APPROVAL_REQUIRED_MESSAGE);
|
|
343
|
-
}
|
|
344
|
-
const reservation = await reserveActiveTurnCall(deps, grant.workspaceId, sessionId);
|
|
547
|
+
const reservation = await reserveExactAttemptCall(deps, grant, authority);
|
|
345
548
|
if (reservation.status === "no_active_turn") {
|
|
346
549
|
return mcpError(TOOLSPACE_NO_ACTIVE_TURN_MESSAGE);
|
|
347
550
|
}
|
|
@@ -350,11 +553,11 @@ function toolspaceToolFor(input: {
|
|
|
350
553
|
`toolspace call budget exhausted (${deps.settings.toolspaceMaxCallsPerTurn}/turn)`,
|
|
351
554
|
);
|
|
352
555
|
}
|
|
353
|
-
const turnId = reservation.
|
|
556
|
+
const turnId = reservation.turn.id;
|
|
354
557
|
// Dial only the ONE server this tool belongs to, from the freshly-built
|
|
355
|
-
// registry
|
|
356
|
-
//
|
|
357
|
-
const registry = await getRegistry();
|
|
558
|
+
// exact-attempt registry. Listing policy is descriptive only; a stale MCP
|
|
559
|
+
// surface can never decide authorization for a successor attempt.
|
|
560
|
+
const registry = await getRegistry(authority.attemptId);
|
|
358
561
|
const config = registry.get(serverId);
|
|
359
562
|
if (!config || !toolspaceCanProxyServer(config) || !allowedByConfig(config, tool.name)) {
|
|
360
563
|
return mcpError(`upstream tool failed: ${name}`);
|
|
@@ -362,47 +565,104 @@ function toolspaceToolFor(input: {
|
|
|
362
565
|
if (mcpToolRequiresApproval(config.requireApproval, tool.name)) {
|
|
363
566
|
return mcpError(APPROVAL_REQUIRED_MESSAGE);
|
|
364
567
|
}
|
|
365
|
-
const connection = await connectToolspaceServer({
|
|
366
|
-
|
|
367
|
-
|
|
568
|
+
const connection = await connectToolspaceServer({
|
|
569
|
+
deps,
|
|
570
|
+
grant,
|
|
571
|
+
config,
|
|
572
|
+
sessionId,
|
|
573
|
+
rootSessionId,
|
|
574
|
+
turn: reservation.turn,
|
|
575
|
+
}).catch(() => null);
|
|
368
576
|
if (!connection) {
|
|
369
577
|
return mcpError(`upstream tool failed: ${name}`);
|
|
370
578
|
}
|
|
371
579
|
try {
|
|
372
580
|
const callId = crypto.randomUUID();
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
581
|
+
const receipt = {
|
|
582
|
+
accountId: grant.accountId,
|
|
583
|
+
workspaceId: grant.workspaceId,
|
|
584
|
+
sessionId,
|
|
585
|
+
turnId,
|
|
586
|
+
executionGeneration: authority.executionGeneration,
|
|
587
|
+
attemptId: authority.attemptId,
|
|
588
|
+
callId,
|
|
589
|
+
};
|
|
590
|
+
const registered = await registerPendingSessionToolCall(deps.db, {
|
|
591
|
+
...receipt,
|
|
592
|
+
callType: "toolspace_call",
|
|
593
|
+
callItem: {
|
|
594
|
+
type: "toolspace_call",
|
|
595
|
+
id: callId,
|
|
596
|
+
name,
|
|
597
|
+
arguments: toolspaceAuditSummary(args),
|
|
598
|
+
serverId,
|
|
599
|
+
toolName: tool.name,
|
|
600
|
+
},
|
|
601
|
+
});
|
|
602
|
+
if (!registered.accepted || !registered.registered) {
|
|
603
|
+
return mcpError(TOOLSPACE_NO_ACTIVE_TURN_MESSAGE);
|
|
604
|
+
}
|
|
605
|
+
const created = await appendAndPublishTurnEventsFenced(
|
|
606
|
+
deps.db,
|
|
607
|
+
deps.bus,
|
|
608
|
+
grant.workspaceId,
|
|
609
|
+
sessionId,
|
|
610
|
+
turnId,
|
|
611
|
+
authority.executionGeneration,
|
|
612
|
+
authority.attemptId,
|
|
613
|
+
[
|
|
614
|
+
{
|
|
615
|
+
type: "agent.toolCall.created",
|
|
616
|
+
turnId,
|
|
617
|
+
turnGeneration: authority.executionGeneration,
|
|
618
|
+
turnAttemptId: authority.attemptId,
|
|
619
|
+
producerId: grant.subjectId,
|
|
620
|
+
payload: {
|
|
621
|
+
id: callId,
|
|
622
|
+
name,
|
|
623
|
+
arguments: args,
|
|
624
|
+
origin: "toolspace",
|
|
625
|
+
subjectId: grant.subjectId,
|
|
626
|
+
raw: {
|
|
627
|
+
type: "toolspace_call",
|
|
628
|
+
serverId,
|
|
629
|
+
toolName: tool.name,
|
|
630
|
+
},
|
|
388
631
|
},
|
|
389
632
|
},
|
|
390
|
-
|
|
391
|
-
|
|
633
|
+
],
|
|
634
|
+
);
|
|
635
|
+
if (!created.accepted) {
|
|
636
|
+
return mcpError(TOOLSPACE_NO_ACTIVE_TURN_MESSAGE);
|
|
637
|
+
}
|
|
392
638
|
const output = await callRemoteTool(deps, connection, tool.name, args);
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
639
|
+
const completed = await appendAndPublishTurnEventsFenced(
|
|
640
|
+
deps.db,
|
|
641
|
+
deps.bus,
|
|
642
|
+
grant.workspaceId,
|
|
643
|
+
sessionId,
|
|
644
|
+
turnId,
|
|
645
|
+
authority.executionGeneration,
|
|
646
|
+
authority.attemptId,
|
|
647
|
+
[
|
|
648
|
+
{
|
|
649
|
+
type: "agent.toolCall.output",
|
|
650
|
+
turnId,
|
|
651
|
+
turnGeneration: authority.executionGeneration,
|
|
652
|
+
turnAttemptId: authority.attemptId,
|
|
653
|
+
producerId: grant.subjectId,
|
|
654
|
+
payload: {
|
|
655
|
+
id: callId,
|
|
656
|
+
output: toolspaceAuditSummary(output),
|
|
657
|
+
origin: "toolspace",
|
|
658
|
+
subjectId: grant.subjectId,
|
|
659
|
+
},
|
|
403
660
|
},
|
|
404
|
-
|
|
405
|
-
|
|
661
|
+
],
|
|
662
|
+
);
|
|
663
|
+
if (completed.accepted) {
|
|
664
|
+
await clearPendingSessionToolspaceCall(deps.db, receipt);
|
|
665
|
+
}
|
|
406
666
|
return output;
|
|
407
667
|
} finally {
|
|
408
668
|
await connection.close();
|
|
@@ -418,7 +678,7 @@ async function callRemoteTool(
|
|
|
418
678
|
args: Record<string, unknown>,
|
|
419
679
|
): Promise<ToolspaceCallResult> {
|
|
420
680
|
try {
|
|
421
|
-
|
|
681
|
+
const output = (await server.client.callTool(
|
|
422
682
|
{
|
|
423
683
|
name: toolName,
|
|
424
684
|
arguments: args,
|
|
@@ -426,7 +686,17 @@ async function callRemoteTool(
|
|
|
426
686
|
undefined,
|
|
427
687
|
toolspaceRequestOptions(server.config),
|
|
428
688
|
)) as ToolspaceCallResult;
|
|
689
|
+
assertMcpPayloadWithinBytes(output, MCP_MAX_TOOL_RESULT_BYTES, "MCP tool result");
|
|
690
|
+
return output;
|
|
429
691
|
} catch (error) {
|
|
692
|
+
if (error instanceof McpPayloadTooLargeError) {
|
|
693
|
+
deps.observability?.warn("toolspace upstream tool result exceeded safety limit", {
|
|
694
|
+
serverId: server.config.id,
|
|
695
|
+
toolName,
|
|
696
|
+
errorClass: error.name,
|
|
697
|
+
});
|
|
698
|
+
return mcpError("upstream tool result exceeded the safety limit");
|
|
699
|
+
}
|
|
430
700
|
if (isToolspaceAuthNeededError(error)) {
|
|
431
701
|
return mcpError(TOOLSPACE_AUTH_NEEDED_MESSAGE);
|
|
432
702
|
}
|
|
@@ -442,30 +712,49 @@ async function callRemoteTool(
|
|
|
442
712
|
}
|
|
443
713
|
}
|
|
444
714
|
|
|
715
|
+
function toolspaceAuditSummary(value: unknown): {
|
|
716
|
+
redacted: true;
|
|
717
|
+
sizeBytes: number;
|
|
718
|
+
sha256: string;
|
|
719
|
+
} {
|
|
720
|
+
let serialized: string;
|
|
721
|
+
try {
|
|
722
|
+
serialized = JSON.stringify(value) ?? "null";
|
|
723
|
+
} catch {
|
|
724
|
+
serialized = "[unserializable]";
|
|
725
|
+
}
|
|
726
|
+
return {
|
|
727
|
+
redacted: true,
|
|
728
|
+
sizeBytes: Buffer.byteLength(serialized),
|
|
729
|
+
sha256: createHash("sha256").update(serialized).digest("hex"),
|
|
730
|
+
};
|
|
731
|
+
}
|
|
732
|
+
|
|
445
733
|
type ToolspaceReservation =
|
|
446
|
-
| { status: "ok";
|
|
734
|
+
| { status: "ok"; turn: SessionTurn }
|
|
447
735
|
| { status: "no_active_turn" }
|
|
448
736
|
| { status: "budget_exhausted" };
|
|
449
737
|
|
|
450
|
-
async function
|
|
738
|
+
async function reserveExactAttemptCall(
|
|
451
739
|
deps: ApiRouteDeps,
|
|
452
|
-
|
|
453
|
-
|
|
740
|
+
grant: AccessGrant,
|
|
741
|
+
authority: ToolspaceAttemptAuthority,
|
|
454
742
|
): Promise<ToolspaceReservation> {
|
|
455
|
-
const
|
|
456
|
-
|
|
457
|
-
|
|
743
|
+
const reservation = await reserveToolspaceCallForAttempt(deps.db, {
|
|
744
|
+
accountId: grant.accountId,
|
|
745
|
+
workspaceId: grant.workspaceId,
|
|
746
|
+
sessionId: authority.sessionId,
|
|
747
|
+
turnId: authority.turnId,
|
|
748
|
+
executionGeneration: authority.executionGeneration,
|
|
749
|
+
attemptId: authority.attemptId,
|
|
750
|
+
limit: deps.settings.toolspaceMaxCallsPerTurn,
|
|
751
|
+
});
|
|
752
|
+
if (!reservation.reserved) {
|
|
753
|
+
return reservation.reason === "budget_exhausted"
|
|
754
|
+
? { status: "budget_exhausted" }
|
|
755
|
+
: { status: "no_active_turn" };
|
|
458
756
|
}
|
|
459
|
-
|
|
460
|
-
deps.db,
|
|
461
|
-
workspaceId,
|
|
462
|
-
sessionId,
|
|
463
|
-
session.activeTurnId,
|
|
464
|
-
deps.settings.toolspaceMaxCallsPerTurn,
|
|
465
|
-
);
|
|
466
|
-
return reservation.reserved
|
|
467
|
-
? { status: "ok", turnId: session.activeTurnId }
|
|
468
|
-
: { status: "budget_exhausted" };
|
|
757
|
+
return { status: "ok", turn: reservation.turn };
|
|
469
758
|
}
|
|
470
759
|
|
|
471
760
|
function selectedMcpServerIds(tools: ToolRef[], sessionServerIds: string[]): Set<string> {
|
|
@@ -536,28 +825,49 @@ type McpRequestInfo = {
|
|
|
536
825
|
toolName?: string;
|
|
537
826
|
};
|
|
538
827
|
|
|
539
|
-
function
|
|
828
|
+
function mcpRequestDestinationUrl(input: string | URL | Request): string {
|
|
829
|
+
return new URL(input instanceof Request ? input.url : input.toString()).toString();
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
export function connectionBrokerFetch(
|
|
540
833
|
baseFetch: FetchLike,
|
|
541
834
|
input: {
|
|
542
835
|
deps: ApiRouteDeps;
|
|
543
836
|
grant: AccessGrant;
|
|
544
837
|
config: McpServerConfig;
|
|
545
838
|
sessionId: string;
|
|
839
|
+
rootSessionId: string;
|
|
840
|
+
turn: SessionTurn;
|
|
546
841
|
},
|
|
547
842
|
): FetchLike {
|
|
548
843
|
const connectionRef = input.config.connectionRef;
|
|
549
844
|
if (!connectionRef) {
|
|
550
845
|
return baseFetch;
|
|
551
846
|
}
|
|
552
|
-
const resolveCredential =
|
|
847
|
+
const resolveCredential = input.deps.connectionCredentials?.mcpCredentials
|
|
848
|
+
? buildHostConnectionTokenResolver(input.deps.connectionCredentials.mcpCredentials, {
|
|
849
|
+
accountId: input.grant.accountId,
|
|
850
|
+
workspaceId: input.grant.workspaceId,
|
|
851
|
+
sessionId: input.sessionId,
|
|
852
|
+
rootSessionId: input.rootSessionId,
|
|
853
|
+
turnId: input.turn.id,
|
|
854
|
+
attemptId: input.turn.activeAttemptId,
|
|
855
|
+
executionGeneration: input.turn.executionGeneration,
|
|
856
|
+
initiator: input.turn.initiator,
|
|
857
|
+
initiatorContext: input.turn.initiatorContext,
|
|
858
|
+
surface: "toolspace",
|
|
859
|
+
})
|
|
860
|
+
: buildConnectionTokenResolver(input.deps.db, input.deps.settings);
|
|
553
861
|
return async (requestInput, init) => {
|
|
554
862
|
const request = await mcpRequestInfo(requestInput, init);
|
|
863
|
+
const destinationUrl = mcpRequestDestinationUrl(requestInput);
|
|
555
864
|
const first = await resolveCredential({
|
|
556
865
|
workspaceId: input.grant.workspaceId,
|
|
557
866
|
serverId: input.config.id,
|
|
558
867
|
connectionRef,
|
|
868
|
+
destinationUrl,
|
|
559
869
|
forceRefresh: false,
|
|
560
|
-
...(request.toolName ? {
|
|
870
|
+
...(request.toolName ? { toolName: request.toolName } : {}),
|
|
561
871
|
subjectId: input.grant.subjectId,
|
|
562
872
|
});
|
|
563
873
|
if (first.status === "auth_needed") {
|
|
@@ -568,23 +878,43 @@ function connectionBrokerFetch(
|
|
|
568
878
|
withConnectionHeaders(requestInput, init, first.headers),
|
|
569
879
|
);
|
|
570
880
|
if (response.status === 401) {
|
|
881
|
+
await cancelMcpResponseBody(response);
|
|
571
882
|
const refreshed = await resolveCredential({
|
|
572
883
|
workspaceId: input.grant.workspaceId,
|
|
573
884
|
serverId: input.config.id,
|
|
574
885
|
connectionRef,
|
|
886
|
+
destinationUrl,
|
|
575
887
|
forceRefresh: true,
|
|
576
|
-
...(request.toolName ? {
|
|
888
|
+
...(request.toolName ? { toolName: request.toolName } : {}),
|
|
577
889
|
subjectId: input.grant.subjectId,
|
|
578
890
|
});
|
|
579
891
|
if (refreshed.status === "auth_needed") {
|
|
580
892
|
return await authNeededFetchResponse(input, request, refreshed);
|
|
581
893
|
}
|
|
582
|
-
|
|
894
|
+
const retry = await baseFetch(
|
|
583
895
|
fetchInputForAttempt(requestInput),
|
|
584
896
|
withConnectionHeaders(requestInput, init, refreshed.headers),
|
|
585
897
|
);
|
|
898
|
+
if (retry.status === 401) {
|
|
899
|
+
await cancelMcpResponseBody(retry);
|
|
900
|
+
return await authNeededFetchResponse(
|
|
901
|
+
input,
|
|
902
|
+
request,
|
|
903
|
+
authNeededFromStatus(input.config, refreshed, "expired"),
|
|
904
|
+
);
|
|
905
|
+
}
|
|
906
|
+
if (retry.status === 403) {
|
|
907
|
+
await cancelMcpResponseBody(retry);
|
|
908
|
+
return await authNeededFetchResponse(
|
|
909
|
+
input,
|
|
910
|
+
request,
|
|
911
|
+
authNeededFromStatus(input.config, refreshed, "insufficient_scope"),
|
|
912
|
+
);
|
|
913
|
+
}
|
|
914
|
+
return retry;
|
|
586
915
|
}
|
|
587
916
|
if (response.status === 403) {
|
|
917
|
+
await cancelMcpResponseBody(response);
|
|
588
918
|
return await authNeededFetchResponse(
|
|
589
919
|
input,
|
|
590
920
|
request,
|
|
@@ -605,9 +935,13 @@ function authNeededFromStatus(
|
|
|
605
935
|
status: "auth_needed",
|
|
606
936
|
reason,
|
|
607
937
|
providerDomain: connectionRef.providerDomain,
|
|
938
|
+
...(connectionRef.provider ? { provider: connectionRef.provider } : {}),
|
|
608
939
|
connectionId: first.connectionId,
|
|
609
940
|
...(connectionRef.scopes ? { scopes: connectionRef.scopes } : {}),
|
|
610
941
|
...(connectionRef.resource ? { resource: connectionRef.resource } : {}),
|
|
942
|
+
...(connectionRef.selectedResources
|
|
943
|
+
? { selectedResources: connectionRef.selectedResources }
|
|
944
|
+
: {}),
|
|
611
945
|
};
|
|
612
946
|
}
|
|
613
947
|
|
|
@@ -617,6 +951,7 @@ async function authNeededFetchResponse(
|
|
|
617
951
|
grant: AccessGrant;
|
|
618
952
|
config: McpServerConfig;
|
|
619
953
|
sessionId: string;
|
|
954
|
+
turn: SessionTurn;
|
|
620
955
|
},
|
|
621
956
|
request: McpRequestInfo,
|
|
622
957
|
auth: Extract<ResolveConnectionCredentialResult, { status: "auth_needed" }>,
|
|
@@ -634,10 +969,12 @@ async function authNeededFetchResponse(
|
|
|
634
969
|
serverId: input.config.id,
|
|
635
970
|
toolName: request.toolName ?? null,
|
|
636
971
|
providerDomain: auth.providerDomain,
|
|
972
|
+
...(auth.provider ? { provider: auth.provider } : {}),
|
|
637
973
|
reason: auth.reason,
|
|
638
974
|
...(auth.connectionId ? { connectionId: auth.connectionId } : {}),
|
|
639
975
|
...(auth.scopes ? { scopes: auth.scopes } : {}),
|
|
640
976
|
...(auth.resource ? { resource: auth.resource } : {}),
|
|
977
|
+
...(auth.selectedResources ? { selectedResources: auth.selectedResources } : {}),
|
|
641
978
|
...(auth.authorizationUrl ? { authorizationUrl: auth.authorizationUrl } : {}),
|
|
642
979
|
subjectId: input.grant.subjectId,
|
|
643
980
|
},
|
|
@@ -663,8 +1000,19 @@ async function authNeededFetchResponse(
|
|
|
663
1000
|
return new Response("Authentication required for MCP server connection", { status: 401 });
|
|
664
1001
|
}
|
|
665
1002
|
|
|
666
|
-
async function mcpRequestInfo(
|
|
667
|
-
|
|
1003
|
+
async function mcpRequestInfo(
|
|
1004
|
+
input: string | URL | Request,
|
|
1005
|
+
init?: RequestInit,
|
|
1006
|
+
): Promise<McpRequestInfo> {
|
|
1007
|
+
const body =
|
|
1008
|
+
typeof init?.body === "string"
|
|
1009
|
+
? init.body
|
|
1010
|
+
: input instanceof Request && (init?.method ?? input.method).toUpperCase() === "POST"
|
|
1011
|
+
? await input
|
|
1012
|
+
.clone()
|
|
1013
|
+
.text()
|
|
1014
|
+
.catch(() => "")
|
|
1015
|
+
: "";
|
|
668
1016
|
if (!body) {
|
|
669
1017
|
return {};
|
|
670
1018
|
}
|
|
@@ -694,19 +1042,21 @@ async function mcpRequestInfo(_input: string | URL, init?: RequestInit): Promise
|
|
|
694
1042
|
}
|
|
695
1043
|
|
|
696
1044
|
function withConnectionHeaders(
|
|
697
|
-
|
|
1045
|
+
input: string | URL | Request,
|
|
698
1046
|
init: RequestInit | undefined,
|
|
699
1047
|
authHeaders: Record<string, string>,
|
|
700
1048
|
): RequestInit {
|
|
701
|
-
const headers = new Headers(
|
|
1049
|
+
const headers = new Headers(
|
|
1050
|
+
init?.headers ?? (input instanceof Request ? input.headers : undefined),
|
|
1051
|
+
);
|
|
702
1052
|
for (const [name, value] of Object.entries(authHeaders)) {
|
|
703
1053
|
headers.set(name, value);
|
|
704
1054
|
}
|
|
705
1055
|
return { ...init, headers };
|
|
706
1056
|
}
|
|
707
1057
|
|
|
708
|
-
function fetchInputForAttempt(input: string | URL): string | URL {
|
|
709
|
-
return input;
|
|
1058
|
+
function fetchInputForAttempt(input: string | URL | Request): string | URL | Request {
|
|
1059
|
+
return input instanceof Request ? input.clone() : input;
|
|
710
1060
|
}
|
|
711
1061
|
|
|
712
1062
|
function isToolspaceAuthNeededError(error: unknown): boolean {
|