@opengeni/core 0.4.5 → 0.4.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +179 -30
- package/dist/index.js +1324 -477
- package/dist/index.js.map +1 -1
- package/package.json +18 -18
- package/src/access/index.ts +132 -57
- package/src/billing/limits.ts +76 -34
- package/src/dependencies.ts +76 -14
- package/src/domain/capabilities.ts +388 -182
- package/src/domain/environments.ts +58 -38
- package/src/domain/packs.ts +49 -16
- package/src/domain/resources.ts +71 -28
- package/src/domain/scheduled-tasks.ts +107 -41
- package/src/domain/sessions.ts +514 -258
- package/src/domain/workspace-members.ts +6 -2
- package/src/index.ts +1 -0
- package/src/rigs/index.ts +540 -0
- package/src/sandbox/fleet.ts +97 -18
- package/src/sandbox/routing.ts +6 -1
- package/src/sandbox-types.ts +17 -5
|
@@ -8,24 +8,23 @@ import {
|
|
|
8
8
|
type AccessGrant,
|
|
9
9
|
type CapabilityCatalogResponse,
|
|
10
10
|
type CapabilityInstallation,
|
|
11
|
-
type CapabilityKind,
|
|
12
11
|
type CreateCapabilityCatalogItemRequest,
|
|
13
12
|
type EnableCapabilityRequest,
|
|
14
13
|
type McpServerConnectionRef,
|
|
15
14
|
} from "@opengeni/contracts";
|
|
16
15
|
import {
|
|
17
|
-
|
|
16
|
+
decryptVariableSetValue,
|
|
18
17
|
decryptedCapabilityHeaders,
|
|
19
18
|
disableCapabilityInstallation,
|
|
20
19
|
enableCapabilityInstallation,
|
|
21
20
|
enablePackInstallation,
|
|
22
|
-
|
|
21
|
+
encryptVariableSetValue,
|
|
23
22
|
getCapabilityCatalogItem,
|
|
24
23
|
getCapabilityInstallation,
|
|
25
24
|
getConnectionMetadata,
|
|
26
25
|
getPackInstallation,
|
|
27
26
|
getStoredCapabilityHeaderCiphertext,
|
|
28
|
-
|
|
27
|
+
getVariableSet,
|
|
29
28
|
listCapabilityCatalogItems,
|
|
30
29
|
listCapabilityInstallations,
|
|
31
30
|
listEnabledMcpCapabilityServers,
|
|
@@ -37,8 +36,13 @@ import {
|
|
|
37
36
|
type EnabledMcpCapabilityServer,
|
|
38
37
|
} from "@opengeni/db";
|
|
39
38
|
import { HTTPException } from "hono/http-exception";
|
|
40
|
-
import {
|
|
41
|
-
import {
|
|
39
|
+
import { validateVariableSetAttachment } from "./environments";
|
|
40
|
+
import {
|
|
41
|
+
assertPackSandboxImageCompatible,
|
|
42
|
+
listCapabilityPacks,
|
|
43
|
+
listWorkspaceCapabilityPacks,
|
|
44
|
+
resolveCapabilityPack,
|
|
45
|
+
} from "./packs";
|
|
42
46
|
|
|
43
47
|
const officialMcpRegistryUrl = "https://registry.modelcontextprotocol.io";
|
|
44
48
|
const firstPartyMcpServerIds = new Set(["opengeni", "files", "docs"]);
|
|
@@ -68,17 +72,27 @@ export async function buildCapabilityCatalog(input: {
|
|
|
68
72
|
listWorkspaceCapabilityPacks(input.db, input.workspaceId),
|
|
69
73
|
discoverBundledSkills(),
|
|
70
74
|
]);
|
|
71
|
-
const capabilityInstallationById = new Map(
|
|
72
|
-
|
|
75
|
+
const capabilityInstallationById = new Map(
|
|
76
|
+
capabilityInstallations.map((installation) => [installation.capabilityId, installation]),
|
|
77
|
+
);
|
|
78
|
+
const activePackIds = new Set(
|
|
79
|
+
packInstallations
|
|
80
|
+
.filter((installation) => installation.status === "active")
|
|
81
|
+
.map((installation) => installation.packId),
|
|
82
|
+
);
|
|
73
83
|
const builtInPackIds = new Set(listCapabilityPacks().map((pack) => pack.id));
|
|
74
84
|
const builtIns = [
|
|
75
|
-
...workspacePacks.map((pack) =>
|
|
85
|
+
...workspacePacks.map((pack) =>
|
|
86
|
+
packCatalogItem(pack, builtInPackIds.has(pack.id) ? "built_in" : "manual"),
|
|
87
|
+
),
|
|
76
88
|
...configuredMcpCatalogItems(input.settings),
|
|
77
89
|
...platformApiCatalogItems(),
|
|
78
90
|
...bundledSkills,
|
|
79
91
|
];
|
|
80
92
|
const items = dedupeCatalogItems([...builtIns, ...persistedItems])
|
|
81
|
-
.map((item) =>
|
|
93
|
+
.map((item) =>
|
|
94
|
+
applyCapabilityEnablement(item, capabilityInstallationById.get(item.id), activePackIds),
|
|
95
|
+
)
|
|
82
96
|
.sort(compareCatalogItems);
|
|
83
97
|
return {
|
|
84
98
|
items,
|
|
@@ -94,12 +108,21 @@ export async function createCatalogItem(input: {
|
|
|
94
108
|
}): Promise<CapabilityCatalogItem> {
|
|
95
109
|
const id = input.payload.id?.trim() || generatedCapabilityId(input.payload);
|
|
96
110
|
if (id.startsWith("pack:")) {
|
|
97
|
-
throw new HTTPException(422, {
|
|
111
|
+
throw new HTTPException(422, {
|
|
112
|
+
message: "packs are managed by OpenGeni and cannot be manually created",
|
|
113
|
+
});
|
|
98
114
|
}
|
|
99
|
-
const source =
|
|
115
|
+
const source =
|
|
116
|
+
input.payload.source === "built_in" ||
|
|
117
|
+
input.payload.source === "configured" ||
|
|
118
|
+
input.payload.source === "registry"
|
|
119
|
+
? "manual"
|
|
120
|
+
: input.payload.source;
|
|
100
121
|
const metadata = {
|
|
101
122
|
...input.payload.metadata,
|
|
102
|
-
...(input.payload.kind === "mcp" &&
|
|
123
|
+
...(input.payload.kind === "mcp" &&
|
|
124
|
+
input.payload.endpointUrl &&
|
|
125
|
+
!input.payload.metadata.mcpServerId
|
|
103
126
|
? { mcpServerId: mcpServerIdForCapability(id, input.payload.metadata) }
|
|
104
127
|
: {}),
|
|
105
128
|
};
|
|
@@ -131,9 +154,16 @@ export async function enableCapability(input: {
|
|
|
131
154
|
payload: EnableCapabilityRequest;
|
|
132
155
|
probeMcpServer?: McpCapabilityProbe;
|
|
133
156
|
}): Promise<CapabilityInstallation> {
|
|
134
|
-
const item = await requireCatalogItem(
|
|
157
|
+
const item = await requireCatalogItem(
|
|
158
|
+
input.db,
|
|
159
|
+
input.workspaceId,
|
|
160
|
+
input.settings,
|
|
161
|
+
input.capabilityId,
|
|
162
|
+
);
|
|
135
163
|
if (item.kind === "mcp" && !item.runtime.available) {
|
|
136
|
-
throw new HTTPException(422, {
|
|
164
|
+
throw new HTTPException(422, {
|
|
165
|
+
message: "MCP capabilities need a remote streamable HTTP endpoint before they can be enabled",
|
|
166
|
+
});
|
|
137
167
|
}
|
|
138
168
|
let installationMetadata = input.payload.metadata;
|
|
139
169
|
// Credential-header storage is written exclusively by this flow; strip the
|
|
@@ -162,7 +192,7 @@ export async function enableCapability(input: {
|
|
|
162
192
|
if (headers) {
|
|
163
193
|
const key = requireCapabilityHeaderEncryption(input.settings);
|
|
164
194
|
installationConfig.headersEncrypted = Object.fromEntries(
|
|
165
|
-
Object.entries(headers).map(([name, value]) => [name,
|
|
195
|
+
Object.entries(headers).map(([name, value]) => [name, encryptVariableSetValue(key, value)]),
|
|
166
196
|
);
|
|
167
197
|
}
|
|
168
198
|
}
|
|
@@ -173,49 +203,60 @@ export async function enableCapability(input: {
|
|
|
173
203
|
throw new HTTPException(404, { message: "pack not found" });
|
|
174
204
|
}
|
|
175
205
|
await assertPackSandboxImageCompatible(input.db, input.workspaceId, pack);
|
|
176
|
-
// The unified capability-enable path accepts an initial
|
|
177
|
-
// attachment (`payload.
|
|
206
|
+
// The unified capability-enable path accepts an initial variableSet
|
|
207
|
+
// attachment (`payload.variableSetId`), mirroring POST /packs/:id/enable:
|
|
178
208
|
// a request-supplied id is validated as a fresh attachment, otherwise the
|
|
179
209
|
// attachment stored by a previous enable is preserved and re-validated.
|
|
180
210
|
const existing = await getPackInstallation(input.db, input.workspaceId, packId);
|
|
181
|
-
const
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
211
|
+
const storedVariableSetId =
|
|
212
|
+
typeof existing?.metadata.variableSetId === "string"
|
|
213
|
+
? existing.metadata.variableSetId
|
|
214
|
+
: typeof existing?.metadata.environmentId === "string"
|
|
215
|
+
? existing.metadata.environmentId
|
|
216
|
+
: undefined;
|
|
217
|
+
const requestedVariableSetId = input.payload.variableSetId;
|
|
218
|
+
const variableSetId = requestedVariableSetId ?? storedVariableSetId;
|
|
219
|
+
if (pack.variableSet?.required && !variableSetId) {
|
|
185
220
|
throw new HTTPException(422, {
|
|
186
|
-
message: `pack ${packId} requires an
|
|
221
|
+
message: `pack ${packId} requires an variableSet attachment; pass variableSetId`,
|
|
187
222
|
});
|
|
188
223
|
}
|
|
189
|
-
if (
|
|
190
|
-
if (
|
|
224
|
+
if (variableSetId) {
|
|
225
|
+
if (requestedVariableSetId) {
|
|
191
226
|
// A fresh attachment: validate it like the packs enable endpoint does.
|
|
192
|
-
// The grant holds workspace:admin here, which implies
|
|
227
|
+
// The grant holds workspace:admin here, which implies variable-sets:use,
|
|
193
228
|
// so the attachment authorization succeeds for this caller.
|
|
194
|
-
const
|
|
229
|
+
const variableSet = await validateVariableSetAttachment(
|
|
195
230
|
{ settings: input.settings, db: input.db },
|
|
196
231
|
input.grant,
|
|
197
232
|
input.workspaceId,
|
|
198
|
-
|
|
233
|
+
requestedVariableSetId,
|
|
234
|
+
);
|
|
235
|
+
const missing = (pack.variableSet?.requiredVariables ?? []).filter(
|
|
236
|
+
(name) => !variableSet.variables.some((variable) => variable.name === name),
|
|
199
237
|
);
|
|
200
|
-
const missing = (pack.environment?.requiredVariables ?? [])
|
|
201
|
-
.filter((name) => !environment.variables.some((variable) => variable.name === name));
|
|
202
238
|
if (missing.length > 0) {
|
|
203
|
-
throw new HTTPException(422, {
|
|
239
|
+
throw new HTTPException(422, {
|
|
240
|
+
message: `variable set is missing required variable(s): ${missing.join(", ")}`,
|
|
241
|
+
});
|
|
204
242
|
}
|
|
205
243
|
} else {
|
|
206
244
|
// The stored attachment was authorized at pack-enable time, but the
|
|
207
|
-
//
|
|
245
|
+
// variableSet may have been deleted or its variables changed since;
|
|
208
246
|
// re-validate it like the packs enable endpoint does.
|
|
209
|
-
const
|
|
210
|
-
if (!
|
|
247
|
+
const variableSet = await getVariableSet(input.db, input.workspaceId, variableSetId);
|
|
248
|
+
if (!variableSet) {
|
|
211
249
|
throw new HTTPException(422, {
|
|
212
|
-
message: `the stored
|
|
250
|
+
message: `the stored variableSet attachment for pack ${packId} no longer exists; re-enable it with variableSetId`,
|
|
213
251
|
});
|
|
214
252
|
}
|
|
215
|
-
const missing = (pack.
|
|
216
|
-
|
|
253
|
+
const missing = (pack.variableSet?.requiredVariables ?? []).filter(
|
|
254
|
+
(name) => !variableSet.variables.some((variable) => variable.name === name),
|
|
255
|
+
);
|
|
217
256
|
if (missing.length > 0) {
|
|
218
|
-
throw new HTTPException(422, {
|
|
257
|
+
throw new HTTPException(422, {
|
|
258
|
+
message: `variable set is missing required variable(s): ${missing.join(", ")}`,
|
|
259
|
+
});
|
|
219
260
|
}
|
|
220
261
|
}
|
|
221
262
|
}
|
|
@@ -226,7 +267,7 @@ export async function enableCapability(input: {
|
|
|
226
267
|
metadata: {
|
|
227
268
|
...input.payload.metadata,
|
|
228
269
|
packVersion: pack.version,
|
|
229
|
-
...(
|
|
270
|
+
...(variableSetId ? { variableSetId } : {}),
|
|
230
271
|
},
|
|
231
272
|
});
|
|
232
273
|
}
|
|
@@ -247,7 +288,12 @@ export async function enableCapability(input: {
|
|
|
247
288
|
* credentials). Returns null when neither exists.
|
|
248
289
|
*/
|
|
249
290
|
async function resolveMcpCredentialHeaders(
|
|
250
|
-
input: {
|
|
291
|
+
input: {
|
|
292
|
+
db: Database;
|
|
293
|
+
workspaceId: string;
|
|
294
|
+
settings: Settings;
|
|
295
|
+
payload: EnableCapabilityRequest;
|
|
296
|
+
},
|
|
251
297
|
item: CapabilityCatalogItem,
|
|
252
298
|
): Promise<Record<string, string> | null> {
|
|
253
299
|
const provided = normalizedMcpCredentialHeaders(input.payload.headers);
|
|
@@ -257,13 +303,22 @@ async function resolveMcpCredentialHeaders(
|
|
|
257
303
|
requireCapabilityHeaderEncryption(input.settings);
|
|
258
304
|
return provided;
|
|
259
305
|
}
|
|
260
|
-
const storedCiphertext = await getStoredCapabilityHeaderCiphertext(
|
|
306
|
+
const storedCiphertext = await getStoredCapabilityHeaderCiphertext(
|
|
307
|
+
input.db,
|
|
308
|
+
input.workspaceId,
|
|
309
|
+
item.id,
|
|
310
|
+
);
|
|
261
311
|
if (!storedCiphertext) {
|
|
262
312
|
return null;
|
|
263
313
|
}
|
|
264
314
|
const key = requireCapabilityHeaderEncryption(input.settings);
|
|
265
315
|
try {
|
|
266
|
-
return Object.fromEntries(
|
|
316
|
+
return Object.fromEntries(
|
|
317
|
+
Object.entries(storedCiphertext).map(([name, value]) => [
|
|
318
|
+
name,
|
|
319
|
+
decryptVariableSetValue(key, value),
|
|
320
|
+
]),
|
|
321
|
+
);
|
|
267
322
|
} catch {
|
|
268
323
|
throw new HTTPException(422, {
|
|
269
324
|
message: `stored credential headers for "${item.name}" could not be decrypted; supply them again in the enable request "headers" field`,
|
|
@@ -271,13 +326,19 @@ async function resolveMcpCredentialHeaders(
|
|
|
271
326
|
}
|
|
272
327
|
}
|
|
273
328
|
|
|
274
|
-
function normalizedMcpCredentialHeaders(
|
|
275
|
-
|
|
329
|
+
function normalizedMcpCredentialHeaders(
|
|
330
|
+
headers: Record<string, string>,
|
|
331
|
+
): Record<string, string> | null {
|
|
332
|
+
const entries = Object.entries(headers)
|
|
333
|
+
.map(([name, value]) => [name.trim(), value] as const)
|
|
334
|
+
.filter(([name]) => name.length > 0);
|
|
276
335
|
if (entries.length === 0) {
|
|
277
336
|
return null;
|
|
278
337
|
}
|
|
279
338
|
if (entries.length > maxMcpCredentialHeaders) {
|
|
280
|
-
throw new HTTPException(422, {
|
|
339
|
+
throw new HTTPException(422, {
|
|
340
|
+
message: `an MCP capability supports at most ${maxMcpCredentialHeaders} credential headers`,
|
|
341
|
+
});
|
|
281
342
|
}
|
|
282
343
|
const seen = new Set<string>();
|
|
283
344
|
for (const [name, value] of entries) {
|
|
@@ -290,13 +351,17 @@ function normalizedMcpCredentialHeaders(headers: Record<string, string>): Record
|
|
|
290
351
|
}
|
|
291
352
|
seen.add(lower);
|
|
292
353
|
if (value.length === 0 || value.length > maxMcpCredentialHeaderValueLength) {
|
|
293
|
-
throw new HTTPException(422, {
|
|
354
|
+
throw new HTTPException(422, {
|
|
355
|
+
message: `credential header ${name} must be 1-${maxMcpCredentialHeaderValueLength} characters`,
|
|
356
|
+
});
|
|
294
357
|
}
|
|
295
358
|
// RFC 9110 §5.5: field values are HTAB / printable characters — reject
|
|
296
359
|
// all other control characters (they would also fail at the HTTP client).
|
|
297
360
|
// eslint-disable-next-line no-control-regex
|
|
298
361
|
if (/[\u0000-\u0008\u000A-\u001F\u007F]/.test(value)) {
|
|
299
|
-
throw new HTTPException(422, {
|
|
362
|
+
throw new HTTPException(422, {
|
|
363
|
+
message: `credential header ${name} contains forbidden control characters`,
|
|
364
|
+
});
|
|
300
365
|
}
|
|
301
366
|
}
|
|
302
367
|
return Object.fromEntries(entries);
|
|
@@ -308,7 +373,9 @@ async function validateMcpCapabilityConnectionRef(
|
|
|
308
373
|
ref: McpServerConnectionRef,
|
|
309
374
|
): Promise<McpServerConnectionRef> {
|
|
310
375
|
if (ref.subjectScope === "subject") {
|
|
311
|
-
throw new HTTPException(422, {
|
|
376
|
+
throw new HTTPException(422, {
|
|
377
|
+
message: "subject-owned connection refs are not supported for agent runtime use yet",
|
|
378
|
+
});
|
|
312
379
|
}
|
|
313
380
|
const normalized: McpServerConnectionRef = {
|
|
314
381
|
providerDomain: ref.providerDomain.trim(),
|
|
@@ -322,26 +389,44 @@ async function validateMcpCapabilityConnectionRef(
|
|
|
322
389
|
throw new HTTPException(422, { message: "connectionRef.providerDomain is required" });
|
|
323
390
|
}
|
|
324
391
|
if (!item.endpointUrl || !item.runtime.mcpServerId) {
|
|
325
|
-
throw new HTTPException(422, {
|
|
392
|
+
throw new HTTPException(422, {
|
|
393
|
+
message:
|
|
394
|
+
"MCP capabilities need a remote streamable HTTP endpoint before they can use a connectionRef",
|
|
395
|
+
});
|
|
326
396
|
}
|
|
327
397
|
if (!normalized.connectionId) {
|
|
328
398
|
return normalized;
|
|
329
399
|
}
|
|
330
|
-
const connection = await getConnectionMetadata(
|
|
400
|
+
const connection = await getConnectionMetadata(
|
|
401
|
+
input.db,
|
|
402
|
+
input.workspaceId,
|
|
403
|
+
normalized.connectionId,
|
|
404
|
+
input.grant.subjectId,
|
|
405
|
+
);
|
|
331
406
|
if (!connection) {
|
|
332
|
-
throw new HTTPException(422, {
|
|
407
|
+
throw new HTTPException(422, {
|
|
408
|
+
message: "connectionRef.connectionId does not reference a visible connection",
|
|
409
|
+
});
|
|
333
410
|
}
|
|
334
411
|
if (connection.subjectId !== null) {
|
|
335
|
-
throw new HTTPException(422, {
|
|
412
|
+
throw new HTTPException(422, {
|
|
413
|
+
message: "agent runtime connection refs must reference workspace-shared connections in I1",
|
|
414
|
+
});
|
|
336
415
|
}
|
|
337
416
|
if (connection.status !== "active") {
|
|
338
|
-
throw new HTTPException(422, {
|
|
417
|
+
throw new HTTPException(422, {
|
|
418
|
+
message: `connectionRef.connectionId is not active (${connection.status})`,
|
|
419
|
+
});
|
|
339
420
|
}
|
|
340
421
|
if (connection.providerDomain !== normalized.providerDomain) {
|
|
341
|
-
throw new HTTPException(422, {
|
|
422
|
+
throw new HTTPException(422, {
|
|
423
|
+
message: "connectionRef.providerDomain does not match the referenced connection",
|
|
424
|
+
});
|
|
342
425
|
}
|
|
343
426
|
if (normalized.kind && connection.kind !== normalized.kind) {
|
|
344
|
-
throw new HTTPException(422, {
|
|
427
|
+
throw new HTTPException(422, {
|
|
428
|
+
message: "connectionRef.kind does not match the referenced connection",
|
|
429
|
+
});
|
|
345
430
|
}
|
|
346
431
|
return normalized;
|
|
347
432
|
}
|
|
@@ -383,13 +468,17 @@ function requiredCapabilityHeaders(metadata: Record<string, unknown>): string[]
|
|
|
383
468
|
if (!Array.isArray(value)) {
|
|
384
469
|
return [];
|
|
385
470
|
}
|
|
386
|
-
return value
|
|
471
|
+
return value
|
|
472
|
+
.filter((name): name is string => typeof name === "string" && name.trim().length > 0)
|
|
473
|
+
.map((name) => name.trim());
|
|
387
474
|
}
|
|
388
475
|
|
|
389
476
|
function requireCapabilityHeaderEncryption(settings: Settings): Uint8Array {
|
|
390
477
|
const key = environmentsEncryptionKeyBytes(settings);
|
|
391
478
|
if (!key) {
|
|
392
|
-
throw new HTTPException(503, {
|
|
479
|
+
throw new HTTPException(503, {
|
|
480
|
+
message: "MCP credential headers require OPENGENI_ENVIRONMENTS_ENCRYPTION_KEY",
|
|
481
|
+
});
|
|
393
482
|
}
|
|
394
483
|
return key;
|
|
395
484
|
}
|
|
@@ -406,7 +495,9 @@ export type McpCapabilityProbeResult = {
|
|
|
406
495
|
toolCount: number;
|
|
407
496
|
};
|
|
408
497
|
|
|
409
|
-
export type McpCapabilityProbe = (
|
|
498
|
+
export type McpCapabilityProbe = (
|
|
499
|
+
input: McpCapabilityProbeInput,
|
|
500
|
+
) => Promise<McpCapabilityProbeResult>;
|
|
410
501
|
|
|
411
502
|
export async function validateMcpCapabilityConnection(
|
|
412
503
|
item: CapabilityCatalogItem,
|
|
@@ -417,7 +508,9 @@ export async function validateMcpCapabilityConnection(
|
|
|
417
508
|
return {};
|
|
418
509
|
}
|
|
419
510
|
if (!item.endpointUrl || !item.runtime.mcpServerId) {
|
|
420
|
-
throw new HTTPException(422, {
|
|
511
|
+
throw new HTTPException(422, {
|
|
512
|
+
message: "MCP capabilities need a remote streamable HTTP endpoint before they can be enabled",
|
|
513
|
+
});
|
|
421
514
|
}
|
|
422
515
|
try {
|
|
423
516
|
const result = await probe({
|
|
@@ -436,15 +529,20 @@ export async function validateMcpCapabilityConnection(
|
|
|
436
529
|
};
|
|
437
530
|
} catch (error) {
|
|
438
531
|
throw new HTTPException(422, {
|
|
439
|
-
message: `MCP capability "${item.name}" could not be enabled because
|
|
532
|
+
message: `MCP capability "${item.name}" could not be enabled because ${mcpProbeErrorMessage(error, item.endpointUrl)}`,
|
|
440
533
|
});
|
|
441
534
|
}
|
|
442
535
|
}
|
|
443
536
|
|
|
444
|
-
async function probeStreamableHttpMcpServer(
|
|
537
|
+
async function probeStreamableHttpMcpServer(
|
|
538
|
+
input: McpCapabilityProbeInput,
|
|
539
|
+
): Promise<McpCapabilityProbeResult> {
|
|
445
540
|
const controller = new AbortController();
|
|
446
541
|
const timeout = setTimeout(() => controller.abort(), input.timeoutMs);
|
|
447
|
-
const client = new Client(
|
|
542
|
+
const client = new Client(
|
|
543
|
+
{ name: "opengeni-capability-probe", version: "0.1.0" },
|
|
544
|
+
{ capabilities: {} },
|
|
545
|
+
);
|
|
448
546
|
try {
|
|
449
547
|
const transport = new StreamableHTTPClientTransport(new URL(input.url), {
|
|
450
548
|
requestInit: {
|
|
@@ -452,8 +550,14 @@ async function probeStreamableHttpMcpServer(input: McpCapabilityProbeInput): Pro
|
|
|
452
550
|
...(input.headers ? { headers: input.headers } : {}),
|
|
453
551
|
},
|
|
454
552
|
});
|
|
455
|
-
await client.connect(transport as unknown as Transport, {
|
|
456
|
-
|
|
553
|
+
await client.connect(transport as unknown as Transport, {
|
|
554
|
+
timeout: input.timeoutMs,
|
|
555
|
+
maxTotalTimeout: input.timeoutMs,
|
|
556
|
+
});
|
|
557
|
+
const tools = await client.listTools(undefined, {
|
|
558
|
+
timeout: input.timeoutMs,
|
|
559
|
+
maxTotalTimeout: input.timeoutMs,
|
|
560
|
+
});
|
|
457
561
|
return { toolCount: tools.tools.length };
|
|
458
562
|
} finally {
|
|
459
563
|
clearTimeout(timeout);
|
|
@@ -461,9 +565,17 @@ async function probeStreamableHttpMcpServer(input: McpCapabilityProbeInput): Pro
|
|
|
461
565
|
}
|
|
462
566
|
}
|
|
463
567
|
|
|
464
|
-
function mcpProbeErrorMessage(error: unknown): string {
|
|
568
|
+
function mcpProbeErrorMessage(error: unknown, endpointUrl: string): string {
|
|
465
569
|
const message = error instanceof Error ? error.message : String(error);
|
|
466
|
-
|
|
570
|
+
const normalized = message.replace(/\s+/g, " ").trim();
|
|
571
|
+
if (
|
|
572
|
+
/404|405|not found|unexpected token|not valid json|invalid json|failed to parse|streamable http error|unable to connect|fetch failed|econnrefused|enotfound|timeout|aborted/i.test(
|
|
573
|
+
normalized,
|
|
574
|
+
)
|
|
575
|
+
) {
|
|
576
|
+
return `OpenGeni could not reach a valid Streamable HTTP MCP server at ${endpointUrl}. Check the endpoint URL or choose a different catalog entry.`;
|
|
577
|
+
}
|
|
578
|
+
return `OpenGeni could not initialize ${endpointUrl}: ${normalized.slice(0, 500) || "unknown error"}`;
|
|
467
579
|
}
|
|
468
580
|
|
|
469
581
|
export async function disableCapability(input: {
|
|
@@ -473,13 +585,26 @@ export async function disableCapability(input: {
|
|
|
473
585
|
settings: Settings;
|
|
474
586
|
capabilityId: string;
|
|
475
587
|
}): Promise<CapabilityInstallation> {
|
|
476
|
-
const item = await requireCatalogItem(
|
|
588
|
+
const item = await requireCatalogItem(
|
|
589
|
+
input.db,
|
|
590
|
+
input.workspaceId,
|
|
591
|
+
input.settings,
|
|
592
|
+
input.capabilityId,
|
|
593
|
+
);
|
|
477
594
|
if ((item.source === "built_in" || item.source === "configured") && item.kind !== "pack") {
|
|
478
|
-
throw new HTTPException(409, {
|
|
595
|
+
throw new HTTPException(409, {
|
|
596
|
+
message:
|
|
597
|
+
"built-in and configured capabilities are always available; remove them from configuration to disable them",
|
|
598
|
+
});
|
|
479
599
|
}
|
|
480
600
|
if (item.kind === "pack") {
|
|
481
|
-
await updatePackInstallationStatus(
|
|
482
|
-
|
|
601
|
+
await updatePackInstallationStatus(
|
|
602
|
+
input.db,
|
|
603
|
+
input.workspaceId,
|
|
604
|
+
packIdFromCapabilityId(item.id),
|
|
605
|
+
"disabled",
|
|
606
|
+
).catch(() => undefined);
|
|
607
|
+
if (!(await getCapabilityInstallation(input.db, input.workspaceId, item.id))) {
|
|
483
608
|
await enableCapabilityInstallation(input.db, {
|
|
484
609
|
accountId: input.accountId,
|
|
485
610
|
workspaceId: input.workspaceId,
|
|
@@ -489,18 +614,25 @@ export async function disableCapability(input: {
|
|
|
489
614
|
config: {},
|
|
490
615
|
});
|
|
491
616
|
}
|
|
492
|
-
} else if (!await getCapabilityInstallation(input.db, input.workspaceId, item.id)) {
|
|
617
|
+
} else if (!(await getCapabilityInstallation(input.db, input.workspaceId, item.id))) {
|
|
493
618
|
throw new HTTPException(409, { message: "capability is not currently enabled" });
|
|
494
619
|
}
|
|
495
620
|
return await disableCapabilityInstallation(input.db, input.workspaceId, item.id);
|
|
496
621
|
}
|
|
497
622
|
|
|
498
|
-
export async function settingsWithEnabledCapabilityMcpServers(
|
|
623
|
+
export async function settingsWithEnabledCapabilityMcpServers(
|
|
624
|
+
db: Database,
|
|
625
|
+
workspaceId: string,
|
|
626
|
+
settings: Settings,
|
|
627
|
+
): Promise<Settings> {
|
|
499
628
|
const enabled = await listEnabledMcpCapabilityServers(db, workspaceId);
|
|
500
629
|
return settingsWithMcpCapabilityServers(settings, enabled);
|
|
501
630
|
}
|
|
502
631
|
|
|
503
|
-
export function settingsWithMcpCapabilityServers(
|
|
632
|
+
export function settingsWithMcpCapabilityServers(
|
|
633
|
+
settings: Settings,
|
|
634
|
+
enabled: EnabledMcpCapabilityServer[],
|
|
635
|
+
): Settings {
|
|
504
636
|
if (enabled.length === 0) {
|
|
505
637
|
return settings;
|
|
506
638
|
}
|
|
@@ -515,18 +647,22 @@ export function settingsWithMcpCapabilityServers(settings: Settings, enabled: En
|
|
|
515
647
|
// connect time and break agent turns; leave it out of the run.
|
|
516
648
|
return [];
|
|
517
649
|
}
|
|
518
|
-
return [
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
650
|
+
return [
|
|
651
|
+
{
|
|
652
|
+
id: server.id,
|
|
653
|
+
name: server.name,
|
|
654
|
+
url: server.url,
|
|
655
|
+
...(server.allowedTools ? { allowedTools: server.allowedTools } : {}),
|
|
656
|
+
...(server.timeoutMs ? { timeoutMs: server.timeoutMs } : {}),
|
|
657
|
+
cacheToolsList: server.cacheToolsList ?? false,
|
|
658
|
+
...(headers && headers !== "unavailable" ? { headers } : {}),
|
|
659
|
+
...(server.connectionRef ? { connectionRef: server.connectionRef } : {}),
|
|
660
|
+
},
|
|
661
|
+
];
|
|
528
662
|
});
|
|
529
|
-
return dynamicServers.length
|
|
663
|
+
return dynamicServers.length
|
|
664
|
+
? { ...settings, mcpServers: [...settings.mcpServers, ...dynamicServers] }
|
|
665
|
+
: settings;
|
|
530
666
|
}
|
|
531
667
|
|
|
532
668
|
export async function discoverMcpRegistryCapabilities(input: {
|
|
@@ -588,19 +724,25 @@ export { officialMcpRegistryUrl };
|
|
|
588
724
|
|
|
589
725
|
type McpRegistryFetch = (input: URL, init?: RequestInit) => Promise<Response>;
|
|
590
726
|
|
|
591
|
-
async function fetchMcpRegistryPage(
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
727
|
+
async function fetchMcpRegistryPage(
|
|
728
|
+
url: URL,
|
|
729
|
+
options: {
|
|
730
|
+
fetchImpl?: McpRegistryFetch;
|
|
731
|
+
timeoutMs?: number;
|
|
732
|
+
} = {},
|
|
733
|
+
): Promise<McpRegistryPage> {
|
|
595
734
|
const fetchImpl = options.fetchImpl ?? fetch;
|
|
596
735
|
const controller = new AbortController();
|
|
597
|
-
const timeout = setTimeout(
|
|
736
|
+
const timeout = setTimeout(
|
|
737
|
+
() => controller.abort(),
|
|
738
|
+
options.timeoutMs ?? mcpRegistryFetchTimeoutMs,
|
|
739
|
+
);
|
|
598
740
|
try {
|
|
599
741
|
const response = await fetchImpl(url, { signal: controller.signal });
|
|
600
742
|
if (!response.ok) {
|
|
601
743
|
throw new HTTPException(502, { message: `MCP registry returned ${response.status}` });
|
|
602
744
|
}
|
|
603
|
-
return await response.json() as McpRegistryPage;
|
|
745
|
+
return (await response.json()) as McpRegistryPage;
|
|
604
746
|
} catch (error) {
|
|
605
747
|
if (error instanceof HTTPException) {
|
|
606
748
|
throw error;
|
|
@@ -616,16 +758,26 @@ async function fetchMcpRegistryPage(url: URL, options: {
|
|
|
616
758
|
}
|
|
617
759
|
}
|
|
618
760
|
|
|
619
|
-
async function requireCatalogItem(
|
|
761
|
+
async function requireCatalogItem(
|
|
762
|
+
db: Database,
|
|
763
|
+
workspaceId: string,
|
|
764
|
+
settings: Settings,
|
|
765
|
+
capabilityId: string,
|
|
766
|
+
): Promise<CapabilityCatalogItem> {
|
|
620
767
|
const catalog = await buildCapabilityCatalog({ db, workspaceId, settings });
|
|
621
|
-
const item =
|
|
768
|
+
const item =
|
|
769
|
+
catalog.items.find((candidate) => candidate.id === capabilityId) ??
|
|
770
|
+
(await getCapabilityCatalogItem(db, workspaceId, capabilityId));
|
|
622
771
|
if (!item) {
|
|
623
772
|
throw new HTTPException(404, { message: "capability not found" });
|
|
624
773
|
}
|
|
625
774
|
return item;
|
|
626
775
|
}
|
|
627
776
|
|
|
628
|
-
function packCatalogItem(
|
|
777
|
+
function packCatalogItem(
|
|
778
|
+
pack: ReturnType<typeof listCapabilityPacks>[number],
|
|
779
|
+
source: "built_in" | "manual",
|
|
780
|
+
): CapabilityCatalogItem {
|
|
629
781
|
return CapabilityCatalogItem.parse({
|
|
630
782
|
id: `pack:${pack.id}`,
|
|
631
783
|
kind: "pack",
|
|
@@ -654,28 +806,32 @@ function packCatalogItem(pack: ReturnType<typeof listCapabilityPacks>[number], s
|
|
|
654
806
|
}
|
|
655
807
|
|
|
656
808
|
function configuredMcpCatalogItems(settings: Settings): CapabilityCatalogItem[] {
|
|
657
|
-
return settings.mcpServers.map((server) =>
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
809
|
+
return settings.mcpServers.map((server) =>
|
|
810
|
+
CapabilityCatalogItem.parse({
|
|
811
|
+
id: `mcp:${server.id}`,
|
|
812
|
+
kind: "mcp",
|
|
813
|
+
source: firstPartyMcpServerIds.has(server.id) ? "built_in" : "configured",
|
|
814
|
+
name: server.name ?? server.id,
|
|
815
|
+
description: firstPartyMcpDescription(server.id),
|
|
816
|
+
category: firstPartyMcpServerIds.has(server.id) ? "platform" : "configured",
|
|
817
|
+
tags: ["mcp", ...(server.allowedTools?.length ? ["limited-tools"] : [])],
|
|
818
|
+
endpointUrl: server.url,
|
|
819
|
+
tools: [{ kind: "mcp", id: server.id }],
|
|
820
|
+
runtime: {
|
|
821
|
+
available: true,
|
|
822
|
+
mcpServerId: server.id,
|
|
823
|
+
transport: "streamable-http",
|
|
824
|
+
notes: firstPartyMcpServerIds.has(server.id)
|
|
825
|
+
? "Available from OpenGeni runtime configuration."
|
|
826
|
+
: "Configured through OPENGENI_MCP_SERVERS.",
|
|
827
|
+
},
|
|
828
|
+
metadata: {
|
|
829
|
+
mcpServerId: server.id,
|
|
830
|
+
allowedTools: server.allowedTools ?? [],
|
|
831
|
+
cacheToolsList: server.cacheToolsList,
|
|
832
|
+
},
|
|
833
|
+
}),
|
|
834
|
+
);
|
|
679
835
|
}
|
|
680
836
|
|
|
681
837
|
function platformApiCatalogItems(): CapabilityCatalogItem[] {
|
|
@@ -712,76 +868,89 @@ function platformApiCatalogItems(): CapabilityCatalogItem[] {
|
|
|
712
868
|
tags: ["api", "schedules", "agents"],
|
|
713
869
|
endpointPath: "/v1/workspaces/{workspaceId}/scheduled-tasks",
|
|
714
870
|
},
|
|
715
|
-
].map((item) =>
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
871
|
+
].map((item) =>
|
|
872
|
+
CapabilityCatalogItem.parse({
|
|
873
|
+
id: item.id,
|
|
874
|
+
name: item.name,
|
|
875
|
+
description: item.description,
|
|
876
|
+
category: item.category,
|
|
877
|
+
tags: item.tags,
|
|
878
|
+
kind: "api",
|
|
879
|
+
source: "built_in",
|
|
880
|
+
runtime: {
|
|
881
|
+
available: true,
|
|
882
|
+
notes: "Available through the OpenGeni API.",
|
|
883
|
+
},
|
|
884
|
+
metadata: {
|
|
885
|
+
endpointPath: item.endpointPath,
|
|
886
|
+
},
|
|
887
|
+
}),
|
|
888
|
+
);
|
|
731
889
|
}
|
|
732
890
|
|
|
733
891
|
async function discoverBundledSkills(): Promise<CapabilityCatalogItem[]> {
|
|
734
|
-
const skillsDir = new URL(
|
|
892
|
+
const skillsDir = new URL(
|
|
893
|
+
"../../../../packages/runtime/src/bundled_hashicorp_terraform_skills/",
|
|
894
|
+
import.meta.url,
|
|
895
|
+
);
|
|
735
896
|
try {
|
|
736
897
|
const entries = await readdir(skillsDir, { withFileTypes: true });
|
|
737
|
-
const skills = await Promise.all(
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
898
|
+
const skills = await Promise.all(
|
|
899
|
+
entries
|
|
900
|
+
.filter((entry) => entry.isDirectory())
|
|
901
|
+
.map(async (entry) => {
|
|
902
|
+
const skill = await readSkillMetadata(
|
|
903
|
+
new URL(`${entry.name}/SKILL.md`, skillsDir),
|
|
904
|
+
entry.name,
|
|
905
|
+
);
|
|
906
|
+
return CapabilityCatalogItem.parse({
|
|
907
|
+
id: `skill:${entry.name}`,
|
|
908
|
+
kind: "skill",
|
|
909
|
+
source: "built_in",
|
|
910
|
+
name: skill.name,
|
|
911
|
+
description: skill.description,
|
|
912
|
+
category: skill.category,
|
|
913
|
+
tags: ["skill", skill.category],
|
|
914
|
+
runtime: {
|
|
915
|
+
available: true,
|
|
916
|
+
notes: "Bundled into the sandbox skill library.",
|
|
917
|
+
},
|
|
918
|
+
metadata: {
|
|
919
|
+
path: `packages/runtime/src/bundled_hashicorp_terraform_skills/${entry.name}/SKILL.md`,
|
|
920
|
+
},
|
|
921
|
+
});
|
|
922
|
+
}),
|
|
923
|
+
);
|
|
758
924
|
return skills;
|
|
759
925
|
} catch {
|
|
760
926
|
return [];
|
|
761
927
|
}
|
|
762
928
|
}
|
|
763
929
|
|
|
764
|
-
async function readSkillMetadata(
|
|
930
|
+
async function readSkillMetadata(
|
|
931
|
+
url: URL,
|
|
932
|
+
fallbackName: string,
|
|
933
|
+
): Promise<{ name: string; description: string | null; category: string }> {
|
|
765
934
|
const content = await readFile(url, "utf8");
|
|
766
935
|
const frontMatter = content.match(/^---\n([\s\S]*?)\n---/);
|
|
767
936
|
const frontMatterBody = frontMatter?.[1] ?? "";
|
|
768
937
|
const name = frontMatterBody.match(/^name:\s*(.+)$/m)?.[1]?.trim() || fallbackName;
|
|
769
|
-
const blockDescription = frontMatterBody
|
|
938
|
+
const blockDescription = frontMatterBody
|
|
939
|
+
.match(/^description:\s*>-\s*\n([\s\S]*?)(?:\n[a-zA-Z_-]+:|\n?$)/m)?.[1]
|
|
770
940
|
?.split("\n")
|
|
771
941
|
.map((line) => line.trim())
|
|
772
942
|
.filter(Boolean)
|
|
773
943
|
.join(" ");
|
|
774
944
|
const inlineDescription = frontMatterBody.match(/^description:\s*(?!>-\s*$)(.+)$/m)?.[1]?.trim();
|
|
775
|
-
const description =
|
|
776
|
-
|| inlineDescription
|
|
777
|
-
|| content.match(/^#\s+(.+)$/m)?.[1]?.trim()
|
|
778
|
-
|| null;
|
|
945
|
+
const description =
|
|
946
|
+
blockDescription || inlineDescription || content.match(/^#\s+(.+)$/m)?.[1]?.trim() || null;
|
|
779
947
|
const lower = `${fallbackName} ${name} ${description ?? ""}`.toLowerCase();
|
|
780
|
-
const category =
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
948
|
+
const category =
|
|
949
|
+
lower.includes("social") || lower.includes("marketing")
|
|
950
|
+
? "marketing"
|
|
951
|
+
: lower.includes("checkov") || lower.includes("terraform") || lower.includes("azure")
|
|
952
|
+
? "infrastructure"
|
|
953
|
+
: "general";
|
|
785
954
|
return { name, description, category };
|
|
786
955
|
}
|
|
787
956
|
|
|
@@ -793,7 +962,8 @@ export function applyCapabilityEnablement(
|
|
|
793
962
|
if (item.kind === "pack") {
|
|
794
963
|
// Pack enablement lives in pack_installations regardless of whether the
|
|
795
964
|
// pack is built in or registered from a workspace manifest.
|
|
796
|
-
const enabled =
|
|
965
|
+
const enabled =
|
|
966
|
+
activePackIds.has(packIdFromCapabilityId(item.id)) || installation?.status === "active";
|
|
797
967
|
return {
|
|
798
968
|
...item,
|
|
799
969
|
enabled,
|
|
@@ -823,13 +993,19 @@ export function applyCapabilityEnablement(
|
|
|
823
993
|
* the enable path — see enableCapability). Headers-enabled and credential-
|
|
824
994
|
* free installations never set it, so this returns null for them.
|
|
825
995
|
*/
|
|
826
|
-
function installationConnectionRef(
|
|
996
|
+
function installationConnectionRef(
|
|
997
|
+
config: Record<string, unknown>,
|
|
998
|
+
): CapabilityCatalogItem["connectionRef"] {
|
|
827
999
|
const ref = config.connectionRef;
|
|
828
1000
|
if (!ref || typeof ref !== "object") {
|
|
829
1001
|
return null;
|
|
830
1002
|
}
|
|
831
1003
|
const { connectionId, providerDomain, kind } = ref as Record<string, unknown>;
|
|
832
|
-
if (
|
|
1004
|
+
if (
|
|
1005
|
+
typeof connectionId !== "string" ||
|
|
1006
|
+
typeof providerDomain !== "string" ||
|
|
1007
|
+
typeof kind !== "string"
|
|
1008
|
+
) {
|
|
833
1009
|
return null;
|
|
834
1010
|
}
|
|
835
1011
|
return { connectionId, providerDomain, kind };
|
|
@@ -861,7 +1037,11 @@ function firstPartyMcpDescription(id: string): string | null {
|
|
|
861
1037
|
}
|
|
862
1038
|
|
|
863
1039
|
function generatedCapabilityId(payload: CreateCapabilityCatalogItemRequest): string {
|
|
864
|
-
const source = [
|
|
1040
|
+
const source = [
|
|
1041
|
+
payload.kind,
|
|
1042
|
+
payload.name,
|
|
1043
|
+
payload.endpointUrl ?? payload.installUrl ?? payload.homepageUrl ?? "",
|
|
1044
|
+
].join(":");
|
|
865
1045
|
return `${payload.kind}:${slugify(payload.name)}-${shortHash(source)}`;
|
|
866
1046
|
}
|
|
867
1047
|
|
|
@@ -882,7 +1062,13 @@ function uniqueStrings(values: string[]): string[] {
|
|
|
882
1062
|
}
|
|
883
1063
|
|
|
884
1064
|
function slugify(value: string): string {
|
|
885
|
-
return
|
|
1065
|
+
return (
|
|
1066
|
+
value
|
|
1067
|
+
.toLowerCase()
|
|
1068
|
+
.replace(/[^a-z0-9_-]+/g, "-")
|
|
1069
|
+
.replace(/^-+|-+$/g, "")
|
|
1070
|
+
.slice(0, 60) || "capability"
|
|
1071
|
+
);
|
|
886
1072
|
}
|
|
887
1073
|
|
|
888
1074
|
function shortHash(value: string): string {
|
|
@@ -946,7 +1132,9 @@ function mcpRegistryEntryToCatalogItem(entry: McpRegistryEntry): CapabilityCatal
|
|
|
946
1132
|
if (official?.isLatest === false) {
|
|
947
1133
|
return null;
|
|
948
1134
|
}
|
|
949
|
-
const remote = server.remotes?.find(
|
|
1135
|
+
const remote = server.remotes?.find(
|
|
1136
|
+
(candidate) => candidate.type === "streamable-http" && candidate.url,
|
|
1137
|
+
);
|
|
950
1138
|
const endpointUrl = validUrl(remote?.url);
|
|
951
1139
|
if (!remote || !endpointUrl) {
|
|
952
1140
|
return null;
|
|
@@ -963,7 +1151,12 @@ function mcpRegistryEntryToCatalogItem(entry: McpRegistryEntry): CapabilityCatal
|
|
|
963
1151
|
name: server.title || server.name,
|
|
964
1152
|
description: server.description ?? null,
|
|
965
1153
|
category: "public-mcp",
|
|
966
|
-
tags: [
|
|
1154
|
+
tags: [
|
|
1155
|
+
"mcp",
|
|
1156
|
+
"public",
|
|
1157
|
+
"registry",
|
|
1158
|
+
...(requiredHeaders.length ? ["requires-credentials"] : []),
|
|
1159
|
+
],
|
|
967
1160
|
homepageUrl,
|
|
968
1161
|
endpointUrl,
|
|
969
1162
|
installUrl: homepageUrl,
|
|
@@ -973,9 +1166,10 @@ function mcpRegistryEntryToCatalogItem(entry: McpRegistryEntry): CapabilityCatal
|
|
|
973
1166
|
available: true,
|
|
974
1167
|
mcpServerId,
|
|
975
1168
|
transport: "streamable-http",
|
|
976
|
-
notes:
|
|
977
|
-
|
|
978
|
-
|
|
1169
|
+
notes:
|
|
1170
|
+
requiredHeaders.length === 0
|
|
1171
|
+
? "Remote MCP server from the official MCP Registry."
|
|
1172
|
+
: `This MCP requires credential header(s) ${requiredHeaders.join(", ")} supplied in the enable request.`,
|
|
979
1173
|
},
|
|
980
1174
|
metadata: {
|
|
981
1175
|
registry: "official_mcp_registry",
|
|
@@ -1016,7 +1210,10 @@ function catalogSearchText(item: CapabilityCatalogItem): string {
|
|
|
1016
1210
|
item.homepageUrl,
|
|
1017
1211
|
item.installUrl,
|
|
1018
1212
|
JSON.stringify(item.metadata),
|
|
1019
|
-
]
|
|
1213
|
+
]
|
|
1214
|
+
.filter(Boolean)
|
|
1215
|
+
.join(" ")
|
|
1216
|
+
.toLowerCase();
|
|
1020
1217
|
}
|
|
1021
1218
|
|
|
1022
1219
|
function capabilityInstallationRuntimeReady(
|
|
@@ -1033,17 +1230,22 @@ function capabilityInstallationRuntimeReady(
|
|
|
1033
1230
|
return false;
|
|
1034
1231
|
}
|
|
1035
1232
|
const connectivity = installation.metadata.mcpConnectivity;
|
|
1036
|
-
return
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1233
|
+
return (
|
|
1234
|
+
!!connectivity &&
|
|
1235
|
+
typeof connectivity === "object" &&
|
|
1236
|
+
"status" in connectivity &&
|
|
1237
|
+
(connectivity.status === "ok" || connectivity.status === "auth_deferred")
|
|
1238
|
+
);
|
|
1040
1239
|
}
|
|
1041
1240
|
|
|
1042
1241
|
/**
|
|
1043
1242
|
* Checks the redacted installation config (header names only) against the
|
|
1044
1243
|
* capability's declared credential requirements.
|
|
1045
1244
|
*/
|
|
1046
|
-
function storedCredentialHeadersSatisfy(
|
|
1245
|
+
function storedCredentialHeadersSatisfy(
|
|
1246
|
+
item: CapabilityCatalogItem,
|
|
1247
|
+
installation: CapabilityInstallation,
|
|
1248
|
+
): boolean {
|
|
1047
1249
|
if (storedConnectionRef(installation.config)) {
|
|
1048
1250
|
return true;
|
|
1049
1251
|
}
|
|
@@ -1061,6 +1263,10 @@ function storedCredentialHeadersSatisfy(item: CapabilityCatalogItem, installatio
|
|
|
1061
1263
|
|
|
1062
1264
|
function storedConnectionRef(config: Record<string, unknown>): boolean {
|
|
1063
1265
|
const ref = config.connectionRef;
|
|
1064
|
-
return
|
|
1065
|
-
|
|
1266
|
+
return (
|
|
1267
|
+
!!ref &&
|
|
1268
|
+
typeof ref === "object" &&
|
|
1269
|
+
!Array.isArray(ref) &&
|
|
1270
|
+
typeof (ref as { providerDomain?: unknown }).providerDomain === "string"
|
|
1271
|
+
);
|
|
1066
1272
|
}
|