@opengeni/core 0.4.6 → 0.4.8
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 +293 -32
- package/dist/index.js +1750 -472
- package/dist/index.js.map +1 -1
- package/package.json +17 -17
- package/src/access/index.ts +136 -57
- package/src/application/session-commands.ts +527 -0
- package/src/billing/limits.ts +76 -34
- package/src/dependencies.ts +67 -14
- package/src/domain/capabilities.ts +380 -181
- 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 +531 -255
- package/src/domain/workspace-members.ts +6 -2
- package/src/index.ts +3 -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
- package/src/workflow-wake-contract.ts +6 -0
|
@@ -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({
|
|
@@ -441,10 +534,15 @@ export async function validateMcpCapabilityConnection(
|
|
|
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);
|
|
@@ -465,8 +569,9 @@ 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();
|
|
467
571
|
if (
|
|
468
|
-
/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
|
|
469
|
-
|
|
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
|
+
)
|
|
470
575
|
) {
|
|
471
576
|
return `OpenGeni could not reach a valid Streamable HTTP MCP server at ${endpointUrl}. Check the endpoint URL or choose a different catalog entry.`;
|
|
472
577
|
}
|
|
@@ -480,13 +585,26 @@ export async function disableCapability(input: {
|
|
|
480
585
|
settings: Settings;
|
|
481
586
|
capabilityId: string;
|
|
482
587
|
}): Promise<CapabilityInstallation> {
|
|
483
|
-
const item = await requireCatalogItem(
|
|
588
|
+
const item = await requireCatalogItem(
|
|
589
|
+
input.db,
|
|
590
|
+
input.workspaceId,
|
|
591
|
+
input.settings,
|
|
592
|
+
input.capabilityId,
|
|
593
|
+
);
|
|
484
594
|
if ((item.source === "built_in" || item.source === "configured") && item.kind !== "pack") {
|
|
485
|
-
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
|
+
});
|
|
486
599
|
}
|
|
487
600
|
if (item.kind === "pack") {
|
|
488
|
-
await updatePackInstallationStatus(
|
|
489
|
-
|
|
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))) {
|
|
490
608
|
await enableCapabilityInstallation(input.db, {
|
|
491
609
|
accountId: input.accountId,
|
|
492
610
|
workspaceId: input.workspaceId,
|
|
@@ -496,18 +614,25 @@ export async function disableCapability(input: {
|
|
|
496
614
|
config: {},
|
|
497
615
|
});
|
|
498
616
|
}
|
|
499
|
-
} else if (!await getCapabilityInstallation(input.db, input.workspaceId, item.id)) {
|
|
617
|
+
} else if (!(await getCapabilityInstallation(input.db, input.workspaceId, item.id))) {
|
|
500
618
|
throw new HTTPException(409, { message: "capability is not currently enabled" });
|
|
501
619
|
}
|
|
502
620
|
return await disableCapabilityInstallation(input.db, input.workspaceId, item.id);
|
|
503
621
|
}
|
|
504
622
|
|
|
505
|
-
export async function settingsWithEnabledCapabilityMcpServers(
|
|
623
|
+
export async function settingsWithEnabledCapabilityMcpServers(
|
|
624
|
+
db: Database,
|
|
625
|
+
workspaceId: string,
|
|
626
|
+
settings: Settings,
|
|
627
|
+
): Promise<Settings> {
|
|
506
628
|
const enabled = await listEnabledMcpCapabilityServers(db, workspaceId);
|
|
507
629
|
return settingsWithMcpCapabilityServers(settings, enabled);
|
|
508
630
|
}
|
|
509
631
|
|
|
510
|
-
export function settingsWithMcpCapabilityServers(
|
|
632
|
+
export function settingsWithMcpCapabilityServers(
|
|
633
|
+
settings: Settings,
|
|
634
|
+
enabled: EnabledMcpCapabilityServer[],
|
|
635
|
+
): Settings {
|
|
511
636
|
if (enabled.length === 0) {
|
|
512
637
|
return settings;
|
|
513
638
|
}
|
|
@@ -522,18 +647,22 @@ export function settingsWithMcpCapabilityServers(settings: Settings, enabled: En
|
|
|
522
647
|
// connect time and break agent turns; leave it out of the run.
|
|
523
648
|
return [];
|
|
524
649
|
}
|
|
525
|
-
return [
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
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
|
+
];
|
|
535
662
|
});
|
|
536
|
-
return dynamicServers.length
|
|
663
|
+
return dynamicServers.length
|
|
664
|
+
? { ...settings, mcpServers: [...settings.mcpServers, ...dynamicServers] }
|
|
665
|
+
: settings;
|
|
537
666
|
}
|
|
538
667
|
|
|
539
668
|
export async function discoverMcpRegistryCapabilities(input: {
|
|
@@ -595,19 +724,25 @@ export { officialMcpRegistryUrl };
|
|
|
595
724
|
|
|
596
725
|
type McpRegistryFetch = (input: URL, init?: RequestInit) => Promise<Response>;
|
|
597
726
|
|
|
598
|
-
async function fetchMcpRegistryPage(
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
727
|
+
async function fetchMcpRegistryPage(
|
|
728
|
+
url: URL,
|
|
729
|
+
options: {
|
|
730
|
+
fetchImpl?: McpRegistryFetch;
|
|
731
|
+
timeoutMs?: number;
|
|
732
|
+
} = {},
|
|
733
|
+
): Promise<McpRegistryPage> {
|
|
602
734
|
const fetchImpl = options.fetchImpl ?? fetch;
|
|
603
735
|
const controller = new AbortController();
|
|
604
|
-
const timeout = setTimeout(
|
|
736
|
+
const timeout = setTimeout(
|
|
737
|
+
() => controller.abort(),
|
|
738
|
+
options.timeoutMs ?? mcpRegistryFetchTimeoutMs,
|
|
739
|
+
);
|
|
605
740
|
try {
|
|
606
741
|
const response = await fetchImpl(url, { signal: controller.signal });
|
|
607
742
|
if (!response.ok) {
|
|
608
743
|
throw new HTTPException(502, { message: `MCP registry returned ${response.status}` });
|
|
609
744
|
}
|
|
610
|
-
return await response.json() as McpRegistryPage;
|
|
745
|
+
return (await response.json()) as McpRegistryPage;
|
|
611
746
|
} catch (error) {
|
|
612
747
|
if (error instanceof HTTPException) {
|
|
613
748
|
throw error;
|
|
@@ -623,16 +758,26 @@ async function fetchMcpRegistryPage(url: URL, options: {
|
|
|
623
758
|
}
|
|
624
759
|
}
|
|
625
760
|
|
|
626
|
-
async function requireCatalogItem(
|
|
761
|
+
async function requireCatalogItem(
|
|
762
|
+
db: Database,
|
|
763
|
+
workspaceId: string,
|
|
764
|
+
settings: Settings,
|
|
765
|
+
capabilityId: string,
|
|
766
|
+
): Promise<CapabilityCatalogItem> {
|
|
627
767
|
const catalog = await buildCapabilityCatalog({ db, workspaceId, settings });
|
|
628
|
-
const item =
|
|
768
|
+
const item =
|
|
769
|
+
catalog.items.find((candidate) => candidate.id === capabilityId) ??
|
|
770
|
+
(await getCapabilityCatalogItem(db, workspaceId, capabilityId));
|
|
629
771
|
if (!item) {
|
|
630
772
|
throw new HTTPException(404, { message: "capability not found" });
|
|
631
773
|
}
|
|
632
774
|
return item;
|
|
633
775
|
}
|
|
634
776
|
|
|
635
|
-
function packCatalogItem(
|
|
777
|
+
function packCatalogItem(
|
|
778
|
+
pack: ReturnType<typeof listCapabilityPacks>[number],
|
|
779
|
+
source: "built_in" | "manual",
|
|
780
|
+
): CapabilityCatalogItem {
|
|
636
781
|
return CapabilityCatalogItem.parse({
|
|
637
782
|
id: `pack:${pack.id}`,
|
|
638
783
|
kind: "pack",
|
|
@@ -661,28 +806,32 @@ function packCatalogItem(pack: ReturnType<typeof listCapabilityPacks>[number], s
|
|
|
661
806
|
}
|
|
662
807
|
|
|
663
808
|
function configuredMcpCatalogItems(settings: Settings): CapabilityCatalogItem[] {
|
|
664
|
-
return settings.mcpServers.map((server) =>
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
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
|
+
);
|
|
686
835
|
}
|
|
687
836
|
|
|
688
837
|
function platformApiCatalogItems(): CapabilityCatalogItem[] {
|
|
@@ -719,76 +868,89 @@ function platformApiCatalogItems(): CapabilityCatalogItem[] {
|
|
|
719
868
|
tags: ["api", "schedules", "agents"],
|
|
720
869
|
endpointPath: "/v1/workspaces/{workspaceId}/scheduled-tasks",
|
|
721
870
|
},
|
|
722
|
-
].map((item) =>
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
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
|
+
);
|
|
738
889
|
}
|
|
739
890
|
|
|
740
891
|
async function discoverBundledSkills(): Promise<CapabilityCatalogItem[]> {
|
|
741
|
-
const skillsDir = new URL(
|
|
892
|
+
const skillsDir = new URL(
|
|
893
|
+
"../../../../packages/runtime/src/bundled_hashicorp_terraform_skills/",
|
|
894
|
+
import.meta.url,
|
|
895
|
+
);
|
|
742
896
|
try {
|
|
743
897
|
const entries = await readdir(skillsDir, { withFileTypes: true });
|
|
744
|
-
const skills = await Promise.all(
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
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
|
+
);
|
|
765
924
|
return skills;
|
|
766
925
|
} catch {
|
|
767
926
|
return [];
|
|
768
927
|
}
|
|
769
928
|
}
|
|
770
929
|
|
|
771
|
-
async function readSkillMetadata(
|
|
930
|
+
async function readSkillMetadata(
|
|
931
|
+
url: URL,
|
|
932
|
+
fallbackName: string,
|
|
933
|
+
): Promise<{ name: string; description: string | null; category: string }> {
|
|
772
934
|
const content = await readFile(url, "utf8");
|
|
773
935
|
const frontMatter = content.match(/^---\n([\s\S]*?)\n---/);
|
|
774
936
|
const frontMatterBody = frontMatter?.[1] ?? "";
|
|
775
937
|
const name = frontMatterBody.match(/^name:\s*(.+)$/m)?.[1]?.trim() || fallbackName;
|
|
776
|
-
const blockDescription = frontMatterBody
|
|
938
|
+
const blockDescription = frontMatterBody
|
|
939
|
+
.match(/^description:\s*>-\s*\n([\s\S]*?)(?:\n[a-zA-Z_-]+:|\n?$)/m)?.[1]
|
|
777
940
|
?.split("\n")
|
|
778
941
|
.map((line) => line.trim())
|
|
779
942
|
.filter(Boolean)
|
|
780
943
|
.join(" ");
|
|
781
944
|
const inlineDescription = frontMatterBody.match(/^description:\s*(?!>-\s*$)(.+)$/m)?.[1]?.trim();
|
|
782
|
-
const description =
|
|
783
|
-
|| inlineDescription
|
|
784
|
-
|| content.match(/^#\s+(.+)$/m)?.[1]?.trim()
|
|
785
|
-
|| null;
|
|
945
|
+
const description =
|
|
946
|
+
blockDescription || inlineDescription || content.match(/^#\s+(.+)$/m)?.[1]?.trim() || null;
|
|
786
947
|
const lower = `${fallbackName} ${name} ${description ?? ""}`.toLowerCase();
|
|
787
|
-
const category =
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
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";
|
|
792
954
|
return { name, description, category };
|
|
793
955
|
}
|
|
794
956
|
|
|
@@ -800,7 +962,8 @@ export function applyCapabilityEnablement(
|
|
|
800
962
|
if (item.kind === "pack") {
|
|
801
963
|
// Pack enablement lives in pack_installations regardless of whether the
|
|
802
964
|
// pack is built in or registered from a workspace manifest.
|
|
803
|
-
const enabled =
|
|
965
|
+
const enabled =
|
|
966
|
+
activePackIds.has(packIdFromCapabilityId(item.id)) || installation?.status === "active";
|
|
804
967
|
return {
|
|
805
968
|
...item,
|
|
806
969
|
enabled,
|
|
@@ -830,13 +993,19 @@ export function applyCapabilityEnablement(
|
|
|
830
993
|
* the enable path — see enableCapability). Headers-enabled and credential-
|
|
831
994
|
* free installations never set it, so this returns null for them.
|
|
832
995
|
*/
|
|
833
|
-
function installationConnectionRef(
|
|
996
|
+
function installationConnectionRef(
|
|
997
|
+
config: Record<string, unknown>,
|
|
998
|
+
): CapabilityCatalogItem["connectionRef"] {
|
|
834
999
|
const ref = config.connectionRef;
|
|
835
1000
|
if (!ref || typeof ref !== "object") {
|
|
836
1001
|
return null;
|
|
837
1002
|
}
|
|
838
1003
|
const { connectionId, providerDomain, kind } = ref as Record<string, unknown>;
|
|
839
|
-
if (
|
|
1004
|
+
if (
|
|
1005
|
+
typeof connectionId !== "string" ||
|
|
1006
|
+
typeof providerDomain !== "string" ||
|
|
1007
|
+
typeof kind !== "string"
|
|
1008
|
+
) {
|
|
840
1009
|
return null;
|
|
841
1010
|
}
|
|
842
1011
|
return { connectionId, providerDomain, kind };
|
|
@@ -868,7 +1037,11 @@ function firstPartyMcpDescription(id: string): string | null {
|
|
|
868
1037
|
}
|
|
869
1038
|
|
|
870
1039
|
function generatedCapabilityId(payload: CreateCapabilityCatalogItemRequest): string {
|
|
871
|
-
const source = [
|
|
1040
|
+
const source = [
|
|
1041
|
+
payload.kind,
|
|
1042
|
+
payload.name,
|
|
1043
|
+
payload.endpointUrl ?? payload.installUrl ?? payload.homepageUrl ?? "",
|
|
1044
|
+
].join(":");
|
|
872
1045
|
return `${payload.kind}:${slugify(payload.name)}-${shortHash(source)}`;
|
|
873
1046
|
}
|
|
874
1047
|
|
|
@@ -889,7 +1062,13 @@ function uniqueStrings(values: string[]): string[] {
|
|
|
889
1062
|
}
|
|
890
1063
|
|
|
891
1064
|
function slugify(value: string): string {
|
|
892
|
-
return
|
|
1065
|
+
return (
|
|
1066
|
+
value
|
|
1067
|
+
.toLowerCase()
|
|
1068
|
+
.replace(/[^a-z0-9_-]+/g, "-")
|
|
1069
|
+
.replace(/^-+|-+$/g, "")
|
|
1070
|
+
.slice(0, 60) || "capability"
|
|
1071
|
+
);
|
|
893
1072
|
}
|
|
894
1073
|
|
|
895
1074
|
function shortHash(value: string): string {
|
|
@@ -953,7 +1132,9 @@ function mcpRegistryEntryToCatalogItem(entry: McpRegistryEntry): CapabilityCatal
|
|
|
953
1132
|
if (official?.isLatest === false) {
|
|
954
1133
|
return null;
|
|
955
1134
|
}
|
|
956
|
-
const remote = server.remotes?.find(
|
|
1135
|
+
const remote = server.remotes?.find(
|
|
1136
|
+
(candidate) => candidate.type === "streamable-http" && candidate.url,
|
|
1137
|
+
);
|
|
957
1138
|
const endpointUrl = validUrl(remote?.url);
|
|
958
1139
|
if (!remote || !endpointUrl) {
|
|
959
1140
|
return null;
|
|
@@ -970,7 +1151,12 @@ function mcpRegistryEntryToCatalogItem(entry: McpRegistryEntry): CapabilityCatal
|
|
|
970
1151
|
name: server.title || server.name,
|
|
971
1152
|
description: server.description ?? null,
|
|
972
1153
|
category: "public-mcp",
|
|
973
|
-
tags: [
|
|
1154
|
+
tags: [
|
|
1155
|
+
"mcp",
|
|
1156
|
+
"public",
|
|
1157
|
+
"registry",
|
|
1158
|
+
...(requiredHeaders.length ? ["requires-credentials"] : []),
|
|
1159
|
+
],
|
|
974
1160
|
homepageUrl,
|
|
975
1161
|
endpointUrl,
|
|
976
1162
|
installUrl: homepageUrl,
|
|
@@ -980,9 +1166,10 @@ function mcpRegistryEntryToCatalogItem(entry: McpRegistryEntry): CapabilityCatal
|
|
|
980
1166
|
available: true,
|
|
981
1167
|
mcpServerId,
|
|
982
1168
|
transport: "streamable-http",
|
|
983
|
-
notes:
|
|
984
|
-
|
|
985
|
-
|
|
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.`,
|
|
986
1173
|
},
|
|
987
1174
|
metadata: {
|
|
988
1175
|
registry: "official_mcp_registry",
|
|
@@ -1023,7 +1210,10 @@ function catalogSearchText(item: CapabilityCatalogItem): string {
|
|
|
1023
1210
|
item.homepageUrl,
|
|
1024
1211
|
item.installUrl,
|
|
1025
1212
|
JSON.stringify(item.metadata),
|
|
1026
|
-
]
|
|
1213
|
+
]
|
|
1214
|
+
.filter(Boolean)
|
|
1215
|
+
.join(" ")
|
|
1216
|
+
.toLowerCase();
|
|
1027
1217
|
}
|
|
1028
1218
|
|
|
1029
1219
|
function capabilityInstallationRuntimeReady(
|
|
@@ -1040,17 +1230,22 @@ function capabilityInstallationRuntimeReady(
|
|
|
1040
1230
|
return false;
|
|
1041
1231
|
}
|
|
1042
1232
|
const connectivity = installation.metadata.mcpConnectivity;
|
|
1043
|
-
return
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1233
|
+
return (
|
|
1234
|
+
!!connectivity &&
|
|
1235
|
+
typeof connectivity === "object" &&
|
|
1236
|
+
"status" in connectivity &&
|
|
1237
|
+
(connectivity.status === "ok" || connectivity.status === "auth_deferred")
|
|
1238
|
+
);
|
|
1047
1239
|
}
|
|
1048
1240
|
|
|
1049
1241
|
/**
|
|
1050
1242
|
* Checks the redacted installation config (header names only) against the
|
|
1051
1243
|
* capability's declared credential requirements.
|
|
1052
1244
|
*/
|
|
1053
|
-
function storedCredentialHeadersSatisfy(
|
|
1245
|
+
function storedCredentialHeadersSatisfy(
|
|
1246
|
+
item: CapabilityCatalogItem,
|
|
1247
|
+
installation: CapabilityInstallation,
|
|
1248
|
+
): boolean {
|
|
1054
1249
|
if (storedConnectionRef(installation.config)) {
|
|
1055
1250
|
return true;
|
|
1056
1251
|
}
|
|
@@ -1068,6 +1263,10 @@ function storedCredentialHeadersSatisfy(item: CapabilityCatalogItem, installatio
|
|
|
1068
1263
|
|
|
1069
1264
|
function storedConnectionRef(config: Record<string, unknown>): boolean {
|
|
1070
1265
|
const ref = config.connectionRef;
|
|
1071
|
-
return
|
|
1072
|
-
|
|
1266
|
+
return (
|
|
1267
|
+
!!ref &&
|
|
1268
|
+
typeof ref === "object" &&
|
|
1269
|
+
!Array.isArray(ref) &&
|
|
1270
|
+
typeof (ref as { providerDomain?: unknown }).providerDomain === "string"
|
|
1271
|
+
);
|
|
1073
1272
|
}
|