@opengeni/api-router 0.23.1 → 0.30.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 +3 -2
- package/dist/app.js +5 -3
- package/dist/auth/canonical-human-session-admission.d.ts +27 -0
- package/dist/browser-auth-broker.d.ts +20 -0
- package/dist/browser-controller-authority.d.ts +61 -0
- package/dist/browser-network-route.d.ts +8 -0
- package/dist/browser-state-authority.d.ts +27 -0
- package/dist/{chunk-T4T2PGU4.js → chunk-2PQMSRCB.js} +37891 -17463
- package/dist/chunk-2PQMSRCB.js.map +1 -0
- package/dist/codemode.d.ts +23 -0
- package/dist/controller-data-plane.d.ts +12 -0
- package/dist/editable-artifact-live-hints.d.ts +11 -0
- package/dist/editable-artifact-native-kernel.d.ts +37 -0
- package/dist/editable-artifact-office-import.d.ts +22 -0
- package/dist/editable-artifact-production.d.ts +29 -0
- package/dist/editable-artifact-websocket.d.ts +49 -0
- package/dist/editable-artifact-workspace-files.d.ts +23 -0
- package/dist/github-browser-flow.d.ts +6 -0
- package/dist/http/auth.d.ts +1 -1
- package/dist/http/cors.d.ts +1 -0
- package/dist/http/sse.d.ts +18 -1
- package/dist/index.d.ts +3 -2
- package/dist/index.js +1796 -22
- package/dist/index.js.map +1 -1
- package/dist/integrations/api-integrations.d.ts +24 -0
- package/dist/integrations/atlassian.d.ts +176 -0
- package/dist/integrations/fiken.d.ts +233 -0
- package/dist/integrations/github-skill-source.d.ts +5 -0
- package/dist/integrations/google-drive.d.ts +85 -0
- package/dist/integrations/oauth-client.d.ts +22 -1
- package/dist/integrations/provider-oauth.d.ts +17 -0
- package/dist/integrations/slack-bot.d.ts +175 -1
- package/dist/integrations/slack-interactions.d.ts +24 -6
- package/dist/integrations/social-api.d.ts +2 -1
- package/dist/interaction-holder-heartbeat.d.ts +17 -0
- package/dist/interaction-metrics.d.ts +11 -0
- package/dist/mcp/editable-artifact-query-schema.d.ts +4 -0
- package/dist/mcp/editable-artifacts.d.ts +13 -0
- package/dist/mcp/scheduled-task-view.d.ts +168 -0
- package/dist/mcp/server.d.ts +16 -4
- package/dist/memory-slack-delivery.d.ts +9 -0
- package/dist/model-catalog.d.ts +1 -0
- package/dist/routes/api-integrations.d.ts +8 -0
- package/dist/routes/browser-identities.d.ts +5 -0
- package/dist/routes/browser-sessions.d.ts +6 -0
- package/dist/routes/canonical-human-identities.d.ts +3 -0
- package/dist/routes/channels.d.ts +3 -0
- package/dist/routes/company-profile.d.ts +3 -0
- package/dist/routes/computer-sessions.d.ts +6 -0
- package/dist/routes/editable-artifacts.d.ts +44 -0
- package/dist/routes/integration-facets.d.ts +3 -0
- package/dist/routes/interaction-resources.d.ts +5 -0
- package/dist/routes/memory-slack-publications.d.ts +6 -0
- package/dist/routes/organization-memberships.d.ts +3 -0
- package/dist/routes/plugins.d.ts +8 -0
- package/dist/routes/sessions.d.ts +18 -2
- package/dist/routes/skills.d.ts +6 -0
- package/dist/routes/slack-task-policy.d.ts +3 -0
- package/dist/routes/supergrok.d.ts +3 -0
- package/dist/routes/video-generation.d.ts +3 -0
- package/dist/sandbox/channel-a.d.ts +62 -2
- package/dist/sandbox/machines.d.ts +2 -2
- package/dist/sandbox/metrics-ingestion.d.ts +6 -1
- package/dist/sandbox/viewer.d.ts +7 -3
- package/dist/slack-reaction-files.d.ts +27 -0
- package/dist/temporal-schedule-cleanup.d.ts +26 -0
- package/dist/transcription/providers/xai-subscription.d.ts +8 -0
- package/dist/xai-realtime.d.ts +26 -0
- package/dist/xai-subscription-auth.d.ts +23 -0
- package/package.json +20 -14
- package/src/app.ts +604 -64
- package/src/auth/canonical-human-session-admission.ts +116 -0
- package/src/auth/managed-auth.ts +101 -0
- package/src/browser-auth-broker.ts +155 -0
- package/src/browser-controller-authority.ts +186 -0
- package/src/browser-network-route.ts +34 -0
- package/src/browser-state-authority.ts +236 -0
- package/src/codemode.ts +186 -0
- package/src/controller-data-plane.ts +35 -0
- package/src/editable-artifact-live-hints.ts +64 -0
- package/src/editable-artifact-native-kernel.ts +659 -0
- package/src/editable-artifact-office-import.ts +230 -0
- package/src/editable-artifact-production.ts +419 -0
- package/src/editable-artifact-websocket.ts +311 -0
- package/src/editable-artifact-workspace-files.ts +186 -0
- package/src/github-browser-flow.ts +35 -6
- package/src/http/auth.ts +29 -9
- package/src/http/cors.ts +3 -0
- package/src/http/sse.ts +229 -5
- package/src/index.ts +119 -20
- package/src/integrations/api-integrations.ts +365 -0
- package/src/integrations/atlassian.ts +1621 -0
- package/src/integrations/fiken.ts +1230 -0
- package/src/integrations/github-skill-source.ts +142 -0
- package/src/integrations/google-drive.ts +1005 -64
- package/src/integrations/oauth-client.ts +123 -33
- package/src/integrations/provider-oauth.ts +756 -0
- package/src/integrations/slack-bot.ts +669 -11
- package/src/integrations/slack-interactions.ts +2063 -93
- package/src/integrations/social-api.ts +11 -0
- package/src/interaction-holder-heartbeat.ts +95 -0
- package/src/interaction-metrics.ts +159 -0
- package/src/mcp/documents.ts +146 -6
- package/src/mcp/editable-artifact-query-schema.ts +236 -0
- package/src/mcp/editable-artifacts.ts +448 -0
- package/src/mcp/scheduled-task-view.ts +34 -0
- package/src/mcp/server.ts +1756 -355
- package/src/memory-slack-delivery.ts +209 -0
- package/src/model-catalog.ts +19 -6
- package/src/routes/api-integrations.ts +410 -0
- package/src/routes/browser-identities.ts +160 -0
- package/src/routes/browser-sessions.ts +4482 -0
- package/src/routes/canonical-human-identities.ts +156 -0
- package/src/routes/channels.ts +90 -0
- package/src/routes/company-profile.ts +255 -0
- package/src/routes/computer-sessions.ts +1602 -0
- package/src/routes/connections.ts +421 -99
- package/src/routes/documents.ts +22 -3
- package/src/routes/editable-artifacts.ts +1159 -0
- package/src/routes/files.ts +106 -4
- package/src/routes/github.ts +18 -2
- package/src/routes/install.ts +7 -1
- package/src/routes/integration-facets.ts +258 -0
- package/src/routes/interaction-resources.ts +595 -0
- package/src/routes/memory-slack-publications.ts +216 -0
- package/src/routes/organization-memberships.ts +53 -0
- package/src/routes/packs.ts +437 -7
- package/src/routes/plugins.ts +752 -0
- package/src/routes/rigs.ts +77 -20
- package/src/routes/scheduled-tasks.ts +67 -40
- package/src/routes/sessions.ts +830 -278
- package/src/routes/skills.ts +256 -0
- package/src/routes/slack-task-policy.ts +115 -0
- package/src/routes/supergrok.ts +717 -0
- package/src/routes/transcription-recordings.ts +9 -2
- package/src/routes/transcriptions.ts +2 -1
- package/src/routes/video-generation.ts +162 -0
- package/src/routes/workspaces.ts +68 -28
- package/src/sandbox/channel-a.ts +750 -116
- package/src/sandbox/machines.ts +5 -5
- package/src/sandbox/metrics-ingestion.ts +121 -3
- package/src/sandbox/rematerialize.ts +35 -47
- package/src/sandbox/viewer.ts +39 -12
- package/src/slack-reaction-files.ts +181 -0
- package/src/temporal-schedule-cleanup.ts +135 -0
- package/src/transcription/providers/xai-subscription.ts +110 -0
- package/src/transcription/service.ts +17 -2
- package/src/xai-realtime.ts +216 -0
- package/src/xai-subscription-auth.ts +132 -0
- package/dist/chunk-T4T2PGU4.js.map +0 -1
- package/dist/mcp/toolspace.d.ts +0 -71
- package/src/mcp/toolspace.ts +0 -1190
package/src/mcp/toolspace.ts
DELETED
|
@@ -1,1190 +0,0 @@
|
|
|
1
|
-
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
2
|
-
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
|
|
3
|
-
import type { Transport } from "@modelcontextprotocol/sdk/shared/transport.js";
|
|
4
|
-
import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
|
|
5
|
-
import { environmentsEncryptionKeyBytes, type McpServerConfig } from "@opengeni/config";
|
|
6
|
-
import {
|
|
7
|
-
prefixedMcpToolName,
|
|
8
|
-
type AccessGrant,
|
|
9
|
-
type McpPersonalConnectionDelegation,
|
|
10
|
-
type SessionTurn,
|
|
11
|
-
type ToolRef,
|
|
12
|
-
} from "@opengeni/contracts";
|
|
13
|
-
import {
|
|
14
|
-
hasPermission,
|
|
15
|
-
withFrozenPersonalConnectionDelegations,
|
|
16
|
-
settingsWithEnabledCapabilityMcpServers,
|
|
17
|
-
type ApiRouteDeps,
|
|
18
|
-
} from "@opengeni/core";
|
|
19
|
-
import {
|
|
20
|
-
buildConnectionTokenResolver,
|
|
21
|
-
buildHostConnectionTokenResolver,
|
|
22
|
-
clearPendingSessionToolspaceCall,
|
|
23
|
-
getActiveSessionTurnForExecution,
|
|
24
|
-
getSessionRootId,
|
|
25
|
-
getWorkspaceGrant,
|
|
26
|
-
listSessionMcpServerMetadata,
|
|
27
|
-
listSessionMcpServersForRun,
|
|
28
|
-
registerPendingSessionToolCall,
|
|
29
|
-
requireSession,
|
|
30
|
-
reserveToolspaceCallForAttempt,
|
|
31
|
-
type ResolveConnectionCredentialResult,
|
|
32
|
-
} from "@opengeni/db";
|
|
33
|
-
import { appendAndPublishEvents, appendAndPublishTurnEventsFenced } from "@opengeni/events";
|
|
34
|
-
import { undiciFetch, type FetchLike } from "@opengeni/network";
|
|
35
|
-
import {
|
|
36
|
-
MCP_MAX_AGGREGATE_TOOL_LIST_BYTES,
|
|
37
|
-
MCP_MAX_AGGREGATE_TOOL_LIST_ENTRIES,
|
|
38
|
-
MCP_MAX_CONCURRENT_SERVER_OPERATIONS,
|
|
39
|
-
MCP_MAX_TOOL_RESULT_BYTES,
|
|
40
|
-
McpAggregateToolListBudget,
|
|
41
|
-
McpPayloadTooLargeError,
|
|
42
|
-
assertMcpPayloadWithinBytes,
|
|
43
|
-
assertMcpServerSelectionWithinBounds,
|
|
44
|
-
assertMcpToolListWithinBounds,
|
|
45
|
-
boundedParallelMap,
|
|
46
|
-
cancelMcpResponseBody,
|
|
47
|
-
guardedMcpFetch,
|
|
48
|
-
mcpJsonRpcErrorPayloadForRequest,
|
|
49
|
-
mcpRequestReplayInfo,
|
|
50
|
-
mcpSerializedSizeBytes,
|
|
51
|
-
type McpRequestReplayInfo,
|
|
52
|
-
} from "@opengeni/runtime/mcp-network";
|
|
53
|
-
import { Buffer } from "node:buffer";
|
|
54
|
-
|
|
55
|
-
export type ToolspaceCallResult = CallToolResult;
|
|
56
|
-
|
|
57
|
-
export type ToolspaceRegisteredTool = {
|
|
58
|
-
name: string;
|
|
59
|
-
description?: string;
|
|
60
|
-
inputSchema?: Record<string, unknown>;
|
|
61
|
-
call: (args: Record<string, unknown>) => Promise<ToolspaceCallResult>;
|
|
62
|
-
};
|
|
63
|
-
|
|
64
|
-
export type ToolspaceMcpSurface = {
|
|
65
|
-
sessionId: string;
|
|
66
|
-
subjectId: string;
|
|
67
|
-
tools: ToolspaceRegisteredTool[];
|
|
68
|
-
close: () => Promise<void>;
|
|
69
|
-
};
|
|
70
|
-
|
|
71
|
-
type ConnectedToolspaceServer = {
|
|
72
|
-
config: McpServerConfig;
|
|
73
|
-
client: Client;
|
|
74
|
-
close: () => Promise<void>;
|
|
75
|
-
};
|
|
76
|
-
|
|
77
|
-
type McpTool = {
|
|
78
|
-
name: string;
|
|
79
|
-
description?: string;
|
|
80
|
-
inputSchema?: Record<string, unknown>;
|
|
81
|
-
};
|
|
82
|
-
|
|
83
|
-
const APPROVAL_REQUIRED_MESSAGE = "requires approval - invoke via the agent";
|
|
84
|
-
const TOOLSPACE_AUTH_NEEDED_ERROR = {
|
|
85
|
-
// OpenGeni application-defined JSON-RPC code. Keep this positive so it cannot
|
|
86
|
-
// collide with MCP SDK transport errors such as RequestTimeout (-32001).
|
|
87
|
-
code: 40_101,
|
|
88
|
-
message: "Authentication required - a connection link was posted to the session.",
|
|
89
|
-
} as const;
|
|
90
|
-
const TOOLSPACE_TOOL_OUTCOME_UNCERTAIN_ERROR = {
|
|
91
|
-
code: 40_102,
|
|
92
|
-
message:
|
|
93
|
-
"Tool outcome uncertain: the provider returned 401 after receiving the request. OpenGeni did not replay this call. Do not retry automatically; verify provider state before any new attempt.",
|
|
94
|
-
} as const;
|
|
95
|
-
const TOOLSPACE_NO_ACTIVE_TURN_MESSAGE =
|
|
96
|
-
"no active turn - toolspace calls require an in-flight turn";
|
|
97
|
-
// First-party OpenGeni MCP proxies (files/docs) route back through the same
|
|
98
|
-
// /mcp mount. They are excluded from the toolspace surface by construction so a
|
|
99
|
-
// toolspace principal can never re-enter /mcp as a first-party caller, even if
|
|
100
|
-
// a future grant carried files:read / documents:search (see docs invariants).
|
|
101
|
-
// Codex Apps is also excluded: its dynamic designated-owner authorization and
|
|
102
|
-
// wire sanitizer live on the model MCP path and must never degrade into static
|
|
103
|
-
// Toolspace headers.
|
|
104
|
-
const FIRST_PARTY_PROXY_IDS = new Set(["files", "docs", "codex_apps"]);
|
|
105
|
-
// In-process cache of the per-session upstream tool listing. Keyed on the set of
|
|
106
|
-
// proxyable server ids + their credential versions, so a credential rotation
|
|
107
|
-
// busts the entry; a short TTL bounds staleness for everything else. This is
|
|
108
|
-
// what keeps list-type /mcp requests (initialize, tools/list) from fanning out
|
|
109
|
-
// to every upstream on every call.
|
|
110
|
-
const TOOLSPACE_TOOL_LIST_TTL_MS = 30_000;
|
|
111
|
-
const TOOLSPACE_TOOL_LIST_CACHE_MAX_ENTRIES = 2_000;
|
|
112
|
-
const TOOLSPACE_TOOL_LIST_CACHE_MAX_BYTES = 64 * 1024 * 1024;
|
|
113
|
-
|
|
114
|
-
export type ToolListingEntry = {
|
|
115
|
-
serverId: string;
|
|
116
|
-
tool: McpTool;
|
|
117
|
-
requireApproval: McpServerConfig["requireApproval"];
|
|
118
|
-
};
|
|
119
|
-
|
|
120
|
-
type ToolListCacheValue = {
|
|
121
|
-
expiresAt: number;
|
|
122
|
-
entries: ToolListingEntry[];
|
|
123
|
-
sizeBytes: number;
|
|
124
|
-
};
|
|
125
|
-
|
|
126
|
-
/** Deterministic LRU bounded by both key count and serialized retained bytes. */
|
|
127
|
-
export class ToolspaceToolListCache {
|
|
128
|
-
private readonly values = new Map<string, ToolListCacheValue>();
|
|
129
|
-
private retainedBytes = 0;
|
|
130
|
-
|
|
131
|
-
constructor(
|
|
132
|
-
private readonly maxEntries = TOOLSPACE_TOOL_LIST_CACHE_MAX_ENTRIES,
|
|
133
|
-
private readonly maxBytes = TOOLSPACE_TOOL_LIST_CACHE_MAX_BYTES,
|
|
134
|
-
private readonly ttlMs = TOOLSPACE_TOOL_LIST_TTL_MS,
|
|
135
|
-
) {
|
|
136
|
-
if (maxEntries < 1 || maxBytes < 1 || ttlMs < 1) {
|
|
137
|
-
throw new Error("toolspace cache limits must be positive");
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
read(key: string, now = Date.now()): ToolListingEntry[] | null {
|
|
142
|
-
const hit = this.values.get(key);
|
|
143
|
-
if (!hit) return null;
|
|
144
|
-
if (hit.expiresAt <= now) {
|
|
145
|
-
this.delete(key);
|
|
146
|
-
return null;
|
|
147
|
-
}
|
|
148
|
-
this.values.delete(key);
|
|
149
|
-
this.values.set(key, hit);
|
|
150
|
-
return hit.entries;
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
write(key: string, entries: ToolListingEntry[], now = Date.now()): boolean {
|
|
154
|
-
const sizeBytes = Buffer.byteLength(key) + mcpSerializedSizeBytes(entries);
|
|
155
|
-
if (sizeBytes > this.maxBytes) return false;
|
|
156
|
-
// A rejected replacement is a no-op: preserve the current safe value
|
|
157
|
-
// until the candidate has passed its own size check.
|
|
158
|
-
this.delete(key);
|
|
159
|
-
for (const [existingKey, value] of this.values) {
|
|
160
|
-
if (value.expiresAt <= now) this.delete(existingKey);
|
|
161
|
-
}
|
|
162
|
-
while (this.values.size >= this.maxEntries || this.retainedBytes + sizeBytes > this.maxBytes) {
|
|
163
|
-
const oldestKey = this.values.keys().next().value as string | undefined;
|
|
164
|
-
if (oldestKey === undefined) break;
|
|
165
|
-
this.delete(oldestKey);
|
|
166
|
-
}
|
|
167
|
-
this.values.set(key, { expiresAt: now + this.ttlMs, entries, sizeBytes });
|
|
168
|
-
this.retainedBytes += sizeBytes;
|
|
169
|
-
return true;
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
clear(): void {
|
|
173
|
-
this.values.clear();
|
|
174
|
-
this.retainedBytes = 0;
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
snapshot(): { entries: number; bytes: number; keys: string[] } {
|
|
178
|
-
return {
|
|
179
|
-
entries: this.values.size,
|
|
180
|
-
bytes: this.retainedBytes,
|
|
181
|
-
keys: [...this.values.keys()],
|
|
182
|
-
};
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
private delete(key: string): void {
|
|
186
|
-
const existing = this.values.get(key);
|
|
187
|
-
if (!existing) return;
|
|
188
|
-
this.values.delete(key);
|
|
189
|
-
this.retainedBytes -= existing.sizeBytes;
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
const toolListCache = new ToolspaceToolListCache();
|
|
194
|
-
|
|
195
|
-
type ToolspaceAuthority = {
|
|
196
|
-
sessionId: string;
|
|
197
|
-
};
|
|
198
|
-
|
|
199
|
-
type ToolspaceAttemptAuthority = ToolspaceAuthority & {
|
|
200
|
-
turnId: string;
|
|
201
|
-
attemptId: string;
|
|
202
|
-
executionGeneration: number;
|
|
203
|
-
};
|
|
204
|
-
|
|
205
|
-
function toolspaceAuthorityForGrant(grant: AccessGrant): ToolspaceAuthority | null {
|
|
206
|
-
const sessionId = grant.metadata?.sessionId;
|
|
207
|
-
return typeof sessionId === "string" ? { sessionId } : null;
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
export function isToolspaceGrant(settings: ApiRouteDeps["settings"], grant: AccessGrant): boolean {
|
|
211
|
-
return (
|
|
212
|
-
settings.toolspaceEnabled &&
|
|
213
|
-
hasPermission(grant.permissions, "toolspace:call") &&
|
|
214
|
-
toolspaceAuthorityForGrant(grant) !== null
|
|
215
|
-
);
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
export async function prepareToolspaceMcpSurface(input: {
|
|
219
|
-
deps: ApiRouteDeps;
|
|
220
|
-
grant: AccessGrant;
|
|
221
|
-
}): Promise<ToolspaceMcpSurface | null> {
|
|
222
|
-
const { deps, grant } = input;
|
|
223
|
-
if (!isToolspaceGrant(deps.settings, grant)) {
|
|
224
|
-
return null;
|
|
225
|
-
}
|
|
226
|
-
const authority = toolspaceAuthorityForGrant(grant);
|
|
227
|
-
if (!authority) {
|
|
228
|
-
return null;
|
|
229
|
-
}
|
|
230
|
-
const { sessionId } = authority;
|
|
231
|
-
const activeTurn = await getActiveSessionTurnForExecution(deps.db, grant.workspaceId, sessionId);
|
|
232
|
-
// Recovering/waiting-capacity attempts retain ownership pointers, but they
|
|
233
|
-
// are not currently executing model code. Do not even enumerate upstream
|
|
234
|
-
// tools until the turn has returned to the running state.
|
|
235
|
-
if (!activeTurn?.activeAttemptId || activeTurn.status !== "running") {
|
|
236
|
-
return emptyToolspaceSurface(sessionId, grant.subjectId);
|
|
237
|
-
}
|
|
238
|
-
const attemptAuthority: ToolspaceAttemptAuthority = {
|
|
239
|
-
sessionId,
|
|
240
|
-
turnId: activeTurn.id,
|
|
241
|
-
attemptId: activeTurn.activeAttemptId,
|
|
242
|
-
executionGeneration: activeTurn.executionGeneration,
|
|
243
|
-
};
|
|
244
|
-
const session = await requireSession(deps.db, grant.workspaceId, sessionId);
|
|
245
|
-
const personalConnectionDelegations = activeTurn.personalConnectionDelegations;
|
|
246
|
-
let rootSessionId = sessionId;
|
|
247
|
-
if (deps.connectionCredentials?.mcpCredentials) {
|
|
248
|
-
const resolvedRootSessionId = await getSessionRootId(deps.db, grant.workspaceId, sessionId);
|
|
249
|
-
if (!resolvedRootSessionId) {
|
|
250
|
-
throw new Error(`cannot resolve host MCP credentials for missing session ${sessionId}`);
|
|
251
|
-
}
|
|
252
|
-
rootSessionId = resolvedRootSessionId;
|
|
253
|
-
}
|
|
254
|
-
const selectedIds = selectedMcpServerIds(
|
|
255
|
-
session.tools,
|
|
256
|
-
session.mcpServers.map((server) => server.id),
|
|
257
|
-
);
|
|
258
|
-
// Proxyable ids: everything selected except the first-party OpenGeni tool
|
|
259
|
-
// server and the first-party MCP proxies, both of which would re-enter /mcp.
|
|
260
|
-
const proxyableIds = [...selectedIds].filter((id) => toolspaceCanProxyServerId(id));
|
|
261
|
-
assertMcpServerSelectionWithinBounds(proxyableIds);
|
|
262
|
-
if (proxyableIds.length === 0) {
|
|
263
|
-
return emptyToolspaceSurface(sessionId, grant.subjectId);
|
|
264
|
-
}
|
|
265
|
-
// The registry (decrypted session servers + capability/pack expansion) is a
|
|
266
|
-
// handful of DB reads with no upstream dials. Build it at most once per
|
|
267
|
-
// request, and only when we actually need it (a cache-miss listing or a real
|
|
268
|
-
// tools/call), so a cache-hit request does no registry work.
|
|
269
|
-
const registryPromises = new Map<string, Promise<Map<string, McpServerConfig>>>();
|
|
270
|
-
const getRegistry = (attemptId: string) => {
|
|
271
|
-
const existing = registryPromises.get(attemptId);
|
|
272
|
-
if (existing) {
|
|
273
|
-
return existing;
|
|
274
|
-
}
|
|
275
|
-
const created = buildToolspaceRegistry(deps, grant.workspaceId, sessionId, attemptId);
|
|
276
|
-
registryPromises.set(attemptId, created);
|
|
277
|
-
return created;
|
|
278
|
-
};
|
|
279
|
-
|
|
280
|
-
const listing = await resolveToolListing({
|
|
281
|
-
deps,
|
|
282
|
-
grant,
|
|
283
|
-
sessionId,
|
|
284
|
-
rootSessionId,
|
|
285
|
-
proxyableIds,
|
|
286
|
-
activeTurn,
|
|
287
|
-
personalConnectionDelegations,
|
|
288
|
-
getRegistry: () => getRegistry(attemptAuthority.attemptId),
|
|
289
|
-
});
|
|
290
|
-
const tools = listing.map((entry) =>
|
|
291
|
-
toolspaceToolFor({
|
|
292
|
-
deps,
|
|
293
|
-
grant,
|
|
294
|
-
authority: attemptAuthority,
|
|
295
|
-
rootSessionId,
|
|
296
|
-
entry,
|
|
297
|
-
personalConnectionDelegations,
|
|
298
|
-
getRegistry,
|
|
299
|
-
}),
|
|
300
|
-
);
|
|
301
|
-
|
|
302
|
-
return {
|
|
303
|
-
sessionId,
|
|
304
|
-
subjectId: grant.subjectId,
|
|
305
|
-
tools,
|
|
306
|
-
// Connections are opened lazily and closed inline (per listing pass, per
|
|
307
|
-
// call), so there is nothing persistent to tear down here.
|
|
308
|
-
close: async () => {},
|
|
309
|
-
};
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
function emptyToolspaceSurface(sessionId: string, subjectId: string): ToolspaceMcpSurface {
|
|
313
|
-
return { sessionId, subjectId, tools: [], close: async () => {} };
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
async function buildToolspaceRegistry(
|
|
317
|
-
deps: ApiRouteDeps,
|
|
318
|
-
workspaceId: string,
|
|
319
|
-
sessionId: string,
|
|
320
|
-
attemptId: string,
|
|
321
|
-
): Promise<Map<string, McpServerConfig>> {
|
|
322
|
-
const runtimeSettings = await settingsWithEnabledCapabilityMcpServers(
|
|
323
|
-
deps.db,
|
|
324
|
-
workspaceId,
|
|
325
|
-
deps.settings,
|
|
326
|
-
);
|
|
327
|
-
const withSessionServers = await settingsWithSessionMcpServersForToolspace(
|
|
328
|
-
deps,
|
|
329
|
-
workspaceId,
|
|
330
|
-
sessionId,
|
|
331
|
-
attemptId,
|
|
332
|
-
runtimeSettings,
|
|
333
|
-
);
|
|
334
|
-
return new Map(withSessionServers.mcpServers.map((server) => [server.id, server]));
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
// Resolve the toolspace tool listing for a request. Serves from the in-process
|
|
338
|
-
// cache when warm; otherwise dials the proxyable upstreams ONCE to (re)list, but
|
|
339
|
-
// only while a turn is active — a request with no active turn never dials an
|
|
340
|
-
// upstream (fix: unbudgeted fan-out). tools/call still funnels through here to
|
|
341
|
-
// register its tool, but with the cache warm that costs no upstream dials.
|
|
342
|
-
async function resolveToolListing(input: {
|
|
343
|
-
deps: ApiRouteDeps;
|
|
344
|
-
grant: AccessGrant;
|
|
345
|
-
sessionId: string;
|
|
346
|
-
rootSessionId: string;
|
|
347
|
-
proxyableIds: string[];
|
|
348
|
-
activeTurn: SessionTurn;
|
|
349
|
-
personalConnectionDelegations: McpPersonalConnectionDelegation[];
|
|
350
|
-
getRegistry: () => Promise<Map<string, McpServerConfig>>;
|
|
351
|
-
}): Promise<ToolListingEntry[]> {
|
|
352
|
-
const {
|
|
353
|
-
deps,
|
|
354
|
-
grant,
|
|
355
|
-
sessionId,
|
|
356
|
-
rootSessionId,
|
|
357
|
-
proxyableIds,
|
|
358
|
-
activeTurn,
|
|
359
|
-
personalConnectionDelegations,
|
|
360
|
-
getRegistry,
|
|
361
|
-
} = input;
|
|
362
|
-
// Host credentials can be initiator-specific. A prior turn's tool list must
|
|
363
|
-
// never be reused under a different frozen authority in the same session.
|
|
364
|
-
const cacheKey = await toolListCacheKey(
|
|
365
|
-
deps,
|
|
366
|
-
grant.workspaceId,
|
|
367
|
-
sessionId,
|
|
368
|
-
proxyableIds,
|
|
369
|
-
activeTurn,
|
|
370
|
-
);
|
|
371
|
-
const cached = readToolListCache(cacheKey);
|
|
372
|
-
if (cached) {
|
|
373
|
-
return cached;
|
|
374
|
-
}
|
|
375
|
-
const registry = await getRegistry();
|
|
376
|
-
const aggregateBudget = new McpAggregateToolListBudget(
|
|
377
|
-
"aggregate Toolspace tool list",
|
|
378
|
-
MCP_MAX_AGGREGATE_TOOL_LIST_ENTRIES,
|
|
379
|
-
MCP_MAX_AGGREGATE_TOOL_LIST_BYTES,
|
|
380
|
-
);
|
|
381
|
-
const entries: ToolListingEntry[] = [];
|
|
382
|
-
// Each mapper commits its source contribution synchronously immediately
|
|
383
|
-
// after the upstream result is bounded. No per-provider result arrays are
|
|
384
|
-
// retained by boundedParallelMap, and a failed aggregate replacement never
|
|
385
|
-
// reaches the cache or the exposed MCP surface.
|
|
386
|
-
await boundedParallelMap(proxyableIds, MCP_MAX_CONCURRENT_SERVER_OPERATIONS, async (serverId) => {
|
|
387
|
-
const config = registry.get(serverId);
|
|
388
|
-
if (!config || !toolspaceCanProxyServer(config)) {
|
|
389
|
-
aggregateBudget.replace(serverId, []);
|
|
390
|
-
return;
|
|
391
|
-
}
|
|
392
|
-
const connection = await connectToolspaceServer({
|
|
393
|
-
deps,
|
|
394
|
-
grant,
|
|
395
|
-
config,
|
|
396
|
-
sessionId,
|
|
397
|
-
rootSessionId,
|
|
398
|
-
turn: activeTurn,
|
|
399
|
-
personalConnectionDelegations,
|
|
400
|
-
}).catch((error) => {
|
|
401
|
-
deps.observability?.warn(
|
|
402
|
-
"toolspace upstream connection failed",
|
|
403
|
-
toolspacePublicErrorFields(error),
|
|
404
|
-
);
|
|
405
|
-
return null;
|
|
406
|
-
});
|
|
407
|
-
if (!connection) {
|
|
408
|
-
aggregateBudget.replace(serverId, []);
|
|
409
|
-
return;
|
|
410
|
-
}
|
|
411
|
-
try {
|
|
412
|
-
const listed = await connection.client
|
|
413
|
-
.listTools(undefined, toolspaceRequestOptions(config))
|
|
414
|
-
.catch((error) => {
|
|
415
|
-
deps.observability?.warn(
|
|
416
|
-
"toolspace upstream tool list failed",
|
|
417
|
-
toolspacePublicErrorFields(error),
|
|
418
|
-
);
|
|
419
|
-
return { tools: [] };
|
|
420
|
-
});
|
|
421
|
-
let boundedTools: readonly McpTool[];
|
|
422
|
-
try {
|
|
423
|
-
boundedTools = assertMcpToolListWithinBounds(listed.tools as McpTool[]) as McpTool[];
|
|
424
|
-
} catch (error) {
|
|
425
|
-
deps.observability?.warn(
|
|
426
|
-
"toolspace upstream tool list exceeded safety limit",
|
|
427
|
-
toolspacePublicErrorFields(error, "tool_list_too_large"),
|
|
428
|
-
);
|
|
429
|
-
aggregateBudget.replace(serverId, []);
|
|
430
|
-
return;
|
|
431
|
-
}
|
|
432
|
-
const sourceEntries = boundedTools
|
|
433
|
-
.filter((tool) => Boolean(tool?.name) && allowedByConfig(config, tool.name))
|
|
434
|
-
.map((tool) => ({
|
|
435
|
-
serverId,
|
|
436
|
-
tool,
|
|
437
|
-
requireApproval: config.requireApproval,
|
|
438
|
-
}));
|
|
439
|
-
aggregateBudget.replace(serverId, sourceEntries);
|
|
440
|
-
entries.push(...sourceEntries);
|
|
441
|
-
} finally {
|
|
442
|
-
await connection.close();
|
|
443
|
-
}
|
|
444
|
-
});
|
|
445
|
-
writeToolListCache(cacheKey, entries);
|
|
446
|
-
return entries;
|
|
447
|
-
}
|
|
448
|
-
|
|
449
|
-
async function toolListCacheKey(
|
|
450
|
-
deps: ApiRouteDeps,
|
|
451
|
-
workspaceId: string,
|
|
452
|
-
sessionId: string,
|
|
453
|
-
proxyableIds: string[],
|
|
454
|
-
turn: SessionTurn,
|
|
455
|
-
): Promise<string> {
|
|
456
|
-
const metadata = await listSessionMcpServerMetadata(deps.db, workspaceId, sessionId);
|
|
457
|
-
const versions = new Map(metadata.map((server) => [server.id, server.credentialVersion]));
|
|
458
|
-
const signature = proxyableIds
|
|
459
|
-
.slice()
|
|
460
|
-
.sort()
|
|
461
|
-
.map((id) => `${id}@${versions.get(id) ?? 0}`)
|
|
462
|
-
.join(",");
|
|
463
|
-
const authority = JSON.stringify({
|
|
464
|
-
turnId: turn.id,
|
|
465
|
-
executionGeneration: turn.executionGeneration,
|
|
466
|
-
attemptId: turn.activeAttemptId,
|
|
467
|
-
initiator: turn.initiator,
|
|
468
|
-
});
|
|
469
|
-
return `${workspaceId}:${sessionId}:${signature}:${authority}`;
|
|
470
|
-
}
|
|
471
|
-
|
|
472
|
-
function readToolListCache(key: string): ToolListingEntry[] | null {
|
|
473
|
-
return toolListCache.read(key);
|
|
474
|
-
}
|
|
475
|
-
|
|
476
|
-
function writeToolListCache(key: string, entries: ToolListingEntry[]): void {
|
|
477
|
-
toolListCache.write(key, entries);
|
|
478
|
-
}
|
|
479
|
-
|
|
480
|
-
async function settingsWithSessionMcpServersForToolspace(
|
|
481
|
-
deps: ApiRouteDeps,
|
|
482
|
-
workspaceId: string,
|
|
483
|
-
sessionId: string,
|
|
484
|
-
attemptId: string,
|
|
485
|
-
settings: ApiRouteDeps["settings"],
|
|
486
|
-
): Promise<ApiRouteDeps["settings"]> {
|
|
487
|
-
const encryptionKey = environmentsEncryptionKeyBytes(settings);
|
|
488
|
-
if (!encryptionKey) {
|
|
489
|
-
const metadata = await listSessionMcpServerMetadata(deps.db, workspaceId, sessionId);
|
|
490
|
-
if (metadata.length === 0) {
|
|
491
|
-
return settings;
|
|
492
|
-
}
|
|
493
|
-
if (metadata.some((server) => server.headerNames.length > 0)) {
|
|
494
|
-
throw new Error(
|
|
495
|
-
"session MCP server credentials require OPENGENI_ENVIRONMENTS_ENCRYPTION_KEY",
|
|
496
|
-
);
|
|
497
|
-
}
|
|
498
|
-
}
|
|
499
|
-
const servers = await listSessionMcpServersForRun(
|
|
500
|
-
deps.db,
|
|
501
|
-
workspaceId,
|
|
502
|
-
sessionId,
|
|
503
|
-
attemptId,
|
|
504
|
-
encryptionKey ?? null,
|
|
505
|
-
);
|
|
506
|
-
if (servers.length === 0) {
|
|
507
|
-
return settings;
|
|
508
|
-
}
|
|
509
|
-
const sessionIds = new Set(servers.map((server) => server.id));
|
|
510
|
-
return {
|
|
511
|
-
...settings,
|
|
512
|
-
mcpServers: [
|
|
513
|
-
...settings.mcpServers.filter((server) => !sessionIds.has(server.id)),
|
|
514
|
-
...servers.map((server) => ({
|
|
515
|
-
id: server.id,
|
|
516
|
-
...(server.name ? { name: server.name } : {}),
|
|
517
|
-
url: server.url,
|
|
518
|
-
...(server.allowedTools ? { allowedTools: server.allowedTools } : {}),
|
|
519
|
-
...(server.timeoutMs ? { timeoutMs: server.timeoutMs } : {}),
|
|
520
|
-
cacheToolsList: server.cacheToolsList ?? false,
|
|
521
|
-
...(server.requireApproval !== undefined
|
|
522
|
-
? { requireApproval: server.requireApproval }
|
|
523
|
-
: {}),
|
|
524
|
-
...(server.connectionRef ? { connectionRef: server.connectionRef } : {}),
|
|
525
|
-
headers: server.headers,
|
|
526
|
-
})),
|
|
527
|
-
],
|
|
528
|
-
};
|
|
529
|
-
}
|
|
530
|
-
|
|
531
|
-
async function connectToolspaceServer(input: {
|
|
532
|
-
deps: ApiRouteDeps;
|
|
533
|
-
grant: AccessGrant;
|
|
534
|
-
config: McpServerConfig;
|
|
535
|
-
sessionId: string;
|
|
536
|
-
rootSessionId: string;
|
|
537
|
-
turn: SessionTurn;
|
|
538
|
-
personalConnectionDelegations: McpPersonalConnectionDelegation[];
|
|
539
|
-
}): Promise<ConnectedToolspaceServer> {
|
|
540
|
-
// npm Undici's dispatcher transport is not reliable under Bun. The worker's
|
|
541
|
-
// model-visible MCP path already uses Bun's native fetch while retaining the
|
|
542
|
-
// same pre-request destination-policy check; Toolspace must do the same or an
|
|
543
|
-
// embedded Bun API can expose a server to the model while silently dropping
|
|
544
|
-
// that exact server from `ogtool list`.
|
|
545
|
-
const useBunNativeFetch = !!process.versions.bun;
|
|
546
|
-
const mcpFetchImpl: FetchLike = useBunNativeFetch
|
|
547
|
-
? globalThis.fetch.bind(globalThis)
|
|
548
|
-
: undiciFetch;
|
|
549
|
-
const guardedFetch = guardedMcpFetch(input.deps.settings, mcpFetchImpl, {
|
|
550
|
-
...(useBunNativeFetch ? { pinResolvedDestination: false } : {}),
|
|
551
|
-
});
|
|
552
|
-
const baseFetch: FetchLike = input.config.connectionRef
|
|
553
|
-
? connectionBrokerFetch(guardedFetch, input)
|
|
554
|
-
: guardedFetch;
|
|
555
|
-
const client = new Client(
|
|
556
|
-
{ name: `opengeni-toolspace-${input.config.id}`, version: "1.0.0" },
|
|
557
|
-
{ capabilities: {} },
|
|
558
|
-
);
|
|
559
|
-
const transport = new StreamableHTTPClientTransport(new URL(input.config.url), {
|
|
560
|
-
...(baseFetch !== globalThis.fetch ? { fetch: baseFetch } : {}),
|
|
561
|
-
requestInit: {
|
|
562
|
-
headers: toolspaceServerHeaders(input.config),
|
|
563
|
-
},
|
|
564
|
-
});
|
|
565
|
-
try {
|
|
566
|
-
await client.connect(transport as unknown as Transport, toolspaceRequestOptions(input.config));
|
|
567
|
-
} catch (error) {
|
|
568
|
-
await client.close().catch(() => undefined);
|
|
569
|
-
throw error;
|
|
570
|
-
}
|
|
571
|
-
return {
|
|
572
|
-
config: input.config,
|
|
573
|
-
client,
|
|
574
|
-
close: async () => {
|
|
575
|
-
await client.close().catch(() => undefined);
|
|
576
|
-
},
|
|
577
|
-
};
|
|
578
|
-
}
|
|
579
|
-
|
|
580
|
-
function toolspaceToolFor(input: {
|
|
581
|
-
deps: ApiRouteDeps;
|
|
582
|
-
grant: AccessGrant;
|
|
583
|
-
authority: ToolspaceAttemptAuthority;
|
|
584
|
-
rootSessionId: string;
|
|
585
|
-
entry: ToolListingEntry;
|
|
586
|
-
personalConnectionDelegations: McpPersonalConnectionDelegation[];
|
|
587
|
-
getRegistry: (attemptId: string) => Promise<Map<string, McpServerConfig>>;
|
|
588
|
-
}): ToolspaceRegisteredTool {
|
|
589
|
-
const {
|
|
590
|
-
deps,
|
|
591
|
-
grant,
|
|
592
|
-
authority,
|
|
593
|
-
rootSessionId,
|
|
594
|
-
entry,
|
|
595
|
-
personalConnectionDelegations,
|
|
596
|
-
getRegistry,
|
|
597
|
-
} = input;
|
|
598
|
-
const { sessionId } = authority;
|
|
599
|
-
const { serverId, tool } = entry;
|
|
600
|
-
const name = prefixedMcpToolName(serverId, tool.name);
|
|
601
|
-
const approvalRequired = mcpToolRequiresApproval(entry.requireApproval, tool.name);
|
|
602
|
-
const description = approvalRequired
|
|
603
|
-
? `${tool.description ?? tool.name} (unavailable: ${APPROVAL_REQUIRED_MESSAGE})`
|
|
604
|
-
: tool.description;
|
|
605
|
-
return {
|
|
606
|
-
name,
|
|
607
|
-
...(description ? { description } : {}),
|
|
608
|
-
...(tool.inputSchema ? { inputSchema: tool.inputSchema } : {}),
|
|
609
|
-
call: async (args) => {
|
|
610
|
-
const reservation = await reserveExactAttemptCall(deps, grant, authority);
|
|
611
|
-
if (reservation.status === "no_active_turn") {
|
|
612
|
-
return mcpError(TOOLSPACE_NO_ACTIVE_TURN_MESSAGE);
|
|
613
|
-
}
|
|
614
|
-
if (reservation.status === "budget_exhausted") {
|
|
615
|
-
return mcpError(
|
|
616
|
-
`toolspace call budget exhausted (${deps.settings.toolspaceMaxCallsPerTurn}/turn)`,
|
|
617
|
-
);
|
|
618
|
-
}
|
|
619
|
-
const turnId = reservation.turn.id;
|
|
620
|
-
// Dial only the ONE server this tool belongs to, from the freshly-built
|
|
621
|
-
// exact-attempt registry. Listing policy is descriptive only; a stale MCP
|
|
622
|
-
// surface can never decide authorization for a successor attempt.
|
|
623
|
-
const registry = await getRegistry(authority.attemptId);
|
|
624
|
-
const config = registry.get(serverId);
|
|
625
|
-
if (!config || !toolspaceCanProxyServer(config) || !allowedByConfig(config, tool.name)) {
|
|
626
|
-
return mcpError(`upstream tool failed: ${name}`);
|
|
627
|
-
}
|
|
628
|
-
if (mcpToolRequiresApproval(config.requireApproval, tool.name)) {
|
|
629
|
-
return mcpError(APPROVAL_REQUIRED_MESSAGE);
|
|
630
|
-
}
|
|
631
|
-
let connection: ConnectedToolspaceServer;
|
|
632
|
-
try {
|
|
633
|
-
connection = await connectToolspaceServer({
|
|
634
|
-
deps,
|
|
635
|
-
grant,
|
|
636
|
-
config,
|
|
637
|
-
sessionId,
|
|
638
|
-
rootSessionId,
|
|
639
|
-
turn: reservation.turn,
|
|
640
|
-
personalConnectionDelegations,
|
|
641
|
-
});
|
|
642
|
-
} catch (error) {
|
|
643
|
-
return mcpError(exactErrorMessage(error));
|
|
644
|
-
}
|
|
645
|
-
try {
|
|
646
|
-
const callId = crypto.randomUUID();
|
|
647
|
-
const receipt = {
|
|
648
|
-
accountId: grant.accountId,
|
|
649
|
-
workspaceId: grant.workspaceId,
|
|
650
|
-
sessionId,
|
|
651
|
-
turnId,
|
|
652
|
-
executionGeneration: authority.executionGeneration,
|
|
653
|
-
attemptId: authority.attemptId,
|
|
654
|
-
callId,
|
|
655
|
-
};
|
|
656
|
-
const registered = await registerPendingSessionToolCall(deps.db, {
|
|
657
|
-
...receipt,
|
|
658
|
-
callType: "toolspace_call",
|
|
659
|
-
callItem: {
|
|
660
|
-
type: "toolspace_call",
|
|
661
|
-
id: callId,
|
|
662
|
-
name,
|
|
663
|
-
arguments: args,
|
|
664
|
-
serverId,
|
|
665
|
-
toolName: tool.name,
|
|
666
|
-
},
|
|
667
|
-
});
|
|
668
|
-
if (!registered.accepted || !registered.registered) {
|
|
669
|
-
return mcpError(TOOLSPACE_NO_ACTIVE_TURN_MESSAGE);
|
|
670
|
-
}
|
|
671
|
-
const created = await appendAndPublishTurnEventsFenced(
|
|
672
|
-
deps.db,
|
|
673
|
-
deps.bus,
|
|
674
|
-
grant.workspaceId,
|
|
675
|
-
sessionId,
|
|
676
|
-
turnId,
|
|
677
|
-
authority.executionGeneration,
|
|
678
|
-
authority.attemptId,
|
|
679
|
-
[
|
|
680
|
-
{
|
|
681
|
-
type: "agent.toolCall.created",
|
|
682
|
-
turnId,
|
|
683
|
-
turnGeneration: authority.executionGeneration,
|
|
684
|
-
turnAttemptId: authority.attemptId,
|
|
685
|
-
producerId: grant.subjectId,
|
|
686
|
-
payload: {
|
|
687
|
-
id: callId,
|
|
688
|
-
name,
|
|
689
|
-
arguments: args,
|
|
690
|
-
origin: "toolspace",
|
|
691
|
-
subjectId: grant.subjectId,
|
|
692
|
-
raw: {
|
|
693
|
-
type: "toolspace_call",
|
|
694
|
-
serverId,
|
|
695
|
-
toolName: tool.name,
|
|
696
|
-
},
|
|
697
|
-
},
|
|
698
|
-
},
|
|
699
|
-
],
|
|
700
|
-
);
|
|
701
|
-
if (!created.accepted) {
|
|
702
|
-
return mcpError(TOOLSPACE_NO_ACTIVE_TURN_MESSAGE);
|
|
703
|
-
}
|
|
704
|
-
const output = await callRemoteTool(deps, connection, tool.name, args);
|
|
705
|
-
const completed = await appendAndPublishTurnEventsFenced(
|
|
706
|
-
deps.db,
|
|
707
|
-
deps.bus,
|
|
708
|
-
grant.workspaceId,
|
|
709
|
-
sessionId,
|
|
710
|
-
turnId,
|
|
711
|
-
authority.executionGeneration,
|
|
712
|
-
authority.attemptId,
|
|
713
|
-
[
|
|
714
|
-
{
|
|
715
|
-
type: "agent.toolCall.output",
|
|
716
|
-
turnId,
|
|
717
|
-
turnGeneration: authority.executionGeneration,
|
|
718
|
-
turnAttemptId: authority.attemptId,
|
|
719
|
-
producerId: grant.subjectId,
|
|
720
|
-
payload: {
|
|
721
|
-
id: callId,
|
|
722
|
-
output,
|
|
723
|
-
origin: "toolspace",
|
|
724
|
-
subjectId: grant.subjectId,
|
|
725
|
-
},
|
|
726
|
-
},
|
|
727
|
-
],
|
|
728
|
-
);
|
|
729
|
-
if (completed.accepted) {
|
|
730
|
-
await clearPendingSessionToolspaceCall(deps.db, receipt);
|
|
731
|
-
}
|
|
732
|
-
return output;
|
|
733
|
-
} finally {
|
|
734
|
-
await connection.close();
|
|
735
|
-
}
|
|
736
|
-
},
|
|
737
|
-
};
|
|
738
|
-
}
|
|
739
|
-
|
|
740
|
-
async function callRemoteTool(
|
|
741
|
-
deps: ApiRouteDeps,
|
|
742
|
-
server: ConnectedToolspaceServer,
|
|
743
|
-
toolName: string,
|
|
744
|
-
args: Record<string, unknown>,
|
|
745
|
-
): Promise<ToolspaceCallResult> {
|
|
746
|
-
try {
|
|
747
|
-
const output = (await server.client.callTool(
|
|
748
|
-
{
|
|
749
|
-
name: toolName,
|
|
750
|
-
arguments: args,
|
|
751
|
-
},
|
|
752
|
-
undefined,
|
|
753
|
-
toolspaceRequestOptions(server.config),
|
|
754
|
-
)) as ToolspaceCallResult;
|
|
755
|
-
assertMcpPayloadWithinBytes(output, MCP_MAX_TOOL_RESULT_BYTES, "MCP tool result");
|
|
756
|
-
return output;
|
|
757
|
-
} catch (error) {
|
|
758
|
-
if (error instanceof McpPayloadTooLargeError) {
|
|
759
|
-
deps.observability?.warn(
|
|
760
|
-
"toolspace upstream tool result exceeded safety limit",
|
|
761
|
-
toolspacePublicErrorFields(error, "tool_result_too_large"),
|
|
762
|
-
);
|
|
763
|
-
return mcpError("upstream tool result exceeded the safety limit");
|
|
764
|
-
}
|
|
765
|
-
if (isToolspaceAuthNeededError(error)) {
|
|
766
|
-
return mcpError(TOOLSPACE_AUTH_NEEDED_ERROR.message);
|
|
767
|
-
}
|
|
768
|
-
if (isToolspaceOutcomeUncertainError(error)) {
|
|
769
|
-
return mcpError(TOOLSPACE_TOOL_OUTCOME_UNCERTAIN_ERROR.message);
|
|
770
|
-
}
|
|
771
|
-
const message = exactErrorMessage(error);
|
|
772
|
-
// The exact provider diagnostic remains the internal/model-facing tool
|
|
773
|
-
// result. Public observability receives only allowlisted metadata.
|
|
774
|
-
deps.observability?.warn(
|
|
775
|
-
"toolspace upstream tool call failed",
|
|
776
|
-
toolspacePublicErrorFields(error),
|
|
777
|
-
);
|
|
778
|
-
return mcpError(message);
|
|
779
|
-
}
|
|
780
|
-
}
|
|
781
|
-
|
|
782
|
-
function exactErrorMessage(error: unknown): string {
|
|
783
|
-
return error instanceof Error ? error.message : String(error);
|
|
784
|
-
}
|
|
785
|
-
|
|
786
|
-
export type ToolspacePublicErrorFields = {
|
|
787
|
-
errorClass: "ToolspaceOperationError";
|
|
788
|
-
errorCode: ToolspacePublicFailureCode;
|
|
789
|
-
status?: number;
|
|
790
|
-
origin: "toolspace";
|
|
791
|
-
};
|
|
792
|
-
|
|
793
|
-
type ToolspacePublicFailureCode =
|
|
794
|
-
| "toolspace_operation_failed"
|
|
795
|
-
| "tool_list_too_large"
|
|
796
|
-
| "tool_result_too_large";
|
|
797
|
-
|
|
798
|
-
/** Allowlisted projection for public telemetry; product data stays exact. */
|
|
799
|
-
export function toolspacePublicErrorFields(
|
|
800
|
-
error: unknown,
|
|
801
|
-
errorCode: ToolspacePublicFailureCode = "toolspace_operation_failed",
|
|
802
|
-
): ToolspacePublicErrorFields {
|
|
803
|
-
const fields: ToolspacePublicErrorFields = {
|
|
804
|
-
errorClass: "ToolspaceOperationError",
|
|
805
|
-
errorCode,
|
|
806
|
-
origin: "toolspace",
|
|
807
|
-
};
|
|
808
|
-
try {
|
|
809
|
-
const rawStatus =
|
|
810
|
-
error && typeof error === "object"
|
|
811
|
-
? ((error as { status?: unknown; statusCode?: unknown }).status ??
|
|
812
|
-
(error as { statusCode?: unknown }).statusCode)
|
|
813
|
-
: undefined;
|
|
814
|
-
const status = Number(rawStatus);
|
|
815
|
-
if (Number.isInteger(status) && status >= 100 && status <= 599) fields.status = status;
|
|
816
|
-
} catch {
|
|
817
|
-
// Public telemetry is best-effort. A hostile getter/proxy must never
|
|
818
|
-
// replace the exact Toolspace failure with a projection failure.
|
|
819
|
-
}
|
|
820
|
-
return fields;
|
|
821
|
-
}
|
|
822
|
-
|
|
823
|
-
type ToolspaceReservation =
|
|
824
|
-
| { status: "ok"; turn: SessionTurn }
|
|
825
|
-
| { status: "no_active_turn" }
|
|
826
|
-
| { status: "budget_exhausted" };
|
|
827
|
-
|
|
828
|
-
async function reserveExactAttemptCall(
|
|
829
|
-
deps: ApiRouteDeps,
|
|
830
|
-
grant: AccessGrant,
|
|
831
|
-
authority: ToolspaceAttemptAuthority,
|
|
832
|
-
): Promise<ToolspaceReservation> {
|
|
833
|
-
const reservation = await reserveToolspaceCallForAttempt(deps.db, {
|
|
834
|
-
accountId: grant.accountId,
|
|
835
|
-
workspaceId: grant.workspaceId,
|
|
836
|
-
sessionId: authority.sessionId,
|
|
837
|
-
turnId: authority.turnId,
|
|
838
|
-
executionGeneration: authority.executionGeneration,
|
|
839
|
-
attemptId: authority.attemptId,
|
|
840
|
-
limit: deps.settings.toolspaceMaxCallsPerTurn,
|
|
841
|
-
});
|
|
842
|
-
if (!reservation.reserved) {
|
|
843
|
-
return reservation.reason === "budget_exhausted"
|
|
844
|
-
? { status: "budget_exhausted" }
|
|
845
|
-
: { status: "no_active_turn" };
|
|
846
|
-
}
|
|
847
|
-
return { status: "ok", turn: reservation.turn };
|
|
848
|
-
}
|
|
849
|
-
|
|
850
|
-
function selectedMcpServerIds(tools: ToolRef[], sessionServerIds: string[]): Set<string> {
|
|
851
|
-
const out = new Set<string>(sessionServerIds);
|
|
852
|
-
for (const tool of tools) {
|
|
853
|
-
if (tool.kind === "mcp") {
|
|
854
|
-
out.add(tool.id);
|
|
855
|
-
}
|
|
856
|
-
}
|
|
857
|
-
return out;
|
|
858
|
-
}
|
|
859
|
-
|
|
860
|
-
// Whether a selected server id may enter the toolspace proxy at all. The
|
|
861
|
-
// The first-party OpenGeni tool server and files/docs proxies are excluded by
|
|
862
|
-
// construction because they route back through /mcp (recursion guard). Codex
|
|
863
|
-
// Apps is excluded because Toolspace does not own its dynamic authorization.
|
|
864
|
-
export function toolspaceCanProxyServerId(serverId: string): boolean {
|
|
865
|
-
return serverId !== "opengeni" && !FIRST_PARTY_PROXY_IDS.has(serverId);
|
|
866
|
-
}
|
|
867
|
-
|
|
868
|
-
function toolspaceCanProxyServer(config: McpServerConfig): boolean {
|
|
869
|
-
return toolspaceCanProxyServerId(config.id);
|
|
870
|
-
}
|
|
871
|
-
|
|
872
|
-
// Only third-party / session / pack MCP servers reach this path (first-party
|
|
873
|
-
// proxies are excluded above), so headers are just the server's own configured
|
|
874
|
-
// or broker-injected headers. The caller's `ogd_` bearer is deliberately never
|
|
875
|
-
// forwarded upstream.
|
|
876
|
-
function toolspaceServerHeaders(config: McpServerConfig): Record<string, string> {
|
|
877
|
-
const headers: Record<string, string> = {};
|
|
878
|
-
for (const [name, value] of Object.entries(config.headers ?? {})) {
|
|
879
|
-
headers[name] = value;
|
|
880
|
-
}
|
|
881
|
-
return headers;
|
|
882
|
-
}
|
|
883
|
-
|
|
884
|
-
function allowedByConfig(config: McpServerConfig, toolName: string): boolean {
|
|
885
|
-
return !config.allowedTools || config.allowedTools.includes(toolName);
|
|
886
|
-
}
|
|
887
|
-
|
|
888
|
-
function mcpToolRequiresApproval(
|
|
889
|
-
policy: McpServerConfig["requireApproval"],
|
|
890
|
-
unprefixedName: string,
|
|
891
|
-
): boolean {
|
|
892
|
-
if (policy === true) {
|
|
893
|
-
return true;
|
|
894
|
-
}
|
|
895
|
-
return Array.isArray(policy) && policy.includes(unprefixedName);
|
|
896
|
-
}
|
|
897
|
-
|
|
898
|
-
function mcpError(message: string): ToolspaceCallResult {
|
|
899
|
-
return {
|
|
900
|
-
isError: true,
|
|
901
|
-
content: [{ type: "text", text: message }],
|
|
902
|
-
};
|
|
903
|
-
}
|
|
904
|
-
|
|
905
|
-
function toolspaceRequestOptions(config: McpServerConfig): {
|
|
906
|
-
timeout?: number;
|
|
907
|
-
maxTotalTimeout?: number;
|
|
908
|
-
} {
|
|
909
|
-
return config.timeoutMs ? { timeout: config.timeoutMs, maxTotalTimeout: config.timeoutMs } : {};
|
|
910
|
-
}
|
|
911
|
-
|
|
912
|
-
function mcpRequestDestinationUrl(input: string | URL | Request): string {
|
|
913
|
-
return new URL(input instanceof Request ? input.url : input.toString()).toString();
|
|
914
|
-
}
|
|
915
|
-
|
|
916
|
-
export function connectionBrokerFetch(
|
|
917
|
-
baseFetch: FetchLike,
|
|
918
|
-
input: {
|
|
919
|
-
deps: ApiRouteDeps;
|
|
920
|
-
grant: AccessGrant;
|
|
921
|
-
config: McpServerConfig;
|
|
922
|
-
sessionId: string;
|
|
923
|
-
rootSessionId: string;
|
|
924
|
-
turn: SessionTurn;
|
|
925
|
-
personalConnectionDelegations?: McpPersonalConnectionDelegation[];
|
|
926
|
-
},
|
|
927
|
-
): FetchLike {
|
|
928
|
-
const connectionRef = input.config.connectionRef;
|
|
929
|
-
if (!connectionRef) {
|
|
930
|
-
return baseFetch;
|
|
931
|
-
}
|
|
932
|
-
const hostCredentialPort = input.deps.connectionCredentials?.mcpCredentials;
|
|
933
|
-
const rawResolveCredential = hostCredentialPort
|
|
934
|
-
? buildHostConnectionTokenResolver(hostCredentialPort, {
|
|
935
|
-
accountId: input.grant.accountId,
|
|
936
|
-
workspaceId: input.grant.workspaceId,
|
|
937
|
-
sessionId: input.sessionId,
|
|
938
|
-
rootSessionId: input.rootSessionId,
|
|
939
|
-
turnId: input.turn.id,
|
|
940
|
-
attemptId: input.turn.activeAttemptId,
|
|
941
|
-
executionGeneration: input.turn.executionGeneration,
|
|
942
|
-
initiator: input.turn.initiator,
|
|
943
|
-
initiatorContext: input.turn.initiatorContext,
|
|
944
|
-
surface: "toolspace",
|
|
945
|
-
})
|
|
946
|
-
: buildConnectionTokenResolver(input.deps.db, input.deps.settings);
|
|
947
|
-
const personalDelegations = input.personalConnectionDelegations ?? [];
|
|
948
|
-
const delegatedMembershipChecks = new Map<string, Promise<boolean>>();
|
|
949
|
-
const delegatedOwnerHasMembership = async (subjectId: string): Promise<boolean> => {
|
|
950
|
-
const existing = delegatedMembershipChecks.get(subjectId);
|
|
951
|
-
if (existing) return await existing;
|
|
952
|
-
const check = getWorkspaceGrant(input.deps.db, subjectId, input.grant.workspaceId).then(
|
|
953
|
-
Boolean,
|
|
954
|
-
);
|
|
955
|
-
delegatedMembershipChecks.set(subjectId, check);
|
|
956
|
-
return await check;
|
|
957
|
-
};
|
|
958
|
-
const resolveCredential = withFrozenPersonalConnectionDelegations({
|
|
959
|
-
resolveCredential: rawResolveCredential,
|
|
960
|
-
settings: { mcpServers: [input.config] },
|
|
961
|
-
personalConnectionDelegations: personalDelegations,
|
|
962
|
-
ownerHasWorkspaceMembership: delegatedOwnerHasMembership,
|
|
963
|
-
});
|
|
964
|
-
return async (requestInput, init) => {
|
|
965
|
-
const request = await mcpRequestReplayInfo(requestInput, init);
|
|
966
|
-
const destinationUrl = mcpRequestDestinationUrl(requestInput);
|
|
967
|
-
const resolverSubjectId =
|
|
968
|
-
connectionRef.subjectScope !== "subject" && hostCredentialPort
|
|
969
|
-
? input.grant.subjectId
|
|
970
|
-
: undefined;
|
|
971
|
-
const resolve = async (forceRefresh: boolean) => {
|
|
972
|
-
const result = await resolveCredential({
|
|
973
|
-
workspaceId: input.grant.workspaceId,
|
|
974
|
-
serverId: input.config.id,
|
|
975
|
-
connectionRef,
|
|
976
|
-
destinationUrl,
|
|
977
|
-
forceRefresh,
|
|
978
|
-
...(request.toolName ? { toolName: request.toolName } : {}),
|
|
979
|
-
...(resolverSubjectId ? { subjectId: resolverSubjectId } : {}),
|
|
980
|
-
});
|
|
981
|
-
return result;
|
|
982
|
-
};
|
|
983
|
-
const first = await resolve(false);
|
|
984
|
-
if (first.status === "auth_needed") {
|
|
985
|
-
return await authNeededFetchResponse(input, request, first);
|
|
986
|
-
}
|
|
987
|
-
const response = await baseFetch(
|
|
988
|
-
fetchInputForAttempt(requestInput),
|
|
989
|
-
withConnectionHeaders(requestInput, init, first.headers),
|
|
990
|
-
);
|
|
991
|
-
if (response.status === 401) {
|
|
992
|
-
await cancelMcpResponseBody(response);
|
|
993
|
-
let refreshed: ResolveConnectionCredentialResult;
|
|
994
|
-
try {
|
|
995
|
-
refreshed = await resolve(true);
|
|
996
|
-
} catch {
|
|
997
|
-
refreshed = authNeededFromStatus(input.config, first, "refresh_failed");
|
|
998
|
-
}
|
|
999
|
-
if (refreshed.status === "auth_needed") {
|
|
1000
|
-
if (!request.replaySafeAfter401) {
|
|
1001
|
-
await publishToolspaceAuthNeeded(input, request, refreshed);
|
|
1002
|
-
return toolspaceMcpOutcomeUncertainResponse(request);
|
|
1003
|
-
}
|
|
1004
|
-
return await authNeededFetchResponse(input, request, refreshed);
|
|
1005
|
-
}
|
|
1006
|
-
if (!request.replaySafeAfter401) {
|
|
1007
|
-
return toolspaceMcpOutcomeUncertainResponse(request);
|
|
1008
|
-
}
|
|
1009
|
-
const retry = await baseFetch(
|
|
1010
|
-
fetchInputForAttempt(requestInput),
|
|
1011
|
-
withConnectionHeaders(requestInput, init, refreshed.headers),
|
|
1012
|
-
);
|
|
1013
|
-
if (retry.status === 401) {
|
|
1014
|
-
await cancelMcpResponseBody(retry);
|
|
1015
|
-
return await authNeededFetchResponse(
|
|
1016
|
-
input,
|
|
1017
|
-
request,
|
|
1018
|
-
authNeededFromStatus(input.config, refreshed, "expired"),
|
|
1019
|
-
);
|
|
1020
|
-
}
|
|
1021
|
-
if (retry.status === 403) {
|
|
1022
|
-
await cancelMcpResponseBody(retry);
|
|
1023
|
-
return await authNeededFetchResponse(
|
|
1024
|
-
input,
|
|
1025
|
-
request,
|
|
1026
|
-
authNeededFromStatus(input.config, refreshed, "insufficient_scope"),
|
|
1027
|
-
);
|
|
1028
|
-
}
|
|
1029
|
-
return retry;
|
|
1030
|
-
}
|
|
1031
|
-
if (response.status === 403) {
|
|
1032
|
-
await cancelMcpResponseBody(response);
|
|
1033
|
-
return await authNeededFetchResponse(
|
|
1034
|
-
input,
|
|
1035
|
-
request,
|
|
1036
|
-
authNeededFromStatus(input.config, first, "insufficient_scope"),
|
|
1037
|
-
);
|
|
1038
|
-
}
|
|
1039
|
-
return response;
|
|
1040
|
-
};
|
|
1041
|
-
}
|
|
1042
|
-
|
|
1043
|
-
function authNeededFromStatus(
|
|
1044
|
-
config: McpServerConfig,
|
|
1045
|
-
first: Extract<ResolveConnectionCredentialResult, { status: "ok" }>,
|
|
1046
|
-
reason: Extract<ResolveConnectionCredentialResult, { status: "auth_needed" }>["reason"],
|
|
1047
|
-
): Extract<ResolveConnectionCredentialResult, { status: "auth_needed" }> {
|
|
1048
|
-
const connectionRef = config.connectionRef!;
|
|
1049
|
-
return {
|
|
1050
|
-
status: "auth_needed",
|
|
1051
|
-
reason,
|
|
1052
|
-
providerDomain: connectionRef.providerDomain,
|
|
1053
|
-
...(connectionRef.provider ? { provider: connectionRef.provider } : {}),
|
|
1054
|
-
...(connectionRef.subjectScope === "subject" ? {} : { connectionId: first.connectionId }),
|
|
1055
|
-
...(connectionRef.scopes ? { scopes: connectionRef.scopes } : {}),
|
|
1056
|
-
...(connectionRef.resource ? { resource: connectionRef.resource } : {}),
|
|
1057
|
-
...(connectionRef.selectedResources
|
|
1058
|
-
? { selectedResources: connectionRef.selectedResources }
|
|
1059
|
-
: {}),
|
|
1060
|
-
};
|
|
1061
|
-
}
|
|
1062
|
-
|
|
1063
|
-
async function authNeededFetchResponse(
|
|
1064
|
-
input: {
|
|
1065
|
-
deps: ApiRouteDeps;
|
|
1066
|
-
grant: AccessGrant;
|
|
1067
|
-
config: McpServerConfig;
|
|
1068
|
-
sessionId: string;
|
|
1069
|
-
turn: SessionTurn;
|
|
1070
|
-
},
|
|
1071
|
-
request: McpRequestReplayInfo,
|
|
1072
|
-
auth: Extract<ResolveConnectionCredentialResult, { status: "auth_needed" }>,
|
|
1073
|
-
): Promise<Response> {
|
|
1074
|
-
await publishToolspaceAuthNeeded(input, request, auth);
|
|
1075
|
-
if (request.method === "tools/call") {
|
|
1076
|
-
return toolspaceMcpErrorResponse(request.id, TOOLSPACE_AUTH_NEEDED_ERROR);
|
|
1077
|
-
}
|
|
1078
|
-
return new Response("Authentication required for MCP server connection", { status: 401 });
|
|
1079
|
-
}
|
|
1080
|
-
|
|
1081
|
-
async function publishToolspaceAuthNeeded(
|
|
1082
|
-
input: {
|
|
1083
|
-
deps: ApiRouteDeps;
|
|
1084
|
-
grant: AccessGrant;
|
|
1085
|
-
config: McpServerConfig;
|
|
1086
|
-
sessionId: string;
|
|
1087
|
-
turn: SessionTurn;
|
|
1088
|
-
},
|
|
1089
|
-
request: McpRequestReplayInfo,
|
|
1090
|
-
auth: Extract<ResolveConnectionCredentialResult, { status: "auth_needed" }>,
|
|
1091
|
-
): Promise<void> {
|
|
1092
|
-
await appendAndPublishEvents(
|
|
1093
|
-
input.deps.db,
|
|
1094
|
-
input.deps.bus,
|
|
1095
|
-
input.grant.workspaceId,
|
|
1096
|
-
input.sessionId,
|
|
1097
|
-
[
|
|
1098
|
-
{
|
|
1099
|
-
type: "tool.auth_needed",
|
|
1100
|
-
producerId: input.grant.subjectId,
|
|
1101
|
-
payload: {
|
|
1102
|
-
serverId: input.config.id,
|
|
1103
|
-
toolName: request.toolName ?? null,
|
|
1104
|
-
providerDomain: auth.providerDomain,
|
|
1105
|
-
...(auth.provider ? { provider: auth.provider } : {}),
|
|
1106
|
-
reason: auth.reason,
|
|
1107
|
-
...(auth.connectionId ? { connectionId: auth.connectionId } : {}),
|
|
1108
|
-
...(auth.scopes ? { scopes: auth.scopes } : {}),
|
|
1109
|
-
...(auth.resource ? { resource: auth.resource } : {}),
|
|
1110
|
-
...(auth.selectedResources ? { selectedResources: auth.selectedResources } : {}),
|
|
1111
|
-
...(auth.authorizationUrl ? { authorizationUrl: auth.authorizationUrl } : {}),
|
|
1112
|
-
subjectId: input.grant.subjectId,
|
|
1113
|
-
},
|
|
1114
|
-
},
|
|
1115
|
-
],
|
|
1116
|
-
).catch(() => undefined);
|
|
1117
|
-
}
|
|
1118
|
-
|
|
1119
|
-
function toolspaceMcpErrorResponse(
|
|
1120
|
-
id: string | number | null | undefined,
|
|
1121
|
-
error: { code: number; message: string },
|
|
1122
|
-
): Response {
|
|
1123
|
-
return new Response(
|
|
1124
|
-
JSON.stringify({
|
|
1125
|
-
jsonrpc: "2.0",
|
|
1126
|
-
id: id ?? null,
|
|
1127
|
-
error,
|
|
1128
|
-
}),
|
|
1129
|
-
{
|
|
1130
|
-
status: 200,
|
|
1131
|
-
headers: { "content-type": "application/json" },
|
|
1132
|
-
},
|
|
1133
|
-
);
|
|
1134
|
-
}
|
|
1135
|
-
|
|
1136
|
-
function toolspaceMcpOutcomeUncertainResponse(request: McpRequestReplayInfo): Response {
|
|
1137
|
-
return new Response(
|
|
1138
|
-
JSON.stringify(
|
|
1139
|
-
mcpJsonRpcErrorPayloadForRequest(request, TOOLSPACE_TOOL_OUTCOME_UNCERTAIN_ERROR),
|
|
1140
|
-
),
|
|
1141
|
-
{
|
|
1142
|
-
status: 200,
|
|
1143
|
-
headers: { "content-type": "application/json" },
|
|
1144
|
-
},
|
|
1145
|
-
);
|
|
1146
|
-
}
|
|
1147
|
-
|
|
1148
|
-
function withConnectionHeaders(
|
|
1149
|
-
input: string | URL | Request,
|
|
1150
|
-
init: RequestInit | undefined,
|
|
1151
|
-
authHeaders: Record<string, string>,
|
|
1152
|
-
): RequestInit {
|
|
1153
|
-
const headers = new Headers(
|
|
1154
|
-
init?.headers ?? (input instanceof Request ? input.headers : undefined),
|
|
1155
|
-
);
|
|
1156
|
-
for (const [name, value] of Object.entries(authHeaders)) {
|
|
1157
|
-
headers.set(name, value);
|
|
1158
|
-
}
|
|
1159
|
-
return { ...init, headers };
|
|
1160
|
-
}
|
|
1161
|
-
|
|
1162
|
-
function fetchInputForAttempt(input: string | URL | Request): string | URL | Request {
|
|
1163
|
-
return input instanceof Request ? input.clone() : input;
|
|
1164
|
-
}
|
|
1165
|
-
|
|
1166
|
-
function isToolspaceAuthNeededError(error: unknown): boolean {
|
|
1167
|
-
if (!(error instanceof Error)) {
|
|
1168
|
-
return false;
|
|
1169
|
-
}
|
|
1170
|
-
const code = (error as { code?: unknown }).code;
|
|
1171
|
-
return (
|
|
1172
|
-
code === TOOLSPACE_AUTH_NEEDED_ERROR.code &&
|
|
1173
|
-
(error.message === TOOLSPACE_AUTH_NEEDED_ERROR.message ||
|
|
1174
|
-
error.message ===
|
|
1175
|
-
`MCP error ${TOOLSPACE_AUTH_NEEDED_ERROR.code}: ${TOOLSPACE_AUTH_NEEDED_ERROR.message}`)
|
|
1176
|
-
);
|
|
1177
|
-
}
|
|
1178
|
-
|
|
1179
|
-
function isToolspaceOutcomeUncertainError(error: unknown): boolean {
|
|
1180
|
-
if (!(error instanceof Error)) {
|
|
1181
|
-
return false;
|
|
1182
|
-
}
|
|
1183
|
-
const code = (error as { code?: unknown }).code;
|
|
1184
|
-
return (
|
|
1185
|
-
code === TOOLSPACE_TOOL_OUTCOME_UNCERTAIN_ERROR.code &&
|
|
1186
|
-
(error.message === TOOLSPACE_TOOL_OUTCOME_UNCERTAIN_ERROR.message ||
|
|
1187
|
-
error.message ===
|
|
1188
|
-
`MCP error ${TOOLSPACE_TOOL_OUTCOME_UNCERTAIN_ERROR.code}: ${TOOLSPACE_TOOL_OUTCOME_UNCERTAIN_ERROR.message}`)
|
|
1189
|
-
);
|
|
1190
|
-
}
|