@opengeni/capabilities 0.1.1
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/LICENSE +190 -0
- package/README.md +23 -0
- package/THIRD_PARTY_NOTICES +28 -0
- package/dist/auth.d.ts +3 -0
- package/dist/graphql.d.ts +52 -0
- package/dist/http.d.ts +15 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +1944 -0
- package/dist/index.js.map +1 -0
- package/dist/mcp-manifest.d.ts +25 -0
- package/dist/openapi.d.ts +69 -0
- package/dist/providers.d.ts +44 -0
- package/dist/revision.d.ts +4 -0
- package/dist/types.d.ts +94 -0
- package/package.json +46 -0
- package/src/auth.ts +171 -0
- package/src/graphql.ts +625 -0
- package/src/http.ts +131 -0
- package/src/index.ts +8 -0
- package/src/mcp-manifest.ts +90 -0
- package/src/openapi.ts +846 -0
- package/src/providers.ts +559 -0
- package/src/revision.ts +40 -0
- package/src/types.ts +126 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1944 @@
|
|
|
1
|
+
// src/types.ts
|
|
2
|
+
var IntegrationProtocolError = class extends Error {
|
|
3
|
+
constructor(code, message) {
|
|
4
|
+
super(message);
|
|
5
|
+
this.code = code;
|
|
6
|
+
this.name = "IntegrationProtocolError";
|
|
7
|
+
}
|
|
8
|
+
};
|
|
9
|
+
var IntegrationInvocationError = class extends Error {
|
|
10
|
+
constructor(code, message, outcome, retryable, status) {
|
|
11
|
+
super(message);
|
|
12
|
+
this.code = code;
|
|
13
|
+
this.outcome = outcome;
|
|
14
|
+
this.retryable = retryable;
|
|
15
|
+
this.status = status;
|
|
16
|
+
this.name = "IntegrationInvocationError";
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
// src/auth.ts
|
|
21
|
+
var forbiddenCredentialHeaders = /* @__PURE__ */ new Set([
|
|
22
|
+
"connection",
|
|
23
|
+
"content-length",
|
|
24
|
+
"host",
|
|
25
|
+
"proxy-authorization",
|
|
26
|
+
"proxy-connection",
|
|
27
|
+
"te",
|
|
28
|
+
"trailer",
|
|
29
|
+
"transfer-encoding",
|
|
30
|
+
"upgrade"
|
|
31
|
+
]);
|
|
32
|
+
var MAX_CREDENTIAL_PLACEMENTS = 32;
|
|
33
|
+
var MAX_CREDENTIAL_NAME_LENGTH = 256;
|
|
34
|
+
var MAX_CREDENTIAL_VALUE_LENGTH = 16384;
|
|
35
|
+
var headerNamePattern = /^[A-Za-z0-9!#$%&'*+.^_`|~-]+$/;
|
|
36
|
+
var queryNamePattern = /^[A-Za-z0-9._~-]+$/;
|
|
37
|
+
var cookieNamePattern = /^[!#$%&'*+.^_`|~0-9A-Za-z-]+$/;
|
|
38
|
+
function normalizeAudiencePath(path) {
|
|
39
|
+
if (!path?.trim()) return "/";
|
|
40
|
+
const normalized = path.startsWith("/") ? path : `/${path}`;
|
|
41
|
+
return normalized.endsWith("/") ? normalized : `${normalized}/`;
|
|
42
|
+
}
|
|
43
|
+
function assertCredentialAudience(credential, destination) {
|
|
44
|
+
let audience;
|
|
45
|
+
try {
|
|
46
|
+
audience = new URL(credential.audience.origin);
|
|
47
|
+
} catch {
|
|
48
|
+
throw new IntegrationInvocationError(
|
|
49
|
+
"credential_audience_invalid",
|
|
50
|
+
"Connection credential audience is invalid",
|
|
51
|
+
"not_started",
|
|
52
|
+
false
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
if (audience.origin !== destination.origin || audience.username || audience.password || audience.pathname !== "/" || audience.search || audience.hash) {
|
|
56
|
+
throw new IntegrationInvocationError(
|
|
57
|
+
"credential_audience_mismatch",
|
|
58
|
+
"Connection credential is not authorized for this integration destination",
|
|
59
|
+
"not_started",
|
|
60
|
+
false
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
const prefix = normalizeAudiencePath(credential.audience.pathPrefix);
|
|
64
|
+
const path = destination.pathname.endsWith("/") ? destination.pathname : `${destination.pathname}/`;
|
|
65
|
+
if (!path.startsWith(prefix)) {
|
|
66
|
+
throw new IntegrationInvocationError(
|
|
67
|
+
"credential_path_mismatch",
|
|
68
|
+
"Connection credential is not authorized for this integration path",
|
|
69
|
+
"not_started",
|
|
70
|
+
false
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
function placementValue(placement) {
|
|
75
|
+
return `${placement.prefix ?? ""}${placement.value}`;
|
|
76
|
+
}
|
|
77
|
+
function validateCredentialPlacements(placements) {
|
|
78
|
+
if (placements.length === 0 || placements.length > MAX_CREDENTIAL_PLACEMENTS) {
|
|
79
|
+
throw new IntegrationInvocationError(
|
|
80
|
+
"credential_placement_invalid",
|
|
81
|
+
"Connection credential placement count is invalid",
|
|
82
|
+
"not_started",
|
|
83
|
+
false
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
const seen = /* @__PURE__ */ new Set();
|
|
87
|
+
for (const placement of placements) {
|
|
88
|
+
const name = placement.name;
|
|
89
|
+
const value = placementValue(placement);
|
|
90
|
+
const normalizedName = placement.carrier === "header" ? name.toLowerCase() : name;
|
|
91
|
+
if (name.length === 0 || name.length > MAX_CREDENTIAL_NAME_LENGTH || placement.value.length === 0 || value.length > MAX_CREDENTIAL_VALUE_LENGTH || /[\r\n\0]/.test(name) || /[\r\n\0]/.test(value)) {
|
|
92
|
+
throw new IntegrationInvocationError(
|
|
93
|
+
"credential_placement_invalid",
|
|
94
|
+
"Connection credential placement is invalid",
|
|
95
|
+
"not_started",
|
|
96
|
+
false
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
if (placement.carrier === "header") {
|
|
100
|
+
if (!headerNamePattern.test(name) || forbiddenCredentialHeaders.has(normalizedName) || normalizedName.startsWith("sec-")) {
|
|
101
|
+
throw new IntegrationInvocationError(
|
|
102
|
+
"credential_header_forbidden",
|
|
103
|
+
"Connection credential targets a forbidden request header",
|
|
104
|
+
"not_started",
|
|
105
|
+
false
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
} else if (placement.carrier === "query") {
|
|
109
|
+
if (!queryNamePattern.test(name)) {
|
|
110
|
+
throw new IntegrationInvocationError(
|
|
111
|
+
"credential_placement_invalid",
|
|
112
|
+
"Connection credential query placement is invalid",
|
|
113
|
+
"not_started",
|
|
114
|
+
false
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
} else if (!cookieNamePattern.test(name) || /;/.test(value)) {
|
|
118
|
+
throw new IntegrationInvocationError(
|
|
119
|
+
"credential_cookie_invalid",
|
|
120
|
+
"Connection credential cookie placement is invalid",
|
|
121
|
+
"not_started",
|
|
122
|
+
false
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
const key = `${placement.carrier}\0${normalizedName}`;
|
|
126
|
+
if (seen.has(key)) {
|
|
127
|
+
throw new IntegrationInvocationError(
|
|
128
|
+
"credential_placement_invalid",
|
|
129
|
+
"Connection credential placements contain a duplicate destination",
|
|
130
|
+
"not_started",
|
|
131
|
+
false
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
seen.add(key);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
function applyCredentialPlacements(destination, headers, credential) {
|
|
138
|
+
assertCredentialAudience(credential, destination);
|
|
139
|
+
validateCredentialPlacements(credential.placements);
|
|
140
|
+
const cookies = [];
|
|
141
|
+
for (const placement of credential.placements) {
|
|
142
|
+
const name = placement.name;
|
|
143
|
+
const value = placementValue(placement);
|
|
144
|
+
if (placement.carrier === "header") {
|
|
145
|
+
headers.set(name, value);
|
|
146
|
+
} else if (placement.carrier === "query") {
|
|
147
|
+
destination.searchParams.set(name, value);
|
|
148
|
+
} else {
|
|
149
|
+
cookies.push(`${name}=${value}`);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
if (cookies.length > 0) {
|
|
153
|
+
const current = headers.get("cookie");
|
|
154
|
+
headers.set("cookie", [...current ? [current] : [], ...cookies].join("; "));
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// src/graphql.ts
|
|
159
|
+
import {
|
|
160
|
+
buildClientSchema,
|
|
161
|
+
getIntrospectionQuery,
|
|
162
|
+
getNamedType,
|
|
163
|
+
isEnumType,
|
|
164
|
+
isInputObjectType,
|
|
165
|
+
isInterfaceType,
|
|
166
|
+
isListType,
|
|
167
|
+
isNonNullType,
|
|
168
|
+
isObjectType,
|
|
169
|
+
isScalarType,
|
|
170
|
+
isUnionType,
|
|
171
|
+
parse
|
|
172
|
+
} from "graphql";
|
|
173
|
+
|
|
174
|
+
// src/http.ts
|
|
175
|
+
import { pinnedFetch, readResponseBodyBounded } from "@opengeni/network";
|
|
176
|
+
var DEFAULT_INTEGRATION_TIMEOUT_MS = 3e4;
|
|
177
|
+
var DEFAULT_INTEGRATION_RESPONSE_BYTES = 4 * 1024 * 1024;
|
|
178
|
+
var MAX_INTEGRATION_SPEC_BYTES = 8 * 1024 * 1024;
|
|
179
|
+
var MAX_INTEGRATION_TOOLS = 2e3;
|
|
180
|
+
async function fetchIntegrationSourceDocument(transport, sourceUrl, maxBytes = MAX_INTEGRATION_SPEC_BYTES) {
|
|
181
|
+
const url = new URL(sourceUrl);
|
|
182
|
+
const response = await fetchWithDeadline(
|
|
183
|
+
transport,
|
|
184
|
+
url,
|
|
185
|
+
{
|
|
186
|
+
method: "GET",
|
|
187
|
+
headers: { accept: "application/json, application/yaml, text/yaml, */*;q=0.5" }
|
|
188
|
+
},
|
|
189
|
+
DEFAULT_INTEGRATION_TIMEOUT_MS
|
|
190
|
+
);
|
|
191
|
+
if (response.status >= 300 && response.status < 400) {
|
|
192
|
+
await response.body?.cancel().catch(() => void 0);
|
|
193
|
+
throw new IntegrationInvocationError(
|
|
194
|
+
"source_redirect_rejected",
|
|
195
|
+
"Integration source attempted to redirect",
|
|
196
|
+
"failed",
|
|
197
|
+
false,
|
|
198
|
+
response.status
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
if (!response.ok) {
|
|
202
|
+
await response.body?.cancel().catch(() => void 0);
|
|
203
|
+
throw new IntegrationInvocationError(
|
|
204
|
+
"source_fetch_rejected",
|
|
205
|
+
"Integration source could not be read",
|
|
206
|
+
"failed",
|
|
207
|
+
response.status >= 500,
|
|
208
|
+
response.status
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
return await readResponseBodyBounded(response, maxBytes, "Integration source");
|
|
212
|
+
}
|
|
213
|
+
function createPinnedIntegrationTransport(options) {
|
|
214
|
+
return {
|
|
215
|
+
fetch: (input, init) => pinnedFetch(input, init, options.network, {
|
|
216
|
+
...options.fetchImpl ? { fetchImpl: options.fetchImpl } : {},
|
|
217
|
+
label: "Integration request",
|
|
218
|
+
requireHttpsOutsideLocalTest: true
|
|
219
|
+
})
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
function directIntegrationTransport(fetchImpl) {
|
|
223
|
+
return { fetch: fetchImpl };
|
|
224
|
+
}
|
|
225
|
+
async function fetchWithDeadline(transport, url, init, timeoutMs = DEFAULT_INTEGRATION_TIMEOUT_MS) {
|
|
226
|
+
if (!Number.isSafeInteger(timeoutMs) || timeoutMs < 1 || timeoutMs > 12e4) {
|
|
227
|
+
throw new RangeError("integration timeout must be between 1 and 120000 milliseconds");
|
|
228
|
+
}
|
|
229
|
+
const controller = new AbortController();
|
|
230
|
+
const onAbort = () => controller.abort(init.signal?.reason);
|
|
231
|
+
if (init.signal?.aborted) onAbort();
|
|
232
|
+
else init.signal?.addEventListener("abort", onAbort, { once: true });
|
|
233
|
+
const timer = setTimeout(
|
|
234
|
+
() => controller.abort(new Error("integration request timed out")),
|
|
235
|
+
timeoutMs
|
|
236
|
+
);
|
|
237
|
+
try {
|
|
238
|
+
return await transport.fetch(url, {
|
|
239
|
+
...init,
|
|
240
|
+
signal: controller.signal,
|
|
241
|
+
redirect: "manual"
|
|
242
|
+
});
|
|
243
|
+
} catch {
|
|
244
|
+
const timedOut = controller.signal.aborted && !init.signal?.aborted;
|
|
245
|
+
throw new IntegrationInvocationError(
|
|
246
|
+
timedOut ? "request_timeout" : "request_failed",
|
|
247
|
+
timedOut ? "Integration request timed out" : "Integration request failed",
|
|
248
|
+
requestCouldHaveStarted(init.method) ? "unknown" : "not_started",
|
|
249
|
+
!requestCouldHaveStarted(init.method)
|
|
250
|
+
);
|
|
251
|
+
} finally {
|
|
252
|
+
clearTimeout(timer);
|
|
253
|
+
init.signal?.removeEventListener("abort", onAbort);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
function requestCouldHaveStarted(method) {
|
|
257
|
+
const normalized = (method ?? "GET").toUpperCase();
|
|
258
|
+
return normalized !== "GET" && normalized !== "HEAD" && normalized !== "OPTIONS";
|
|
259
|
+
}
|
|
260
|
+
async function readIntegrationResponse(response, maxBytes = DEFAULT_INTEGRATION_RESPONSE_BYTES) {
|
|
261
|
+
const body = await readResponseBodyBounded(response, maxBytes, "Integration response");
|
|
262
|
+
const contentType = response.headers.get("content-type")?.split(";", 1)[0]?.trim().toLowerCase() ?? "";
|
|
263
|
+
if (body.byteLength === 0) return { data: null, contentType, bytes: 0 };
|
|
264
|
+
const text = new TextDecoder("utf-8", { fatal: false }).decode(body);
|
|
265
|
+
if (contentType === "application/json" || contentType.endsWith("+json")) {
|
|
266
|
+
try {
|
|
267
|
+
return { data: JSON.parse(text), contentType, bytes: body.byteLength };
|
|
268
|
+
} catch {
|
|
269
|
+
throw new IntegrationInvocationError(
|
|
270
|
+
"response_json_invalid",
|
|
271
|
+
"Integration returned invalid JSON",
|
|
272
|
+
"failed",
|
|
273
|
+
false,
|
|
274
|
+
response.status
|
|
275
|
+
);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
return { data: text, contentType, bytes: body.byteLength };
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// src/revision.ts
|
|
282
|
+
import { createHash } from "crypto";
|
|
283
|
+
function canonicalize(value) {
|
|
284
|
+
if (Array.isArray(value)) return value.map(canonicalize);
|
|
285
|
+
if (!value || typeof value !== "object") return value;
|
|
286
|
+
return Object.fromEntries(
|
|
287
|
+
Object.entries(value).sort(([left], [right]) => left.localeCompare(right)).map(([key, entry]) => [key, canonicalize(entry)])
|
|
288
|
+
);
|
|
289
|
+
}
|
|
290
|
+
function canonicalJson(value) {
|
|
291
|
+
return JSON.stringify(canonicalize(value));
|
|
292
|
+
}
|
|
293
|
+
function sha256Hex(value) {
|
|
294
|
+
return createHash("sha256").update(value).digest("hex");
|
|
295
|
+
}
|
|
296
|
+
function immutableRevisionId(protocol, contentSha256) {
|
|
297
|
+
if (!/^[a-f0-9]{64}$/.test(contentSha256)) {
|
|
298
|
+
throw new Error("contentSha256 must be a lowercase SHA-256 digest");
|
|
299
|
+
}
|
|
300
|
+
return `${protocol}:${contentSha256.slice(0, 24)}`;
|
|
301
|
+
}
|
|
302
|
+
function stableToolId(value, seen) {
|
|
303
|
+
const normalized = value.trim().toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "").slice(0, 54);
|
|
304
|
+
const base = normalized || "tool";
|
|
305
|
+
if (!seen) return base;
|
|
306
|
+
const count = (seen.get(base) ?? 0) + 1;
|
|
307
|
+
seen.set(base, count);
|
|
308
|
+
return count === 1 ? base : `${base}_${count}`;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// src/graphql.ts
|
|
312
|
+
function compileGraphqlRevision(introspection, options) {
|
|
313
|
+
const document = parseIntrospection(introspection);
|
|
314
|
+
let schema;
|
|
315
|
+
try {
|
|
316
|
+
schema = buildClientSchema(document);
|
|
317
|
+
} catch {
|
|
318
|
+
throw new IntegrationProtocolError(
|
|
319
|
+
"graphql_introspection_invalid",
|
|
320
|
+
"GraphQL introspection result cannot build a client schema"
|
|
321
|
+
);
|
|
322
|
+
}
|
|
323
|
+
const endpoint = validateGraphqlEndpoint(options.endpoint);
|
|
324
|
+
const contentSha256 = sha256Hex(canonicalJson(document));
|
|
325
|
+
const id = immutableRevisionId("graphql", contentSha256);
|
|
326
|
+
const tools = [];
|
|
327
|
+
const bindings = {};
|
|
328
|
+
const seen = /* @__PURE__ */ new Map();
|
|
329
|
+
for (const [kind, root] of [
|
|
330
|
+
["query", schema.getQueryType()],
|
|
331
|
+
["mutation", schema.getMutationType()]
|
|
332
|
+
]) {
|
|
333
|
+
if (!root) continue;
|
|
334
|
+
for (const field of Object.values(root.getFields()).sort(
|
|
335
|
+
(left, right) => left.name.localeCompare(right.name)
|
|
336
|
+
)) {
|
|
337
|
+
const toolId = stableToolId(`${kind}_${field.name}`, seen);
|
|
338
|
+
const namedOutput = getNamedType(field.type);
|
|
339
|
+
const selectionAllowed = !isLeafType(namedOutput);
|
|
340
|
+
const defaultSelection = selectionAllowed ? buildDefaultSelection(field.type, /* @__PURE__ */ new Set(), 0) : void 0;
|
|
341
|
+
const properties = Object.fromEntries(
|
|
342
|
+
field.args.map((arg) => [
|
|
343
|
+
arg.name,
|
|
344
|
+
{
|
|
345
|
+
...inputTypeSchema(arg.type, /* @__PURE__ */ new Set(), 0),
|
|
346
|
+
...arg.description ? { description: arg.description } : {}
|
|
347
|
+
}
|
|
348
|
+
])
|
|
349
|
+
);
|
|
350
|
+
if (selectionAllowed) {
|
|
351
|
+
properties.select = {
|
|
352
|
+
type: "string",
|
|
353
|
+
description: "Optional GraphQL field selection without outer braces. The default selects safe scalar fields.",
|
|
354
|
+
maxLength: 4e3
|
|
355
|
+
};
|
|
356
|
+
}
|
|
357
|
+
const required = field.args.filter((arg) => isNonNullType(arg.type)).map((arg) => arg.name);
|
|
358
|
+
const description = [
|
|
359
|
+
field.description?.trim(),
|
|
360
|
+
kind === "mutation" ? "Changes external state and requires approval." : "Read-only GraphQL query."
|
|
361
|
+
].filter(Boolean).join(" ");
|
|
362
|
+
tools.push({
|
|
363
|
+
id: toolId,
|
|
364
|
+
operationKey: `${kind}:${field.name}`,
|
|
365
|
+
name: field.name,
|
|
366
|
+
description,
|
|
367
|
+
inputSchema: {
|
|
368
|
+
type: "object",
|
|
369
|
+
properties,
|
|
370
|
+
required,
|
|
371
|
+
additionalProperties: false
|
|
372
|
+
},
|
|
373
|
+
safety: kind === "query" ? "read" : "write",
|
|
374
|
+
approvalMode: kind === "query" ? "never" : "ask",
|
|
375
|
+
deprecated: field.deprecationReason != null
|
|
376
|
+
});
|
|
377
|
+
bindings[toolId] = {
|
|
378
|
+
kind,
|
|
379
|
+
fieldName: field.name,
|
|
380
|
+
operationName: stableGraphqlName(`${kind}_${field.name}`),
|
|
381
|
+
variableDefinitions: field.args.map((arg) => `$${arg.name}: ${String(arg.type)}`),
|
|
382
|
+
variableNames: field.args.map((arg) => arg.name),
|
|
383
|
+
...defaultSelection ? { defaultSelection } : {},
|
|
384
|
+
selectionAllowed
|
|
385
|
+
};
|
|
386
|
+
if (tools.length > MAX_INTEGRATION_TOOLS) {
|
|
387
|
+
throw new IntegrationProtocolError(
|
|
388
|
+
"graphql_tool_limit",
|
|
389
|
+
`GraphQL schema exceeds the ${MAX_INTEGRATION_TOOLS}-tool limit`
|
|
390
|
+
);
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
if (tools.length === 0) {
|
|
395
|
+
throw new IntegrationProtocolError("graphql_empty", "GraphQL schema exposes no root fields");
|
|
396
|
+
}
|
|
397
|
+
return {
|
|
398
|
+
id,
|
|
399
|
+
protocol: "graphql",
|
|
400
|
+
integrationId: options.integrationId,
|
|
401
|
+
contentSha256,
|
|
402
|
+
source: {
|
|
403
|
+
url: options.sourceUrl ?? endpoint,
|
|
404
|
+
...options.provider ? { provider: options.provider } : {}
|
|
405
|
+
},
|
|
406
|
+
title: options.name?.trim() || options.integrationId,
|
|
407
|
+
tools,
|
|
408
|
+
bindings
|
|
409
|
+
};
|
|
410
|
+
}
|
|
411
|
+
async function fetchGraphqlIntrospection(options) {
|
|
412
|
+
const request = { query: getIntrospectionQuery({ descriptions: true }) };
|
|
413
|
+
const firstCredential = await resolveGraphqlCredential(
|
|
414
|
+
options,
|
|
415
|
+
"graphql-introspection",
|
|
416
|
+
"pending",
|
|
417
|
+
"__introspection",
|
|
418
|
+
false
|
|
419
|
+
);
|
|
420
|
+
let response = await sendGraphqlRequest(options, request, firstCredential);
|
|
421
|
+
if (response.status === 401 && options.credentialResolver && options.authority.connectionRef) {
|
|
422
|
+
const refreshed = await resolveGraphqlCredential(
|
|
423
|
+
options,
|
|
424
|
+
"graphql-introspection",
|
|
425
|
+
"pending",
|
|
426
|
+
"__introspection",
|
|
427
|
+
true
|
|
428
|
+
);
|
|
429
|
+
await response.body?.cancel().catch(() => void 0);
|
|
430
|
+
if (!refreshed) {
|
|
431
|
+
throw new IntegrationInvocationError(
|
|
432
|
+
"graphql_introspection_rejected",
|
|
433
|
+
"GraphQL endpoint did not return an introspection schema",
|
|
434
|
+
"failed",
|
|
435
|
+
false,
|
|
436
|
+
401
|
|
437
|
+
);
|
|
438
|
+
}
|
|
439
|
+
response = await sendGraphqlRequest(options, request, refreshed);
|
|
440
|
+
}
|
|
441
|
+
if (response.status >= 300 && response.status < 400) {
|
|
442
|
+
await response.body?.cancel().catch(() => void 0);
|
|
443
|
+
throw new IntegrationInvocationError(
|
|
444
|
+
"redirect_rejected",
|
|
445
|
+
"GraphQL endpoint attempted to redirect the introspection request",
|
|
446
|
+
"unknown",
|
|
447
|
+
false,
|
|
448
|
+
response.status
|
|
449
|
+
);
|
|
450
|
+
}
|
|
451
|
+
const body = await readIntegrationResponse(response, MAX_INTEGRATION_SPEC_BYTES);
|
|
452
|
+
if (!response.ok || !isRecord(body.data) || !isRecord(body.data.data)) {
|
|
453
|
+
throw new IntegrationInvocationError(
|
|
454
|
+
"graphql_introspection_rejected",
|
|
455
|
+
"GraphQL endpoint did not return an introspection schema",
|
|
456
|
+
"failed",
|
|
457
|
+
false,
|
|
458
|
+
response.status
|
|
459
|
+
);
|
|
460
|
+
}
|
|
461
|
+
return body.data.data;
|
|
462
|
+
}
|
|
463
|
+
var GraphqlMcpServer = class {
|
|
464
|
+
constructor(options) {
|
|
465
|
+
this.options = options;
|
|
466
|
+
this.name = `graphql:${stableToolId(options.revision.integrationId)}`;
|
|
467
|
+
}
|
|
468
|
+
cacheToolsList = true;
|
|
469
|
+
useStructuredContent = true;
|
|
470
|
+
name;
|
|
471
|
+
async connect() {
|
|
472
|
+
}
|
|
473
|
+
async close() {
|
|
474
|
+
}
|
|
475
|
+
async invalidateToolsCache() {
|
|
476
|
+
}
|
|
477
|
+
async listTools() {
|
|
478
|
+
return this.options.revision.tools.map(
|
|
479
|
+
(tool) => ({
|
|
480
|
+
name: tool.id,
|
|
481
|
+
description: tool.description,
|
|
482
|
+
inputSchema: normalizeMcpSchema(tool.inputSchema),
|
|
483
|
+
annotations: {
|
|
484
|
+
readOnlyHint: tool.safety === "read",
|
|
485
|
+
destructiveHint: false,
|
|
486
|
+
idempotentHint: tool.safety === "read",
|
|
487
|
+
openWorldHint: true
|
|
488
|
+
},
|
|
489
|
+
_meta: {
|
|
490
|
+
"opengeni/approvalMode": tool.approvalMode,
|
|
491
|
+
"opengeni/operationKey": tool.operationKey,
|
|
492
|
+
"opengeni/revisionId": this.options.revision.id
|
|
493
|
+
}
|
|
494
|
+
})
|
|
495
|
+
);
|
|
496
|
+
}
|
|
497
|
+
async callTool(toolName, args, _meta, callOptions) {
|
|
498
|
+
const result = await invokeGraphqlOperation(
|
|
499
|
+
this.options,
|
|
500
|
+
toolName,
|
|
501
|
+
args ?? {},
|
|
502
|
+
callOptions?.signal
|
|
503
|
+
);
|
|
504
|
+
const content = [
|
|
505
|
+
{ type: "text", text: JSON.stringify(result) }
|
|
506
|
+
];
|
|
507
|
+
content.structuredContent = result;
|
|
508
|
+
content.isError = result.ok === false;
|
|
509
|
+
return content;
|
|
510
|
+
}
|
|
511
|
+
};
|
|
512
|
+
function createGraphqlMcpServer(options) {
|
|
513
|
+
return new GraphqlMcpServer(options);
|
|
514
|
+
}
|
|
515
|
+
async function invokeGraphqlOperation(options, toolId, args, signal) {
|
|
516
|
+
const binding = options.revision.bindings[toolId];
|
|
517
|
+
if (!binding) {
|
|
518
|
+
throw new IntegrationInvocationError(
|
|
519
|
+
"operation_not_found",
|
|
520
|
+
"GraphQL operation is not present in the frozen revision",
|
|
521
|
+
"not_started",
|
|
522
|
+
false
|
|
523
|
+
);
|
|
524
|
+
}
|
|
525
|
+
const select = binding.selectionAllowed ? validateGraphqlSelection(
|
|
526
|
+
typeof args.select === "string" ? args.select : binding.defaultSelection ?? "__typename"
|
|
527
|
+
) : void 0;
|
|
528
|
+
const variables = Object.fromEntries(
|
|
529
|
+
binding.variableNames.flatMap((name) => args[name] === void 0 ? [] : [[name, args[name]]])
|
|
530
|
+
);
|
|
531
|
+
const definitions = binding.variableDefinitions.length ? `(${binding.variableDefinitions.join(", ")})` : "";
|
|
532
|
+
const argumentsText = binding.variableNames.length ? `(${binding.variableNames.map((name) => `${name}: $${name}`).join(", ")})` : "";
|
|
533
|
+
const query = `${binding.kind} ${binding.operationName}${definitions} { ${binding.fieldName}${argumentsText}${select ? ` { ${select} }` : ""} }`;
|
|
534
|
+
const request = { query, variables, operationName: binding.operationName };
|
|
535
|
+
const firstCredential = await resolveGraphqlCredential(
|
|
536
|
+
options,
|
|
537
|
+
options.revision.integrationId,
|
|
538
|
+
options.revision.id,
|
|
539
|
+
toolId,
|
|
540
|
+
false
|
|
541
|
+
);
|
|
542
|
+
let response = await sendGraphqlRequest(options, request, firstCredential, signal);
|
|
543
|
+
if (response.status === 401 && options.credentialResolver && options.authority.connectionRef) {
|
|
544
|
+
const refreshed = await resolveGraphqlCredential(
|
|
545
|
+
options,
|
|
546
|
+
options.revision.integrationId,
|
|
547
|
+
options.revision.id,
|
|
548
|
+
toolId,
|
|
549
|
+
true
|
|
550
|
+
);
|
|
551
|
+
await response.body?.cancel().catch(() => void 0);
|
|
552
|
+
if (binding.kind === "query" && refreshed) {
|
|
553
|
+
response = await sendGraphqlRequest(options, request, refreshed, signal);
|
|
554
|
+
} else {
|
|
555
|
+
throw new IntegrationInvocationError(
|
|
556
|
+
"authorization_rejected",
|
|
557
|
+
"The connected account is no longer authorized for this GraphQL operation",
|
|
558
|
+
binding.kind === "mutation" ? "unknown" : "failed",
|
|
559
|
+
false,
|
|
560
|
+
401
|
|
561
|
+
);
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
if (response.status >= 300 && response.status < 400) {
|
|
565
|
+
await response.body?.cancel().catch(() => void 0);
|
|
566
|
+
throw new IntegrationInvocationError(
|
|
567
|
+
"redirect_rejected",
|
|
568
|
+
"GraphQL endpoint attempted to redirect a credential-bearing request",
|
|
569
|
+
binding.kind === "mutation" ? "unknown" : "failed",
|
|
570
|
+
false,
|
|
571
|
+
response.status
|
|
572
|
+
);
|
|
573
|
+
}
|
|
574
|
+
const payload = await readIntegrationResponse(
|
|
575
|
+
response,
|
|
576
|
+
options.maxResponseBytes ?? DEFAULT_INTEGRATION_RESPONSE_BYTES
|
|
577
|
+
);
|
|
578
|
+
if (response.status === 401 || response.status === 403) {
|
|
579
|
+
throw new IntegrationInvocationError(
|
|
580
|
+
"authorization_rejected",
|
|
581
|
+
"The connected account is no longer authorized for this GraphQL operation",
|
|
582
|
+
binding.kind === "mutation" ? "unknown" : "failed",
|
|
583
|
+
false,
|
|
584
|
+
response.status
|
|
585
|
+
);
|
|
586
|
+
}
|
|
587
|
+
const graph = isRecord(payload.data) ? payload.data : {};
|
|
588
|
+
return {
|
|
589
|
+
ok: response.ok && !Array.isArray(graph.errors),
|
|
590
|
+
status: response.status,
|
|
591
|
+
data: graph.data ?? null,
|
|
592
|
+
errors: graph.errors ?? null
|
|
593
|
+
};
|
|
594
|
+
}
|
|
595
|
+
async function resolveGraphqlCredential(options, integrationId, revisionId, operationKey, forceRefresh) {
|
|
596
|
+
if (!options.credentialResolver || !options.authority.connectionRef) return null;
|
|
597
|
+
const credential = await options.credentialResolver.resolve({
|
|
598
|
+
...options.authority,
|
|
599
|
+
protocol: "graphql",
|
|
600
|
+
integrationId,
|
|
601
|
+
revisionId,
|
|
602
|
+
operationKey,
|
|
603
|
+
destinationUrl: graphqlEndpoint(options).toString(),
|
|
604
|
+
...forceRefresh ? { forceRefresh: true } : {}
|
|
605
|
+
});
|
|
606
|
+
if (!credential && !forceRefresh) {
|
|
607
|
+
throw new IntegrationInvocationError(
|
|
608
|
+
"connection_required",
|
|
609
|
+
"This GraphQL integration needs a connected account",
|
|
610
|
+
"not_started",
|
|
611
|
+
false
|
|
612
|
+
);
|
|
613
|
+
}
|
|
614
|
+
return credential;
|
|
615
|
+
}
|
|
616
|
+
async function sendGraphqlRequest(options, request, credential, signal) {
|
|
617
|
+
const endpoint = graphqlEndpoint(options);
|
|
618
|
+
const headers = new Headers(options.staticHeaders);
|
|
619
|
+
headers.set("accept", "application/json");
|
|
620
|
+
headers.set("content-type", "application/json");
|
|
621
|
+
if (credential) applyCredentialPlacements(endpoint, headers, credential);
|
|
622
|
+
return await fetchWithDeadline(
|
|
623
|
+
options.transport,
|
|
624
|
+
endpoint,
|
|
625
|
+
{
|
|
626
|
+
method: "POST",
|
|
627
|
+
headers,
|
|
628
|
+
body: JSON.stringify(request),
|
|
629
|
+
...signal ? { signal } : {}
|
|
630
|
+
},
|
|
631
|
+
options.timeoutMs ?? DEFAULT_INTEGRATION_TIMEOUT_MS
|
|
632
|
+
);
|
|
633
|
+
}
|
|
634
|
+
function graphqlEndpoint(options) {
|
|
635
|
+
const endpoint = new URL(validateGraphqlEndpoint(options.endpoint));
|
|
636
|
+
for (const [name, value] of Object.entries(options.staticQuery ?? {})) {
|
|
637
|
+
endpoint.searchParams.set(name, value);
|
|
638
|
+
}
|
|
639
|
+
return endpoint;
|
|
640
|
+
}
|
|
641
|
+
function validateGraphqlSelection(value) {
|
|
642
|
+
const normalized = value.trim();
|
|
643
|
+
if (!normalized || normalized.length > 4e3) {
|
|
644
|
+
throw new IntegrationInvocationError(
|
|
645
|
+
"graphql_selection_invalid",
|
|
646
|
+
"GraphQL selection must contain between 1 and 4000 characters",
|
|
647
|
+
"not_started",
|
|
648
|
+
false
|
|
649
|
+
);
|
|
650
|
+
}
|
|
651
|
+
try {
|
|
652
|
+
const document = parse(`fragment OpenGeniSelection on Placeholder { ${normalized} }`);
|
|
653
|
+
if (document.definitions.length !== 1 || document.definitions[0]?.kind !== "FragmentDefinition") {
|
|
654
|
+
throw new Error("invalid selection document");
|
|
655
|
+
}
|
|
656
|
+
return normalized;
|
|
657
|
+
} catch {
|
|
658
|
+
throw new IntegrationInvocationError(
|
|
659
|
+
"graphql_selection_invalid",
|
|
660
|
+
"GraphQL selection is invalid",
|
|
661
|
+
"not_started",
|
|
662
|
+
false
|
|
663
|
+
);
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
function parseIntrospection(value) {
|
|
667
|
+
let parsed = value;
|
|
668
|
+
if (typeof value === "string") {
|
|
669
|
+
if (Buffer.byteLength(value) > MAX_INTEGRATION_SPEC_BYTES) {
|
|
670
|
+
throw new IntegrationProtocolError(
|
|
671
|
+
"graphql_introspection_size",
|
|
672
|
+
`GraphQL introspection exceeds ${MAX_INTEGRATION_SPEC_BYTES} bytes`
|
|
673
|
+
);
|
|
674
|
+
}
|
|
675
|
+
try {
|
|
676
|
+
parsed = JSON.parse(value);
|
|
677
|
+
} catch {
|
|
678
|
+
throw new IntegrationProtocolError(
|
|
679
|
+
"graphql_introspection_parse",
|
|
680
|
+
"GraphQL introspection is not valid JSON"
|
|
681
|
+
);
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
if (isRecord(parsed) && isRecord(parsed.data) && isRecord(parsed.data.__schema)) {
|
|
685
|
+
return parsed.data;
|
|
686
|
+
}
|
|
687
|
+
if (isRecord(parsed) && isRecord(parsed.__schema)) {
|
|
688
|
+
return parsed;
|
|
689
|
+
}
|
|
690
|
+
throw new IntegrationProtocolError(
|
|
691
|
+
"graphql_introspection_shape",
|
|
692
|
+
"GraphQL introspection result has no __schema object"
|
|
693
|
+
);
|
|
694
|
+
}
|
|
695
|
+
function validateGraphqlEndpoint(value) {
|
|
696
|
+
let endpoint;
|
|
697
|
+
try {
|
|
698
|
+
endpoint = new URL(value);
|
|
699
|
+
} catch {
|
|
700
|
+
throw new IntegrationProtocolError(
|
|
701
|
+
"graphql_endpoint_invalid",
|
|
702
|
+
"GraphQL endpoint URL is invalid"
|
|
703
|
+
);
|
|
704
|
+
}
|
|
705
|
+
if (!/^https?:$/.test(endpoint.protocol) || endpoint.username || endpoint.password || endpoint.hash) {
|
|
706
|
+
throw new IntegrationProtocolError(
|
|
707
|
+
"graphql_endpoint_invalid",
|
|
708
|
+
"GraphQL endpoint URL is invalid"
|
|
709
|
+
);
|
|
710
|
+
}
|
|
711
|
+
return endpoint.toString();
|
|
712
|
+
}
|
|
713
|
+
function inputTypeSchema(input, seen, depth) {
|
|
714
|
+
if (depth > 12) return {};
|
|
715
|
+
if (isNonNullType(input)) return inputTypeSchema(input.ofType, seen, depth + 1);
|
|
716
|
+
if (isListType(input)) {
|
|
717
|
+
return { type: "array", items: inputTypeSchema(input.ofType, seen, depth + 1) };
|
|
718
|
+
}
|
|
719
|
+
const type = getNamedType(input);
|
|
720
|
+
if (isScalarType(type)) return scalarSchema(type.name);
|
|
721
|
+
if (isEnumType(type))
|
|
722
|
+
return { type: "string", enum: type.getValues().map((entry) => entry.name) };
|
|
723
|
+
if (isInputObjectType(type)) {
|
|
724
|
+
if (seen.has(type.name)) return { type: "object", additionalProperties: true };
|
|
725
|
+
const nextSeen = new Set(seen).add(type.name);
|
|
726
|
+
const fields = Object.values(type.getFields());
|
|
727
|
+
return {
|
|
728
|
+
type: "object",
|
|
729
|
+
properties: Object.fromEntries(
|
|
730
|
+
fields.map((field) => [
|
|
731
|
+
field.name,
|
|
732
|
+
{
|
|
733
|
+
...inputTypeSchema(field.type, nextSeen, depth + 1),
|
|
734
|
+
...field.description ? { description: field.description } : {}
|
|
735
|
+
}
|
|
736
|
+
])
|
|
737
|
+
),
|
|
738
|
+
required: fields.filter((field) => isNonNullType(field.type)).map((field) => field.name),
|
|
739
|
+
additionalProperties: false
|
|
740
|
+
};
|
|
741
|
+
}
|
|
742
|
+
return {};
|
|
743
|
+
}
|
|
744
|
+
function scalarSchema(name) {
|
|
745
|
+
if (name === "Boolean") return { type: "boolean" };
|
|
746
|
+
if (name === "Int") return { type: "integer" };
|
|
747
|
+
if (name === "Float") return { type: "number" };
|
|
748
|
+
if (name === "ID" || name === "String") return { type: "string" };
|
|
749
|
+
return { description: `GraphQL scalar ${name}` };
|
|
750
|
+
}
|
|
751
|
+
function buildDefaultSelection(output, seen, depth) {
|
|
752
|
+
const type = getNamedType(output);
|
|
753
|
+
if (isLeafType(type)) return void 0;
|
|
754
|
+
if (depth > 2 || seen.has(type.name)) return "__typename";
|
|
755
|
+
if (isUnionType(type) || isInterfaceType(type)) return "__typename";
|
|
756
|
+
if (!isObjectType(type)) return "__typename";
|
|
757
|
+
const nextSeen = new Set(seen).add(type.name);
|
|
758
|
+
const fields = Object.values(type.getFields());
|
|
759
|
+
const scalarFields = fields.filter((field) => isLeafType(getNamedType(field.type))).slice(0, 20);
|
|
760
|
+
const selections = scalarFields.map((field) => field.name);
|
|
761
|
+
if (selections.length < 3 && depth < 2) {
|
|
762
|
+
const nested = fields.find(
|
|
763
|
+
(field) => field.args.length === 0 && !isLeafType(getNamedType(field.type))
|
|
764
|
+
);
|
|
765
|
+
if (nested) {
|
|
766
|
+
const child = buildDefaultSelection(nested.type, nextSeen, depth + 1);
|
|
767
|
+
if (child) selections.push(`${nested.name} { ${child} }`);
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
return selections.length ? selections.join(" ") : "__typename";
|
|
771
|
+
}
|
|
772
|
+
function isLeafType(type) {
|
|
773
|
+
return isScalarType(type) || isEnumType(type);
|
|
774
|
+
}
|
|
775
|
+
function stableGraphqlName(value) {
|
|
776
|
+
const normalized = value.replace(/[^_0-9A-Za-z]/g, "_").replace(/^([^_A-Za-z])/, "_$1");
|
|
777
|
+
return normalized || "OpenGeniOperation";
|
|
778
|
+
}
|
|
779
|
+
function normalizeMcpSchema(schema) {
|
|
780
|
+
return {
|
|
781
|
+
type: "object",
|
|
782
|
+
properties: isRecord(schema.properties) ? schema.properties : {},
|
|
783
|
+
required: Array.isArray(schema.required) ? schema.required.filter((entry) => typeof entry === "string") : [],
|
|
784
|
+
additionalProperties: schema.additionalProperties === true
|
|
785
|
+
};
|
|
786
|
+
}
|
|
787
|
+
function isRecord(value) {
|
|
788
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
// src/mcp-manifest.ts
|
|
792
|
+
function extractMcpToolManifest(listToolsResult, metadata = {}) {
|
|
793
|
+
const listed = listToolsResult && typeof listToolsResult === "object" && Array.isArray(listToolsResult.tools) ? listToolsResult.tools : [];
|
|
794
|
+
const seen = /* @__PURE__ */ new Map();
|
|
795
|
+
const tools = listed.flatMap((value) => {
|
|
796
|
+
if (!value || typeof value !== "object") return [];
|
|
797
|
+
const tool = value;
|
|
798
|
+
if (typeof tool.name !== "string" || !tool.name.trim()) return [];
|
|
799
|
+
const toolName = tool.name.trim();
|
|
800
|
+
return [
|
|
801
|
+
{
|
|
802
|
+
toolId: stableToolId(toolName, seen),
|
|
803
|
+
toolName,
|
|
804
|
+
description: typeof tool.description === "string" ? tool.description : null,
|
|
805
|
+
...tool.inputSchema !== void 0 ? { inputSchema: tool.inputSchema } : tool.parameters !== void 0 ? { inputSchema: tool.parameters } : {},
|
|
806
|
+
...tool.outputSchema !== void 0 ? { outputSchema: tool.outputSchema } : {},
|
|
807
|
+
...tool.annotations && typeof tool.annotations === "object" ? { annotations: tool.annotations } : {}
|
|
808
|
+
}
|
|
809
|
+
];
|
|
810
|
+
});
|
|
811
|
+
const info = metadata.serverInfo && typeof metadata.serverInfo === "object" ? metadata.serverInfo : null;
|
|
812
|
+
return {
|
|
813
|
+
server: info ? {
|
|
814
|
+
name: typeof info.name === "string" ? info.name : null,
|
|
815
|
+
version: typeof info.version === "string" ? info.version : null,
|
|
816
|
+
instructions: metadata.instructions ?? null
|
|
817
|
+
} : null,
|
|
818
|
+
tools
|
|
819
|
+
};
|
|
820
|
+
}
|
|
821
|
+
function deriveMcpNamespace(input) {
|
|
822
|
+
const candidate = input.name?.trim() || hostname(input.endpoint) || basename(input.command) || "mcp";
|
|
823
|
+
return stableToolId(candidate);
|
|
824
|
+
}
|
|
825
|
+
function hostname(value) {
|
|
826
|
+
if (!value || !URL.canParse(value)) return "";
|
|
827
|
+
return new URL(value).hostname;
|
|
828
|
+
}
|
|
829
|
+
function basename(value) {
|
|
830
|
+
return value?.trim().split(/[\\/]/).pop() ?? "";
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
// src/openapi.ts
|
|
834
|
+
import { load as parseYaml } from "js-yaml";
|
|
835
|
+
var methods = /* @__PURE__ */ new Set([
|
|
836
|
+
"get",
|
|
837
|
+
"put",
|
|
838
|
+
"post",
|
|
839
|
+
"delete",
|
|
840
|
+
"patch",
|
|
841
|
+
"head",
|
|
842
|
+
"options",
|
|
843
|
+
"trace"
|
|
844
|
+
]);
|
|
845
|
+
var forbiddenParameterHeaders = /* @__PURE__ */ new Set([
|
|
846
|
+
"host",
|
|
847
|
+
"content-length",
|
|
848
|
+
"connection",
|
|
849
|
+
"transfer-encoding"
|
|
850
|
+
]);
|
|
851
|
+
function parseOpenApiDocument(source) {
|
|
852
|
+
const bytes = typeof source === "string" ? Buffer.byteLength(source) : source.byteLength;
|
|
853
|
+
if (bytes === 0 || bytes > MAX_INTEGRATION_SPEC_BYTES) {
|
|
854
|
+
throw new IntegrationProtocolError(
|
|
855
|
+
"openapi_spec_size",
|
|
856
|
+
`OpenAPI document must be between 1 and ${MAX_INTEGRATION_SPEC_BYTES} bytes`
|
|
857
|
+
);
|
|
858
|
+
}
|
|
859
|
+
const text = typeof source === "string" ? source : new TextDecoder("utf-8", { fatal: true }).decode(source);
|
|
860
|
+
let parsed;
|
|
861
|
+
try {
|
|
862
|
+
parsed = parseYaml(text, { json: true });
|
|
863
|
+
} catch {
|
|
864
|
+
throw new IntegrationProtocolError(
|
|
865
|
+
"openapi_parse",
|
|
866
|
+
"OpenAPI document is not valid JSON or YAML"
|
|
867
|
+
);
|
|
868
|
+
}
|
|
869
|
+
if (!isRecord2(parsed) || typeof parsed.openapi !== "string" || !/^3\.(?:0|1)(?:\.|$)/.test(parsed.openapi)) {
|
|
870
|
+
throw new IntegrationProtocolError(
|
|
871
|
+
"openapi_version",
|
|
872
|
+
"Only OpenAPI 3.0 and 3.1 documents are supported"
|
|
873
|
+
);
|
|
874
|
+
}
|
|
875
|
+
if (!isRecord2(parsed.paths)) {
|
|
876
|
+
throw new IntegrationProtocolError("openapi_paths", "OpenAPI document has no paths object");
|
|
877
|
+
}
|
|
878
|
+
return parsed;
|
|
879
|
+
}
|
|
880
|
+
function compileOpenApiRevision(source, options) {
|
|
881
|
+
const document = isRecord2(source) ? source : parseOpenApiDocument(source);
|
|
882
|
+
const contentSha256 = sha256Hex(canonicalJson(document));
|
|
883
|
+
const revisionId = immutableRevisionId("openapi", contentSha256);
|
|
884
|
+
const info = isRecord2(document.info) ? document.info : {};
|
|
885
|
+
const documentServers = readServers(document.servers, options.baseUrl, options.sourceUrl);
|
|
886
|
+
const documentSecurity = readSecurity(document.security);
|
|
887
|
+
const tools = [];
|
|
888
|
+
const bindings = {};
|
|
889
|
+
const seen = /* @__PURE__ */ new Map();
|
|
890
|
+
for (const [pathTemplate, rawPathItem] of Object.entries(
|
|
891
|
+
document.paths
|
|
892
|
+
)) {
|
|
893
|
+
const pathItem = resolveObject(document, rawPathItem, "path item");
|
|
894
|
+
const sharedParameters = readParameters(document, pathItem.parameters);
|
|
895
|
+
const pathServers = readServers(pathItem.servers, void 0, void 0);
|
|
896
|
+
for (const [rawMethod, rawOperation] of Object.entries(pathItem)) {
|
|
897
|
+
const method = rawMethod.toLowerCase();
|
|
898
|
+
if (!methods.has(method) || !isRecord2(rawOperation)) continue;
|
|
899
|
+
const operation = resolveObject(document, rawOperation, "operation");
|
|
900
|
+
const operationKey = operationIdentity(method, pathTemplate, operation.operationId);
|
|
901
|
+
const id = stableToolId(operationKey, seen);
|
|
902
|
+
const parameters = mergeParameters(
|
|
903
|
+
sharedParameters,
|
|
904
|
+
readParameters(document, operation.parameters)
|
|
905
|
+
);
|
|
906
|
+
const requestBody = readRequestBody(document, operation.requestBody);
|
|
907
|
+
const serverUrl = firstServerUrl(
|
|
908
|
+
readServers(operation.servers, void 0, void 0),
|
|
909
|
+
pathServers,
|
|
910
|
+
documentServers
|
|
911
|
+
);
|
|
912
|
+
const requiredScopeAlternatives = operation.security === void 0 ? documentSecurity : readSecurity(operation.security);
|
|
913
|
+
const safety = classifyHttpSafety(method, operation);
|
|
914
|
+
const inputSchema = operationInputSchema(parameters, requestBody);
|
|
915
|
+
const outputSchema = operationOutputSchema(document, operation.responses);
|
|
916
|
+
const summary = stringValue(operation.summary) ?? stringValue(operation.description);
|
|
917
|
+
tools.push({
|
|
918
|
+
id,
|
|
919
|
+
operationKey,
|
|
920
|
+
name: summary ?? `${method.toUpperCase()} ${pathTemplate}`,
|
|
921
|
+
description: toolDescription(method, pathTemplate, operation, safety),
|
|
922
|
+
inputSchema,
|
|
923
|
+
...outputSchema ? { outputSchema } : {},
|
|
924
|
+
safety,
|
|
925
|
+
approvalMode: safety === "read" ? "never" : "ask",
|
|
926
|
+
deprecated: operation.deprecated === true
|
|
927
|
+
});
|
|
928
|
+
bindings[id] = {
|
|
929
|
+
method,
|
|
930
|
+
pathTemplate,
|
|
931
|
+
serverUrl,
|
|
932
|
+
parameters,
|
|
933
|
+
...requestBody ? { requestBody } : {},
|
|
934
|
+
...requiredScopeAlternatives.length > 0 ? { requiredScopeAlternatives } : {}
|
|
935
|
+
};
|
|
936
|
+
if (tools.length > MAX_INTEGRATION_TOOLS) {
|
|
937
|
+
throw new IntegrationProtocolError(
|
|
938
|
+
"openapi_tool_limit",
|
|
939
|
+
`OpenAPI document exceeds the ${MAX_INTEGRATION_TOOLS}-tool limit`
|
|
940
|
+
);
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
}
|
|
944
|
+
if (tools.length === 0) {
|
|
945
|
+
throw new IntegrationProtocolError("openapi_empty", "OpenAPI document exposes no operations");
|
|
946
|
+
}
|
|
947
|
+
return {
|
|
948
|
+
id: revisionId,
|
|
949
|
+
protocol: "openapi",
|
|
950
|
+
integrationId: options.integrationId,
|
|
951
|
+
contentSha256,
|
|
952
|
+
source: {
|
|
953
|
+
...options.sourceUrl ? { url: options.sourceUrl } : {},
|
|
954
|
+
...options.provider ? { provider: options.provider } : {}
|
|
955
|
+
},
|
|
956
|
+
title: stringValue(info.title) ?? options.integrationId,
|
|
957
|
+
...stringValue(info.description) ? { description: stringValue(info.description) } : {},
|
|
958
|
+
...stringValue(info.version) ? { version: stringValue(info.version) } : {},
|
|
959
|
+
tools,
|
|
960
|
+
bindings
|
|
961
|
+
};
|
|
962
|
+
}
|
|
963
|
+
function discoverOpenApiAuth(document) {
|
|
964
|
+
const components = isRecord2(document.components) ? document.components : {};
|
|
965
|
+
const schemes = isRecord2(components.securitySchemes) ? components.securitySchemes : {};
|
|
966
|
+
for (const raw of Object.values(schemes)) {
|
|
967
|
+
const scheme = resolveObject(document, raw, "security scheme");
|
|
968
|
+
if (scheme.type === "oauth2") {
|
|
969
|
+
const flows = isRecord2(scheme.flows) ? scheme.flows : {};
|
|
970
|
+
const scopes = /* @__PURE__ */ new Set();
|
|
971
|
+
for (const flow of Object.values(flows)) {
|
|
972
|
+
if (!isRecord2(flow) || !isRecord2(flow.scopes)) continue;
|
|
973
|
+
for (const scope of Object.keys(flow.scopes)) scopes.add(scope);
|
|
974
|
+
}
|
|
975
|
+
return { kind: "oauth2", scopes: [...scopes].sort() };
|
|
976
|
+
}
|
|
977
|
+
if (scheme.type === "apiKey" && (scheme.in === "header" || scheme.in === "query" || scheme.in === "cookie") && typeof scheme.name === "string" && scheme.name.length > 0) {
|
|
978
|
+
return { kind: "api_key", carrier: scheme.in, name: scheme.name };
|
|
979
|
+
}
|
|
980
|
+
if (scheme.type === "http" && typeof scheme.scheme === "string") {
|
|
981
|
+
return { kind: "http", scheme: scheme.scheme.toLowerCase() };
|
|
982
|
+
}
|
|
983
|
+
}
|
|
984
|
+
return { kind: "none" };
|
|
985
|
+
}
|
|
986
|
+
var OpenApiMcpServer = class {
|
|
987
|
+
constructor(options) {
|
|
988
|
+
this.options = options;
|
|
989
|
+
this.name = `openapi:${stableToolId(options.revision.integrationId)}`;
|
|
990
|
+
}
|
|
991
|
+
cacheToolsList = true;
|
|
992
|
+
useStructuredContent = true;
|
|
993
|
+
name;
|
|
994
|
+
async connect() {
|
|
995
|
+
}
|
|
996
|
+
async close() {
|
|
997
|
+
}
|
|
998
|
+
async invalidateToolsCache() {
|
|
999
|
+
}
|
|
1000
|
+
async listTools() {
|
|
1001
|
+
return this.options.revision.tools.map(
|
|
1002
|
+
(tool) => ({
|
|
1003
|
+
name: tool.id,
|
|
1004
|
+
description: tool.description,
|
|
1005
|
+
inputSchema: normalizeMcpSchema2(tool.inputSchema),
|
|
1006
|
+
annotations: {
|
|
1007
|
+
readOnlyHint: tool.safety === "read",
|
|
1008
|
+
destructiveHint: tool.safety === "destructive",
|
|
1009
|
+
idempotentHint: isIdempotentMethod(this.options.revision.bindings[tool.id]?.method),
|
|
1010
|
+
openWorldHint: true
|
|
1011
|
+
},
|
|
1012
|
+
_meta: {
|
|
1013
|
+
"opengeni/approvalMode": tool.approvalMode,
|
|
1014
|
+
"opengeni/operationKey": tool.operationKey,
|
|
1015
|
+
"opengeni/revisionId": this.options.revision.id
|
|
1016
|
+
}
|
|
1017
|
+
})
|
|
1018
|
+
);
|
|
1019
|
+
}
|
|
1020
|
+
async callTool(toolName, args, _meta, callOptions) {
|
|
1021
|
+
const result = await invokeOpenApiOperation(
|
|
1022
|
+
this.options,
|
|
1023
|
+
toolName,
|
|
1024
|
+
args ?? {},
|
|
1025
|
+
callOptions?.signal
|
|
1026
|
+
);
|
|
1027
|
+
const content = [
|
|
1028
|
+
{
|
|
1029
|
+
type: "text",
|
|
1030
|
+
text: JSON.stringify(result)
|
|
1031
|
+
}
|
|
1032
|
+
];
|
|
1033
|
+
content.structuredContent = result;
|
|
1034
|
+
content.isError = result.ok === false;
|
|
1035
|
+
return content;
|
|
1036
|
+
}
|
|
1037
|
+
};
|
|
1038
|
+
function createOpenApiMcpServer(options) {
|
|
1039
|
+
return new OpenApiMcpServer(options);
|
|
1040
|
+
}
|
|
1041
|
+
async function invokeOpenApiOperation(options, toolId, args, signal) {
|
|
1042
|
+
const binding = options.revision.bindings[toolId];
|
|
1043
|
+
if (!binding) {
|
|
1044
|
+
throw new IntegrationInvocationError(
|
|
1045
|
+
"operation_not_found",
|
|
1046
|
+
"Integration operation is not present in the frozen revision",
|
|
1047
|
+
"not_started",
|
|
1048
|
+
false
|
|
1049
|
+
);
|
|
1050
|
+
}
|
|
1051
|
+
const firstCredential = await resolveOpenApiCredential(options, binding, toolId, args, false);
|
|
1052
|
+
let response = await sendOpenApiRequest(options, binding, args, firstCredential, signal);
|
|
1053
|
+
if (response.status === 401 && options.credentialResolver && options.authority.connectionRef) {
|
|
1054
|
+
const refreshed = await resolveOpenApiCredential(options, binding, toolId, args, true);
|
|
1055
|
+
if (isReplaySafeMethod(binding.method) && refreshed) {
|
|
1056
|
+
await response.body?.cancel().catch(() => void 0);
|
|
1057
|
+
response = await sendOpenApiRequest(options, binding, args, refreshed, signal);
|
|
1058
|
+
} else {
|
|
1059
|
+
await response.body?.cancel().catch(() => void 0);
|
|
1060
|
+
throw new IntegrationInvocationError(
|
|
1061
|
+
"authorization_rejected",
|
|
1062
|
+
"The connected account is no longer authorized for this operation",
|
|
1063
|
+
isReplaySafeMethod(binding.method) ? "failed" : "unknown",
|
|
1064
|
+
false,
|
|
1065
|
+
response.status
|
|
1066
|
+
);
|
|
1067
|
+
}
|
|
1068
|
+
}
|
|
1069
|
+
if (response.status >= 300 && response.status < 400) {
|
|
1070
|
+
await response.body?.cancel().catch(() => void 0);
|
|
1071
|
+
throw new IntegrationInvocationError(
|
|
1072
|
+
"redirect_rejected",
|
|
1073
|
+
"Integration attempted to redirect a credential-bearing request",
|
|
1074
|
+
binding.method === "get" || binding.method === "head" ? "failed" : "unknown",
|
|
1075
|
+
false,
|
|
1076
|
+
response.status
|
|
1077
|
+
);
|
|
1078
|
+
}
|
|
1079
|
+
const payload = await readIntegrationResponse(
|
|
1080
|
+
response,
|
|
1081
|
+
options.maxResponseBytes ?? DEFAULT_INTEGRATION_RESPONSE_BYTES
|
|
1082
|
+
);
|
|
1083
|
+
const result = {
|
|
1084
|
+
ok: response.ok,
|
|
1085
|
+
status: response.status,
|
|
1086
|
+
contentType: payload.contentType,
|
|
1087
|
+
data: payload.data
|
|
1088
|
+
};
|
|
1089
|
+
if (!response.ok && (response.status === 401 || response.status === 403)) {
|
|
1090
|
+
throw new IntegrationInvocationError(
|
|
1091
|
+
"authorization_rejected",
|
|
1092
|
+
"The connected account is no longer authorized for this operation",
|
|
1093
|
+
binding.method === "get" || binding.method === "head" ? "failed" : "unknown",
|
|
1094
|
+
false,
|
|
1095
|
+
response.status
|
|
1096
|
+
);
|
|
1097
|
+
}
|
|
1098
|
+
return result;
|
|
1099
|
+
}
|
|
1100
|
+
async function resolveOpenApiCredential(options, binding, toolId, args, forceRefresh) {
|
|
1101
|
+
if (!options.credentialResolver || !options.authority.connectionRef) return null;
|
|
1102
|
+
const destinationUrl = buildOperationUrl(binding, args).toString();
|
|
1103
|
+
const credential = await options.credentialResolver.resolve({
|
|
1104
|
+
...options.authority,
|
|
1105
|
+
protocol: "openapi",
|
|
1106
|
+
integrationId: options.revision.integrationId,
|
|
1107
|
+
revisionId: options.revision.id,
|
|
1108
|
+
operationKey: toolId,
|
|
1109
|
+
destinationUrl,
|
|
1110
|
+
...binding.requiredScopeAlternatives ? { requiredScopeAlternatives: binding.requiredScopeAlternatives } : {},
|
|
1111
|
+
...forceRefresh ? { forceRefresh: true } : {}
|
|
1112
|
+
});
|
|
1113
|
+
if (!credential && !forceRefresh) {
|
|
1114
|
+
throw new IntegrationInvocationError(
|
|
1115
|
+
"connection_required",
|
|
1116
|
+
"This integration needs a connected account",
|
|
1117
|
+
"not_started",
|
|
1118
|
+
false
|
|
1119
|
+
);
|
|
1120
|
+
}
|
|
1121
|
+
return credential;
|
|
1122
|
+
}
|
|
1123
|
+
async function sendOpenApiRequest(options, binding, args, credential, signal) {
|
|
1124
|
+
const url = buildOperationUrl(binding, args);
|
|
1125
|
+
const headers = buildOperationHeaders(binding, args);
|
|
1126
|
+
const body = buildOperationBody(binding, args, headers);
|
|
1127
|
+
if (credential) applyCredentialPlacements(url, headers, credential);
|
|
1128
|
+
return await fetchWithDeadline(
|
|
1129
|
+
options.transport,
|
|
1130
|
+
url,
|
|
1131
|
+
{
|
|
1132
|
+
method: binding.method.toUpperCase(),
|
|
1133
|
+
headers,
|
|
1134
|
+
...body !== void 0 ? { body } : {},
|
|
1135
|
+
...signal ? { signal } : {}
|
|
1136
|
+
},
|
|
1137
|
+
options.timeoutMs ?? DEFAULT_INTEGRATION_TIMEOUT_MS
|
|
1138
|
+
);
|
|
1139
|
+
}
|
|
1140
|
+
function readServers(value, explicitBaseUrl, sourceUrl) {
|
|
1141
|
+
if (explicitBaseUrl) return [normalizeServerUrl(explicitBaseUrl)];
|
|
1142
|
+
const servers = Array.isArray(value) ? value.flatMap(
|
|
1143
|
+
(entry) => isRecord2(entry) && typeof entry.url === "string" ? [resolveServerUrl(entry.url, sourceUrl)] : []
|
|
1144
|
+
) : [];
|
|
1145
|
+
if (servers.length > 0) return servers;
|
|
1146
|
+
if (sourceUrl && URL.canParse(sourceUrl)) {
|
|
1147
|
+
const source = new URL(sourceUrl);
|
|
1148
|
+
return [`${source.origin}/`];
|
|
1149
|
+
}
|
|
1150
|
+
return [];
|
|
1151
|
+
}
|
|
1152
|
+
function firstServerUrl(...groups) {
|
|
1153
|
+
const server = groups.flat().find(Boolean);
|
|
1154
|
+
if (!server) {
|
|
1155
|
+
throw new IntegrationProtocolError(
|
|
1156
|
+
"openapi_server_missing",
|
|
1157
|
+
"OpenAPI operation has no resolvable server URL"
|
|
1158
|
+
);
|
|
1159
|
+
}
|
|
1160
|
+
return server;
|
|
1161
|
+
}
|
|
1162
|
+
function resolveServerUrl(value, sourceUrl) {
|
|
1163
|
+
if (/[{}]/.test(value)) {
|
|
1164
|
+
throw new IntegrationProtocolError(
|
|
1165
|
+
"openapi_server_variable",
|
|
1166
|
+
"OpenAPI server variables require an explicit resolved base URL"
|
|
1167
|
+
);
|
|
1168
|
+
}
|
|
1169
|
+
try {
|
|
1170
|
+
return normalizeServerUrl(sourceUrl ? new URL(value, sourceUrl).toString() : value);
|
|
1171
|
+
} catch {
|
|
1172
|
+
throw new IntegrationProtocolError("openapi_server_invalid", "OpenAPI server URL is invalid");
|
|
1173
|
+
}
|
|
1174
|
+
}
|
|
1175
|
+
function normalizeServerUrl(value) {
|
|
1176
|
+
const url = new URL(value);
|
|
1177
|
+
if (!/^https?:$/.test(url.protocol) || url.username || url.password || url.hash) {
|
|
1178
|
+
throw new IntegrationProtocolError("openapi_server_invalid", "OpenAPI server URL is invalid");
|
|
1179
|
+
}
|
|
1180
|
+
return url.toString();
|
|
1181
|
+
}
|
|
1182
|
+
function readParameters(document, value) {
|
|
1183
|
+
if (!Array.isArray(value)) return [];
|
|
1184
|
+
return value.flatMap((raw) => {
|
|
1185
|
+
const parameter = resolveObject(document, raw, "parameter");
|
|
1186
|
+
const location = parameter.in;
|
|
1187
|
+
if (typeof parameter.name !== "string" || location !== "path" && location !== "query" && location !== "header" && location !== "cookie") {
|
|
1188
|
+
return [];
|
|
1189
|
+
}
|
|
1190
|
+
if (location === "header" && forbiddenParameterHeaders.has(parameter.name.toLowerCase()))
|
|
1191
|
+
return [];
|
|
1192
|
+
return [
|
|
1193
|
+
{
|
|
1194
|
+
name: parameter.name,
|
|
1195
|
+
location,
|
|
1196
|
+
required: location === "path" || parameter.required === true,
|
|
1197
|
+
schema: dereferenceSchema(document, parameter.schema),
|
|
1198
|
+
...stringValue(parameter.description) ? { description: stringValue(parameter.description) } : {}
|
|
1199
|
+
}
|
|
1200
|
+
];
|
|
1201
|
+
});
|
|
1202
|
+
}
|
|
1203
|
+
function mergeParameters(base, override) {
|
|
1204
|
+
const merged = new Map(base.map((entry) => [`${entry.location}:${entry.name}`, entry]));
|
|
1205
|
+
for (const entry of override) merged.set(`${entry.location}:${entry.name}`, entry);
|
|
1206
|
+
return [...merged.values()];
|
|
1207
|
+
}
|
|
1208
|
+
function readRequestBody(document, value) {
|
|
1209
|
+
if (value === void 0) return void 0;
|
|
1210
|
+
const body = resolveObject(document, value, "request body");
|
|
1211
|
+
if (!isRecord2(body.content)) return void 0;
|
|
1212
|
+
const schemas = {};
|
|
1213
|
+
for (const [contentType, rawMedia] of Object.entries(body.content)) {
|
|
1214
|
+
if (!isRecord2(rawMedia)) continue;
|
|
1215
|
+
schemas[contentType.toLowerCase()] = dereferenceSchema(document, rawMedia.schema);
|
|
1216
|
+
}
|
|
1217
|
+
const contentTypes = Object.keys(schemas);
|
|
1218
|
+
return contentTypes.length === 0 ? void 0 : { required: body.required === true, contentTypes, schemas };
|
|
1219
|
+
}
|
|
1220
|
+
function operationInputSchema(parameters, body) {
|
|
1221
|
+
const properties = {};
|
|
1222
|
+
const required = [];
|
|
1223
|
+
for (const location of ["path", "query", "header", "cookie"]) {
|
|
1224
|
+
const group = parameters.filter((entry) => entry.location === location);
|
|
1225
|
+
if (group.length === 0) continue;
|
|
1226
|
+
properties[location] = {
|
|
1227
|
+
type: "object",
|
|
1228
|
+
properties: Object.fromEntries(
|
|
1229
|
+
group.map((entry) => [
|
|
1230
|
+
entry.name,
|
|
1231
|
+
{ ...entry.schema, ...entry.description ? { description: entry.description } : {} }
|
|
1232
|
+
])
|
|
1233
|
+
),
|
|
1234
|
+
required: group.filter((entry) => entry.required).map((entry) => entry.name),
|
|
1235
|
+
additionalProperties: false
|
|
1236
|
+
};
|
|
1237
|
+
if (group.some((entry) => entry.required)) required.push(location);
|
|
1238
|
+
}
|
|
1239
|
+
if (body) {
|
|
1240
|
+
properties.body = body.schemas[body.contentTypes[0]] ?? {};
|
|
1241
|
+
if (body.contentTypes.length > 1) {
|
|
1242
|
+
properties.contentType = { type: "string", enum: body.contentTypes };
|
|
1243
|
+
}
|
|
1244
|
+
if (body.required) required.push("body");
|
|
1245
|
+
}
|
|
1246
|
+
return { type: "object", properties, required, additionalProperties: false };
|
|
1247
|
+
}
|
|
1248
|
+
function operationOutputSchema(document, value) {
|
|
1249
|
+
if (!isRecord2(value)) return void 0;
|
|
1250
|
+
for (const status of ["200", "201", "202", "203", "204", "default"]) {
|
|
1251
|
+
if (!(status in value)) continue;
|
|
1252
|
+
const response = resolveObject(document, value[status], "response");
|
|
1253
|
+
if (!isRecord2(response.content)) return void 0;
|
|
1254
|
+
for (const media of Object.values(response.content)) {
|
|
1255
|
+
if (isRecord2(media) && media.schema !== void 0) {
|
|
1256
|
+
return dereferenceSchema(document, media.schema);
|
|
1257
|
+
}
|
|
1258
|
+
}
|
|
1259
|
+
}
|
|
1260
|
+
return void 0;
|
|
1261
|
+
}
|
|
1262
|
+
function readSecurity(value) {
|
|
1263
|
+
if (!Array.isArray(value)) return [];
|
|
1264
|
+
return value.flatMap((entry) => {
|
|
1265
|
+
if (!isRecord2(entry)) return [];
|
|
1266
|
+
const scopes = Object.values(entry).flatMap(
|
|
1267
|
+
(raw) => Array.isArray(raw) ? raw.filter((scope) => typeof scope === "string") : []
|
|
1268
|
+
);
|
|
1269
|
+
return scopes.length > 0 ? [[...new Set(scopes)].sort()] : [];
|
|
1270
|
+
});
|
|
1271
|
+
}
|
|
1272
|
+
function classifyHttpSafety(method, operation) {
|
|
1273
|
+
if (method === "get" || method === "head" || method === "options") return "read";
|
|
1274
|
+
const text = `${stringValue(operation.operationId) ?? ""} ${stringValue(operation.summary) ?? ""}`.toLowerCase();
|
|
1275
|
+
return method === "delete" || /\b(delete|destroy|remove|revoke|cancel|purge)\b/.test(text) ? "destructive" : "write";
|
|
1276
|
+
}
|
|
1277
|
+
function operationIdentity(method, path, operationId) {
|
|
1278
|
+
return typeof operationId === "string" && operationId.trim() ? operationId.trim() : `${method}_${path}`;
|
|
1279
|
+
}
|
|
1280
|
+
function toolDescription(method, path, operation, safety) {
|
|
1281
|
+
const description = stringValue(operation.description) ?? stringValue(operation.summary);
|
|
1282
|
+
const approval = safety === "read" ? "Read-only." : "Changes external state and requires approval.";
|
|
1283
|
+
return `${description ? `${description.trim()} ` : ""}${method.toUpperCase()} ${path}. ${approval}`.trim();
|
|
1284
|
+
}
|
|
1285
|
+
function isIdempotentMethod(method) {
|
|
1286
|
+
return method === "get" || method === "head" || method === "options" || method === "put" || method === "delete";
|
|
1287
|
+
}
|
|
1288
|
+
function isReplaySafeMethod(method) {
|
|
1289
|
+
return method === "get" || method === "head" || method === "options";
|
|
1290
|
+
}
|
|
1291
|
+
function buildOperationUrl(binding, args) {
|
|
1292
|
+
const pathArgs = objectValue(args.path);
|
|
1293
|
+
const path = binding.pathTemplate.replace(/\{([^}]+)\}/g, (_match, name) => {
|
|
1294
|
+
const value = pathArgs[name];
|
|
1295
|
+
if (value === void 0 || value === null) {
|
|
1296
|
+
throw new IntegrationInvocationError(
|
|
1297
|
+
"path_parameter_missing",
|
|
1298
|
+
"A required integration path parameter is missing",
|
|
1299
|
+
"not_started",
|
|
1300
|
+
false
|
|
1301
|
+
);
|
|
1302
|
+
}
|
|
1303
|
+
return encodeURIComponent(scalarString(value));
|
|
1304
|
+
});
|
|
1305
|
+
const base = new URL(binding.serverUrl);
|
|
1306
|
+
const url = new URL(
|
|
1307
|
+
path.replace(/^\//, ""),
|
|
1308
|
+
base.toString().endsWith("/") ? base : new URL(`${base}/`)
|
|
1309
|
+
);
|
|
1310
|
+
const query = objectValue(args.query);
|
|
1311
|
+
for (const [name, value] of Object.entries(query)) appendQueryValue(url, name, value);
|
|
1312
|
+
return url;
|
|
1313
|
+
}
|
|
1314
|
+
function buildOperationHeaders(binding, args) {
|
|
1315
|
+
const headers = new Headers({ accept: "application/json, text/plain;q=0.9, */*;q=0.5" });
|
|
1316
|
+
for (const [name, value] of Object.entries(objectValue(args.header))) {
|
|
1317
|
+
if (forbiddenParameterHeaders.has(name.toLowerCase())) continue;
|
|
1318
|
+
headers.set(name, scalarString(value));
|
|
1319
|
+
}
|
|
1320
|
+
const cookies = Object.entries(objectValue(args.cookie)).map(
|
|
1321
|
+
([name, value]) => `${encodeURIComponent(name)}=${encodeURIComponent(scalarString(value))}`
|
|
1322
|
+
);
|
|
1323
|
+
if (cookies.length > 0) headers.set("cookie", cookies.join("; "));
|
|
1324
|
+
if (binding.requestBody && args.body !== void 0) {
|
|
1325
|
+
const requested = typeof args.contentType === "string" ? args.contentType.toLowerCase() : void 0;
|
|
1326
|
+
const contentType = requested && binding.requestBody.contentTypes.includes(requested) ? requested : binding.requestBody.contentTypes[0];
|
|
1327
|
+
headers.set("content-type", contentType);
|
|
1328
|
+
}
|
|
1329
|
+
return headers;
|
|
1330
|
+
}
|
|
1331
|
+
function buildOperationBody(binding, args, headers) {
|
|
1332
|
+
if (!binding.requestBody || args.body === void 0) return void 0;
|
|
1333
|
+
const contentType = headers.get("content-type") ?? "application/json";
|
|
1334
|
+
if (contentType === "application/x-www-form-urlencoded") {
|
|
1335
|
+
const params = new URLSearchParams();
|
|
1336
|
+
for (const [key, value] of Object.entries(objectValue(args.body)))
|
|
1337
|
+
appendSearchParam(params, key, value);
|
|
1338
|
+
return params;
|
|
1339
|
+
}
|
|
1340
|
+
if (contentType === "application/json" || contentType.endsWith("+json")) {
|
|
1341
|
+
return JSON.stringify(args.body);
|
|
1342
|
+
}
|
|
1343
|
+
if (typeof args.body === "string") return args.body;
|
|
1344
|
+
throw new IntegrationInvocationError(
|
|
1345
|
+
"request_body_unsupported",
|
|
1346
|
+
"This operation requires a text body for the selected content type",
|
|
1347
|
+
"not_started",
|
|
1348
|
+
false
|
|
1349
|
+
);
|
|
1350
|
+
}
|
|
1351
|
+
function appendQueryValue(url, name, value) {
|
|
1352
|
+
if (Array.isArray(value)) {
|
|
1353
|
+
for (const entry of value) url.searchParams.append(name, scalarString(entry));
|
|
1354
|
+
} else if (value !== void 0 && value !== null) {
|
|
1355
|
+
url.searchParams.append(name, scalarString(value));
|
|
1356
|
+
}
|
|
1357
|
+
}
|
|
1358
|
+
function appendSearchParam(params, name, value) {
|
|
1359
|
+
if (Array.isArray(value)) {
|
|
1360
|
+
for (const entry of value) params.append(name, scalarString(entry));
|
|
1361
|
+
} else if (value !== void 0 && value !== null) {
|
|
1362
|
+
params.append(name, scalarString(value));
|
|
1363
|
+
}
|
|
1364
|
+
}
|
|
1365
|
+
function scalarString(value) {
|
|
1366
|
+
if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
|
|
1367
|
+
return String(value);
|
|
1368
|
+
}
|
|
1369
|
+
throw new IntegrationInvocationError(
|
|
1370
|
+
"parameter_invalid",
|
|
1371
|
+
"Integration parameters must be strings, numbers, booleans, or arrays of them",
|
|
1372
|
+
"not_started",
|
|
1373
|
+
false
|
|
1374
|
+
);
|
|
1375
|
+
}
|
|
1376
|
+
function objectValue(value) {
|
|
1377
|
+
return isRecord2(value) ? value : {};
|
|
1378
|
+
}
|
|
1379
|
+
function resolveObject(document, value, label) {
|
|
1380
|
+
const resolved = resolveLocalRef(document, value);
|
|
1381
|
+
if (!isRecord2(resolved)) {
|
|
1382
|
+
throw new IntegrationProtocolError("openapi_shape", `OpenAPI ${label} is invalid`);
|
|
1383
|
+
}
|
|
1384
|
+
return resolved;
|
|
1385
|
+
}
|
|
1386
|
+
function resolveLocalRef(document, value) {
|
|
1387
|
+
if (!isRecord2(value) || typeof value.$ref !== "string") return value;
|
|
1388
|
+
if (!value.$ref.startsWith("#/")) {
|
|
1389
|
+
throw new IntegrationProtocolError(
|
|
1390
|
+
"openapi_external_ref",
|
|
1391
|
+
"External OpenAPI references are not supported; bundle the document first"
|
|
1392
|
+
);
|
|
1393
|
+
}
|
|
1394
|
+
return value.$ref.slice(2).split("/").map((part) => part.replaceAll("~1", "/").replaceAll("~0", "~")).reduce((current, part) => isRecord2(current) ? current[part] : void 0, document);
|
|
1395
|
+
}
|
|
1396
|
+
function dereferenceSchema(document, value, seen = /* @__PURE__ */ new Set(), depth = 0) {
|
|
1397
|
+
if (depth > 20) return {};
|
|
1398
|
+
if (isRecord2(value) && typeof value.$ref === "string") {
|
|
1399
|
+
if (seen.has(value.$ref)) return {};
|
|
1400
|
+
const nextSeen = new Set(seen).add(value.$ref);
|
|
1401
|
+
return dereferenceSchema(document, resolveLocalRef(document, value), nextSeen, depth + 1);
|
|
1402
|
+
}
|
|
1403
|
+
if (!isRecord2(value)) return {};
|
|
1404
|
+
const result = {};
|
|
1405
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
1406
|
+
if (key === "properties" && isRecord2(entry)) {
|
|
1407
|
+
result.properties = Object.fromEntries(
|
|
1408
|
+
Object.entries(entry).map(([name, schema]) => [
|
|
1409
|
+
name,
|
|
1410
|
+
dereferenceSchema(document, schema, seen, depth + 1)
|
|
1411
|
+
])
|
|
1412
|
+
);
|
|
1413
|
+
} else if (key === "items") {
|
|
1414
|
+
result.items = dereferenceSchema(document, entry, seen, depth + 1);
|
|
1415
|
+
} else if (key === "allOf" || key === "anyOf" || key === "oneOf") {
|
|
1416
|
+
result[key] = Array.isArray(entry) ? entry.map((schema) => dereferenceSchema(document, schema, seen, depth + 1)) : [];
|
|
1417
|
+
} else if (key !== "$ref") {
|
|
1418
|
+
result[key] = entry;
|
|
1419
|
+
}
|
|
1420
|
+
}
|
|
1421
|
+
return result;
|
|
1422
|
+
}
|
|
1423
|
+
function normalizeMcpSchema2(schema) {
|
|
1424
|
+
return {
|
|
1425
|
+
type: "object",
|
|
1426
|
+
properties: isRecord2(schema.properties) ? schema.properties : {},
|
|
1427
|
+
required: Array.isArray(schema.required) ? schema.required.filter((entry) => typeof entry === "string") : [],
|
|
1428
|
+
additionalProperties: schema.additionalProperties === true
|
|
1429
|
+
};
|
|
1430
|
+
}
|
|
1431
|
+
function stringValue(value) {
|
|
1432
|
+
return typeof value === "string" && value.trim() ? value : void 0;
|
|
1433
|
+
}
|
|
1434
|
+
function isRecord2(value) {
|
|
1435
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
1436
|
+
}
|
|
1437
|
+
|
|
1438
|
+
// src/providers.ts
|
|
1439
|
+
var accountIdentityFeature = (provider) => ({
|
|
1440
|
+
featureKey: "account-identity",
|
|
1441
|
+
kind: "identity_link",
|
|
1442
|
+
configSchema: { type: "object", properties: {}, additionalProperties: false },
|
|
1443
|
+
capabilities: {
|
|
1444
|
+
provider,
|
|
1445
|
+
connectionRequired: true,
|
|
1446
|
+
identity: "connected_account"
|
|
1447
|
+
}
|
|
1448
|
+
});
|
|
1449
|
+
var driveKnowledgeFeature = (provider) => ({
|
|
1450
|
+
featureKey: "drive-content",
|
|
1451
|
+
kind: "knowledge_source",
|
|
1452
|
+
configSchema: {
|
|
1453
|
+
type: "object",
|
|
1454
|
+
required: ["sources", "destination", "syncCadence", "readPolicy"],
|
|
1455
|
+
properties: {
|
|
1456
|
+
sources: {
|
|
1457
|
+
type: "array",
|
|
1458
|
+
minItems: 1,
|
|
1459
|
+
maxItems: 100,
|
|
1460
|
+
items: {
|
|
1461
|
+
type: "object",
|
|
1462
|
+
required: ["id", "name", "mimeType", "sourceKind", "includeDescendants"],
|
|
1463
|
+
properties: {
|
|
1464
|
+
id: { type: "string", minLength: 1, maxLength: 512 },
|
|
1465
|
+
name: { type: "string", minLength: 1, maxLength: 1024 },
|
|
1466
|
+
mimeType: { type: "string", minLength: 1, maxLength: 256 },
|
|
1467
|
+
driveId: { type: "string", minLength: 1, maxLength: 512 },
|
|
1468
|
+
sourceKind: {
|
|
1469
|
+
type: "string",
|
|
1470
|
+
enum: provider === "google-drive" ? ["my_drive", "shared_drive", "folder"] : ["my_drive", "shared_library", "folder"]
|
|
1471
|
+
},
|
|
1472
|
+
includeDescendants: { type: "boolean" }
|
|
1473
|
+
},
|
|
1474
|
+
additionalProperties: false
|
|
1475
|
+
}
|
|
1476
|
+
},
|
|
1477
|
+
destination: {
|
|
1478
|
+
type: "object",
|
|
1479
|
+
required: ["authorityKind", "authorityAccountId"],
|
|
1480
|
+
properties: {
|
|
1481
|
+
authorityKind: {
|
|
1482
|
+
type: "string",
|
|
1483
|
+
enum: ["organization", "workspace", "personal"]
|
|
1484
|
+
},
|
|
1485
|
+
authorityAccountId: { type: "string", minLength: 1, maxLength: 128 },
|
|
1486
|
+
authorityWorkspaceId: { type: "string", minLength: 1, maxLength: 128 },
|
|
1487
|
+
authoritySubjectId: { type: "string", minLength: 1, maxLength: 512 },
|
|
1488
|
+
collectionId: { type: "string", minLength: 1, maxLength: 512 }
|
|
1489
|
+
},
|
|
1490
|
+
additionalProperties: false
|
|
1491
|
+
},
|
|
1492
|
+
syncCadence: { type: "string", enum: ["manual", "hourly", "daily"] },
|
|
1493
|
+
readPolicy: { type: "string", enum: ["allow", "ask", "block"] }
|
|
1494
|
+
},
|
|
1495
|
+
additionalProperties: false
|
|
1496
|
+
},
|
|
1497
|
+
capabilities: {
|
|
1498
|
+
provider,
|
|
1499
|
+
connectionRequired: true,
|
|
1500
|
+
sync: "incremental",
|
|
1501
|
+
cursor: provider === "google-drive" ? "page_token" : "delta_link"
|
|
1502
|
+
}
|
|
1503
|
+
});
|
|
1504
|
+
var mailboxFeatures = (provider) => [
|
|
1505
|
+
{
|
|
1506
|
+
featureKey: "mail-inbox",
|
|
1507
|
+
kind: "inbound_trigger",
|
|
1508
|
+
configSchema: {
|
|
1509
|
+
type: "object",
|
|
1510
|
+
properties: {
|
|
1511
|
+
folder: { type: "string", minLength: 1, maxLength: 256 },
|
|
1512
|
+
unreadOnly: { type: "boolean" }
|
|
1513
|
+
},
|
|
1514
|
+
additionalProperties: false
|
|
1515
|
+
},
|
|
1516
|
+
capabilities: {
|
|
1517
|
+
provider,
|
|
1518
|
+
connectionRequired: true,
|
|
1519
|
+
delivery: "poll",
|
|
1520
|
+
cursor: provider === "google-gmail" ? "history_id" : "delta_link"
|
|
1521
|
+
}
|
|
1522
|
+
},
|
|
1523
|
+
{
|
|
1524
|
+
featureKey: "mail-delivery",
|
|
1525
|
+
kind: "delivery_destination",
|
|
1526
|
+
configSchema: {
|
|
1527
|
+
type: "object",
|
|
1528
|
+
properties: {
|
|
1529
|
+
fromAlias: { type: "string", minLength: 1, maxLength: 512 },
|
|
1530
|
+
saveToSent: { type: "boolean" }
|
|
1531
|
+
},
|
|
1532
|
+
additionalProperties: false
|
|
1533
|
+
},
|
|
1534
|
+
capabilities: {
|
|
1535
|
+
provider,
|
|
1536
|
+
connectionRequired: true,
|
|
1537
|
+
delivery: "email"
|
|
1538
|
+
}
|
|
1539
|
+
},
|
|
1540
|
+
accountIdentityFeature(provider === "google-gmail" ? "google" : "microsoft")
|
|
1541
|
+
];
|
|
1542
|
+
var googleDiscoveryUrl = (service, version) => `https://www.googleapis.com/discovery/v1/apis/${service}/${version}/rest`;
|
|
1543
|
+
var googleOAuth = (scopes) => ({
|
|
1544
|
+
authorizationUrl: "https://accounts.google.com/o/oauth2/v2/auth",
|
|
1545
|
+
tokenUrl: "https://oauth2.googleapis.com/token",
|
|
1546
|
+
scopes: ["openid", "email", "profile", ...scopes],
|
|
1547
|
+
tokenPlacement: { carrier: "header", name: "Authorization", prefix: "Bearer " }
|
|
1548
|
+
});
|
|
1549
|
+
var GOOGLE_DRIVE_PRESET = {
|
|
1550
|
+
id: "google-drive",
|
|
1551
|
+
name: "Google Drive",
|
|
1552
|
+
summary: "Files, folders, permissions, and shared drives.",
|
|
1553
|
+
family: "google",
|
|
1554
|
+
sourceFormat: "google-discovery",
|
|
1555
|
+
sourceUrl: googleDiscoveryUrl("drive", "v3"),
|
|
1556
|
+
baseUrl: "https://www.googleapis.com/drive/v3/",
|
|
1557
|
+
oauth: googleOAuth(["https://www.googleapis.com/auth/drive"]),
|
|
1558
|
+
healthOperation: "drive.about.get",
|
|
1559
|
+
healthArgs: { query: { fields: "user" } },
|
|
1560
|
+
features: [driveKnowledgeFeature("google-drive"), accountIdentityFeature("google")]
|
|
1561
|
+
};
|
|
1562
|
+
var GOOGLE_GMAIL_PRESET = {
|
|
1563
|
+
id: "google-gmail",
|
|
1564
|
+
name: "Gmail",
|
|
1565
|
+
summary: "Messages, threads, labels, drafts, and sending mail.",
|
|
1566
|
+
family: "google",
|
|
1567
|
+
sourceFormat: "google-discovery",
|
|
1568
|
+
sourceUrl: googleDiscoveryUrl("gmail", "v1"),
|
|
1569
|
+
baseUrl: "https://gmail.googleapis.com/",
|
|
1570
|
+
oauth: googleOAuth(["https://mail.google.com/"]),
|
|
1571
|
+
healthOperation: "gmail.users.labels.list",
|
|
1572
|
+
healthArgs: { path: { userId: "me" } },
|
|
1573
|
+
features: mailboxFeatures("google-gmail")
|
|
1574
|
+
};
|
|
1575
|
+
var MICROSOFT_GRAPH_OPENAPI_URL = "https://raw.githubusercontent.com/microsoftgraph/msgraph-metadata/master/openapi/v1.0/openapi.yaml";
|
|
1576
|
+
var MICROSOFT_GRAPH_BASE_URL = "https://graph.microsoft.com/v1.0";
|
|
1577
|
+
var microsoftOAuth = (scopes) => ({
|
|
1578
|
+
authorizationUrl: "https://login.microsoftonline.com/common/oauth2/v2.0/authorize",
|
|
1579
|
+
tokenUrl: "https://login.microsoftonline.com/common/oauth2/v2.0/token",
|
|
1580
|
+
scopes: ["offline_access", "User.Read", ...scopes],
|
|
1581
|
+
tokenPlacement: { carrier: "header", name: "Authorization", prefix: "Bearer " }
|
|
1582
|
+
});
|
|
1583
|
+
var MICROSOFT_OUTLOOK_MAIL_PRESET = {
|
|
1584
|
+
id: "microsoft-outlook-mail",
|
|
1585
|
+
name: "Outlook Mail",
|
|
1586
|
+
summary: "Messages, folders, attachments, settings, and sending mail.",
|
|
1587
|
+
family: "microsoft",
|
|
1588
|
+
sourceFormat: "openapi",
|
|
1589
|
+
sourceUrl: MICROSOFT_GRAPH_OPENAPI_URL,
|
|
1590
|
+
baseUrl: MICROSOFT_GRAPH_BASE_URL,
|
|
1591
|
+
oauth: microsoftOAuth(["Mail.ReadWrite", "Mail.Send", "MailboxSettings.ReadWrite"]),
|
|
1592
|
+
pathPrefixes: [
|
|
1593
|
+
"/me/messages",
|
|
1594
|
+
"/me/mailFolders",
|
|
1595
|
+
"/me/sendMail",
|
|
1596
|
+
"/me/getMailTips",
|
|
1597
|
+
"/me/inferenceClassification",
|
|
1598
|
+
"/me/mailboxSettings",
|
|
1599
|
+
"/me/outlook"
|
|
1600
|
+
],
|
|
1601
|
+
features: mailboxFeatures("microsoft-outlook-mail")
|
|
1602
|
+
};
|
|
1603
|
+
var MICROSOFT_OUTLOOK_CALENDAR_PRESET = {
|
|
1604
|
+
id: "microsoft-outlook-calendar",
|
|
1605
|
+
name: "Outlook Calendar",
|
|
1606
|
+
summary: "Calendars, events, availability, and scheduling.",
|
|
1607
|
+
family: "microsoft",
|
|
1608
|
+
sourceFormat: "openapi",
|
|
1609
|
+
sourceUrl: MICROSOFT_GRAPH_OPENAPI_URL,
|
|
1610
|
+
baseUrl: MICROSOFT_GRAPH_BASE_URL,
|
|
1611
|
+
oauth: microsoftOAuth(["Calendars.ReadWrite"]),
|
|
1612
|
+
pathPrefixes: [
|
|
1613
|
+
"/me/calendar",
|
|
1614
|
+
"/me/calendars",
|
|
1615
|
+
"/me/calendarGroups",
|
|
1616
|
+
"/me/calendarView",
|
|
1617
|
+
"/me/events",
|
|
1618
|
+
"/me/findMeetingTimes",
|
|
1619
|
+
"/me/reminderView"
|
|
1620
|
+
],
|
|
1621
|
+
features: [
|
|
1622
|
+
{
|
|
1623
|
+
featureKey: "calendar-events",
|
|
1624
|
+
kind: "inbound_trigger",
|
|
1625
|
+
configSchema: {
|
|
1626
|
+
type: "object",
|
|
1627
|
+
properties: {
|
|
1628
|
+
calendarId: { type: "string", minLength: 1, maxLength: 512 },
|
|
1629
|
+
lookaheadDays: { type: "integer", minimum: 1, maximum: 365 }
|
|
1630
|
+
},
|
|
1631
|
+
additionalProperties: false
|
|
1632
|
+
},
|
|
1633
|
+
capabilities: {
|
|
1634
|
+
provider: "microsoft-outlook-calendar",
|
|
1635
|
+
connectionRequired: true,
|
|
1636
|
+
delivery: "poll",
|
|
1637
|
+
cursor: "delta_link"
|
|
1638
|
+
}
|
|
1639
|
+
},
|
|
1640
|
+
{
|
|
1641
|
+
featureKey: "calendar-delivery",
|
|
1642
|
+
kind: "delivery_destination",
|
|
1643
|
+
configSchema: {
|
|
1644
|
+
type: "object",
|
|
1645
|
+
properties: {
|
|
1646
|
+
calendarId: { type: "string", minLength: 1, maxLength: 512 }
|
|
1647
|
+
},
|
|
1648
|
+
additionalProperties: false
|
|
1649
|
+
},
|
|
1650
|
+
capabilities: {
|
|
1651
|
+
provider: "microsoft-outlook-calendar",
|
|
1652
|
+
connectionRequired: true,
|
|
1653
|
+
delivery: "calendar_event"
|
|
1654
|
+
}
|
|
1655
|
+
},
|
|
1656
|
+
accountIdentityFeature("microsoft")
|
|
1657
|
+
]
|
|
1658
|
+
};
|
|
1659
|
+
var MICROSOFT_OUTLOOK_CONTACTS_PRESET = {
|
|
1660
|
+
id: "microsoft-outlook-contacts",
|
|
1661
|
+
name: "Outlook Contacts",
|
|
1662
|
+
summary: "Contacts, contact folders, and people suggestions.",
|
|
1663
|
+
family: "microsoft",
|
|
1664
|
+
sourceFormat: "openapi",
|
|
1665
|
+
sourceUrl: MICROSOFT_GRAPH_OPENAPI_URL,
|
|
1666
|
+
baseUrl: MICROSOFT_GRAPH_BASE_URL,
|
|
1667
|
+
oauth: microsoftOAuth(["Contacts.ReadWrite", "People.Read.All"]),
|
|
1668
|
+
pathPrefixes: ["/me/contacts", "/me/contactFolders", "/me/people"],
|
|
1669
|
+
features: [accountIdentityFeature("microsoft")]
|
|
1670
|
+
};
|
|
1671
|
+
var MICROSOFT_ONEDRIVE_PRESET = {
|
|
1672
|
+
id: "microsoft-onedrive",
|
|
1673
|
+
name: "OneDrive",
|
|
1674
|
+
summary: "Drives, files, folders, sharing links, and permissions.",
|
|
1675
|
+
family: "microsoft",
|
|
1676
|
+
sourceFormat: "openapi",
|
|
1677
|
+
sourceUrl: MICROSOFT_GRAPH_OPENAPI_URL,
|
|
1678
|
+
baseUrl: MICROSOFT_GRAPH_BASE_URL,
|
|
1679
|
+
oauth: microsoftOAuth(["Files.ReadWrite.All", "Sites.ReadWrite.All"]),
|
|
1680
|
+
pathPrefixes: ["/me/drive", "/me/drives", "/me/followedSites", "/drives", "/shares"],
|
|
1681
|
+
features: [driveKnowledgeFeature("microsoft-onedrive"), accountIdentityFeature("microsoft")]
|
|
1682
|
+
};
|
|
1683
|
+
var CORE_PROVIDER_PRESETS = [
|
|
1684
|
+
GOOGLE_DRIVE_PRESET,
|
|
1685
|
+
GOOGLE_GMAIL_PRESET,
|
|
1686
|
+
MICROSOFT_OUTLOOK_MAIL_PRESET,
|
|
1687
|
+
MICROSOFT_OUTLOOK_CALENDAR_PRESET,
|
|
1688
|
+
MICROSOFT_OUTLOOK_CONTACTS_PRESET,
|
|
1689
|
+
MICROSOFT_ONEDRIVE_PRESET
|
|
1690
|
+
];
|
|
1691
|
+
function providerPresetById(id) {
|
|
1692
|
+
return CORE_PROVIDER_PRESETS.find((preset) => preset.id === id);
|
|
1693
|
+
}
|
|
1694
|
+
function providerDomainForPreset(preset) {
|
|
1695
|
+
return new URL(preset.baseUrl).hostname.toLowerCase();
|
|
1696
|
+
}
|
|
1697
|
+
function integrationFeaturesForPreset(presetId) {
|
|
1698
|
+
return presetId ? providerPresetById(presetId)?.features ?? [] : [];
|
|
1699
|
+
}
|
|
1700
|
+
function filterOpenApiDocumentForPreset(document, preset) {
|
|
1701
|
+
if (!preset.pathPrefixes?.length) return document;
|
|
1702
|
+
if (!isRecord3(document.paths)) {
|
|
1703
|
+
throw new IntegrationProtocolError("openapi_paths", "OpenAPI document has no paths object");
|
|
1704
|
+
}
|
|
1705
|
+
const paths = Object.fromEntries(
|
|
1706
|
+
Object.entries(document.paths).filter(
|
|
1707
|
+
([path]) => preset.pathPrefixes.some((prefix) => path === prefix || path.startsWith(`${prefix}/`))
|
|
1708
|
+
)
|
|
1709
|
+
);
|
|
1710
|
+
if (Object.keys(paths).length === 0) {
|
|
1711
|
+
throw new IntegrationProtocolError(
|
|
1712
|
+
"provider_preset_empty",
|
|
1713
|
+
`${preset.name} did not match any operations in the supplied OpenAPI document`
|
|
1714
|
+
);
|
|
1715
|
+
}
|
|
1716
|
+
return {
|
|
1717
|
+
...document,
|
|
1718
|
+
paths,
|
|
1719
|
+
...preset.baseUrl ? { servers: [{ url: preset.baseUrl }] } : {}
|
|
1720
|
+
};
|
|
1721
|
+
}
|
|
1722
|
+
function googleDiscoveryToOpenApi(discovery) {
|
|
1723
|
+
if (!isRecord3(discovery)) {
|
|
1724
|
+
throw new IntegrationProtocolError(
|
|
1725
|
+
"google_discovery_shape",
|
|
1726
|
+
"Google Discovery document is invalid"
|
|
1727
|
+
);
|
|
1728
|
+
}
|
|
1729
|
+
const rootUrl = stringValue2(discovery.rootUrl) ?? stringValue2(discovery.baseUrl);
|
|
1730
|
+
const servicePath = stringValue2(discovery.servicePath) ?? "";
|
|
1731
|
+
if (!rootUrl || !URL.canParse(rootUrl)) {
|
|
1732
|
+
throw new IntegrationProtocolError(
|
|
1733
|
+
"google_discovery_server",
|
|
1734
|
+
"Google Discovery document has no valid root URL"
|
|
1735
|
+
);
|
|
1736
|
+
}
|
|
1737
|
+
const paths = {};
|
|
1738
|
+
collectGoogleMethods(discovery, discovery.methods, paths);
|
|
1739
|
+
collectGoogleResources(discovery, discovery.resources, paths);
|
|
1740
|
+
if (Object.keys(paths).length === 0) {
|
|
1741
|
+
throw new IntegrationProtocolError(
|
|
1742
|
+
"google_discovery_empty",
|
|
1743
|
+
"Google Discovery document exposes no methods"
|
|
1744
|
+
);
|
|
1745
|
+
}
|
|
1746
|
+
const scopes = isRecord3(discovery.auth) && isRecord3(discovery.auth.oauth2) ? discovery.auth.oauth2.scopes : void 0;
|
|
1747
|
+
const scopeMap = isRecord3(scopes) ? Object.fromEntries(
|
|
1748
|
+
Object.entries(scopes).map(([scope, value]) => [
|
|
1749
|
+
scope,
|
|
1750
|
+
isRecord3(value) && typeof value.description === "string" ? value.description : ""
|
|
1751
|
+
])
|
|
1752
|
+
) : {};
|
|
1753
|
+
return {
|
|
1754
|
+
openapi: "3.1.0",
|
|
1755
|
+
info: {
|
|
1756
|
+
title: stringValue2(discovery.title) ?? stringValue2(discovery.name) ?? "Google API",
|
|
1757
|
+
description: stringValue2(discovery.description) ?? "Google Discovery API",
|
|
1758
|
+
version: stringValue2(discovery.version) ?? "v1"
|
|
1759
|
+
},
|
|
1760
|
+
servers: [{ url: new URL(servicePath, rootUrl).toString() }],
|
|
1761
|
+
paths,
|
|
1762
|
+
components: {
|
|
1763
|
+
schemas: Object.fromEntries(
|
|
1764
|
+
Object.entries(isRecord3(discovery.schemas) ? discovery.schemas : {}).map(
|
|
1765
|
+
([name, schema]) => [name, convertGoogleSchema(schema)]
|
|
1766
|
+
)
|
|
1767
|
+
),
|
|
1768
|
+
securitySchemes: {
|
|
1769
|
+
googleOAuth2: {
|
|
1770
|
+
type: "oauth2",
|
|
1771
|
+
flows: {
|
|
1772
|
+
authorizationCode: {
|
|
1773
|
+
authorizationUrl: "https://accounts.google.com/o/oauth2/v2/auth",
|
|
1774
|
+
tokenUrl: "https://oauth2.googleapis.com/token",
|
|
1775
|
+
scopes: scopeMap
|
|
1776
|
+
}
|
|
1777
|
+
}
|
|
1778
|
+
}
|
|
1779
|
+
}
|
|
1780
|
+
},
|
|
1781
|
+
security: Object.keys(scopeMap).length > 0 ? [{ googleOAuth2: [] }] : []
|
|
1782
|
+
};
|
|
1783
|
+
}
|
|
1784
|
+
function collectGoogleResources(document, value, paths) {
|
|
1785
|
+
if (!isRecord3(value)) return;
|
|
1786
|
+
for (const resource of Object.values(value)) {
|
|
1787
|
+
if (!isRecord3(resource)) continue;
|
|
1788
|
+
collectGoogleMethods(document, resource.methods, paths);
|
|
1789
|
+
collectGoogleResources(document, resource.resources, paths);
|
|
1790
|
+
}
|
|
1791
|
+
}
|
|
1792
|
+
function collectGoogleMethods(document, value, paths) {
|
|
1793
|
+
if (!isRecord3(value)) return;
|
|
1794
|
+
for (const [fallbackId, rawMethod] of Object.entries(value)) {
|
|
1795
|
+
if (!isRecord3(rawMethod)) continue;
|
|
1796
|
+
const path = stringValue2(rawMethod.path);
|
|
1797
|
+
const httpMethod = stringValue2(rawMethod.httpMethod)?.toLowerCase();
|
|
1798
|
+
if (!path || !httpMethod) continue;
|
|
1799
|
+
const parameters = Object.entries(
|
|
1800
|
+
isRecord3(rawMethod.parameters) ? rawMethod.parameters : {}
|
|
1801
|
+
).flatMap(([name, rawParameter]) => {
|
|
1802
|
+
if (!isRecord3(rawParameter)) return [];
|
|
1803
|
+
const location = rawParameter.location === "path" ? "path" : "query";
|
|
1804
|
+
return [
|
|
1805
|
+
{
|
|
1806
|
+
name,
|
|
1807
|
+
in: location,
|
|
1808
|
+
required: location === "path" || rawParameter.required === true,
|
|
1809
|
+
...stringValue2(rawParameter.description) ? { description: stringValue2(rawParameter.description) } : {},
|
|
1810
|
+
schema: convertGoogleSchema(rawParameter)
|
|
1811
|
+
}
|
|
1812
|
+
];
|
|
1813
|
+
});
|
|
1814
|
+
const requestRef = isRecord3(rawMethod.request) ? stringValue2(rawMethod.request.$ref) : void 0;
|
|
1815
|
+
const responseRef = isRecord3(rawMethod.response) ? stringValue2(rawMethod.response.$ref) : void 0;
|
|
1816
|
+
const operation = {
|
|
1817
|
+
operationId: stringValue2(rawMethod.id) ?? fallbackId,
|
|
1818
|
+
summary: stringValue2(rawMethod.description) ?? stringValue2(rawMethod.id) ?? fallbackId,
|
|
1819
|
+
description: stringValue2(rawMethod.description),
|
|
1820
|
+
parameters,
|
|
1821
|
+
responses: {
|
|
1822
|
+
"200": {
|
|
1823
|
+
description: "Successful response",
|
|
1824
|
+
...responseRef ? {
|
|
1825
|
+
content: {
|
|
1826
|
+
"application/json": {
|
|
1827
|
+
schema: { $ref: `#/components/schemas/${escapeJsonPointer(responseRef)}` }
|
|
1828
|
+
}
|
|
1829
|
+
}
|
|
1830
|
+
} : {}
|
|
1831
|
+
}
|
|
1832
|
+
},
|
|
1833
|
+
...Array.isArray(rawMethod.scopes) && rawMethod.scopes.length > 0 ? { security: [{ googleOAuth2: rawMethod.scopes }] } : {}
|
|
1834
|
+
};
|
|
1835
|
+
if (requestRef) {
|
|
1836
|
+
operation.requestBody = {
|
|
1837
|
+
required: true,
|
|
1838
|
+
content: {
|
|
1839
|
+
"application/json": {
|
|
1840
|
+
schema: { $ref: `#/components/schemas/${escapeJsonPointer(requestRef)}` }
|
|
1841
|
+
}
|
|
1842
|
+
}
|
|
1843
|
+
};
|
|
1844
|
+
}
|
|
1845
|
+
const normalizedPath = path.startsWith("/") ? path : `/${path}`;
|
|
1846
|
+
const existing = isRecord3(paths[normalizedPath]) ? paths[normalizedPath] : {};
|
|
1847
|
+
paths[normalizedPath] = { ...existing, [httpMethod]: operation };
|
|
1848
|
+
}
|
|
1849
|
+
}
|
|
1850
|
+
function convertGoogleSchema(value, depth = 0) {
|
|
1851
|
+
if (!isRecord3(value) || depth > 20) return {};
|
|
1852
|
+
if (typeof value.$ref === "string") {
|
|
1853
|
+
return { $ref: `#/components/schemas/${escapeJsonPointer(value.$ref)}` };
|
|
1854
|
+
}
|
|
1855
|
+
const result = {};
|
|
1856
|
+
const type = stringValue2(value.type);
|
|
1857
|
+
if (type) result.type = type === "any" ? void 0 : type;
|
|
1858
|
+
for (const key of [
|
|
1859
|
+
"description",
|
|
1860
|
+
"format",
|
|
1861
|
+
"pattern",
|
|
1862
|
+
"minimum",
|
|
1863
|
+
"maximum",
|
|
1864
|
+
"default"
|
|
1865
|
+
]) {
|
|
1866
|
+
if (value[key] !== void 0) result[key] = value[key];
|
|
1867
|
+
}
|
|
1868
|
+
if (Array.isArray(value.enum)) result.enum = value.enum;
|
|
1869
|
+
if (isRecord3(value.properties)) {
|
|
1870
|
+
result.type = result.type ?? "object";
|
|
1871
|
+
result.properties = Object.fromEntries(
|
|
1872
|
+
Object.entries(value.properties).map(([name, schema]) => [
|
|
1873
|
+
name,
|
|
1874
|
+
convertGoogleSchema(schema, depth + 1)
|
|
1875
|
+
])
|
|
1876
|
+
);
|
|
1877
|
+
}
|
|
1878
|
+
if (value.items !== void 0) {
|
|
1879
|
+
result.type = result.type ?? "array";
|
|
1880
|
+
result.items = convertGoogleSchema(value.items, depth + 1);
|
|
1881
|
+
}
|
|
1882
|
+
if (value.additionalProperties !== void 0) {
|
|
1883
|
+
result.additionalProperties = value.additionalProperties === true ? true : convertGoogleSchema(value.additionalProperties, depth + 1);
|
|
1884
|
+
}
|
|
1885
|
+
if (Array.isArray(value.required)) result.required = value.required;
|
|
1886
|
+
return Object.fromEntries(Object.entries(result).filter(([, entry]) => entry !== void 0));
|
|
1887
|
+
}
|
|
1888
|
+
function escapeJsonPointer(value) {
|
|
1889
|
+
return value.replaceAll("~", "~0").replaceAll("/", "~1");
|
|
1890
|
+
}
|
|
1891
|
+
function stringValue2(value) {
|
|
1892
|
+
return typeof value === "string" && value.trim() ? value : void 0;
|
|
1893
|
+
}
|
|
1894
|
+
function isRecord3(value) {
|
|
1895
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
1896
|
+
}
|
|
1897
|
+
export {
|
|
1898
|
+
CORE_PROVIDER_PRESETS,
|
|
1899
|
+
DEFAULT_INTEGRATION_RESPONSE_BYTES,
|
|
1900
|
+
DEFAULT_INTEGRATION_TIMEOUT_MS,
|
|
1901
|
+
GOOGLE_DRIVE_PRESET,
|
|
1902
|
+
GOOGLE_GMAIL_PRESET,
|
|
1903
|
+
GraphqlMcpServer,
|
|
1904
|
+
IntegrationInvocationError,
|
|
1905
|
+
IntegrationProtocolError,
|
|
1906
|
+
MAX_INTEGRATION_SPEC_BYTES,
|
|
1907
|
+
MAX_INTEGRATION_TOOLS,
|
|
1908
|
+
MICROSOFT_GRAPH_BASE_URL,
|
|
1909
|
+
MICROSOFT_GRAPH_OPENAPI_URL,
|
|
1910
|
+
MICROSOFT_ONEDRIVE_PRESET,
|
|
1911
|
+
MICROSOFT_OUTLOOK_CALENDAR_PRESET,
|
|
1912
|
+
MICROSOFT_OUTLOOK_CONTACTS_PRESET,
|
|
1913
|
+
MICROSOFT_OUTLOOK_MAIL_PRESET,
|
|
1914
|
+
OpenApiMcpServer,
|
|
1915
|
+
applyCredentialPlacements,
|
|
1916
|
+
assertCredentialAudience,
|
|
1917
|
+
canonicalJson,
|
|
1918
|
+
compileGraphqlRevision,
|
|
1919
|
+
compileOpenApiRevision,
|
|
1920
|
+
createGraphqlMcpServer,
|
|
1921
|
+
createOpenApiMcpServer,
|
|
1922
|
+
createPinnedIntegrationTransport,
|
|
1923
|
+
deriveMcpNamespace,
|
|
1924
|
+
directIntegrationTransport,
|
|
1925
|
+
discoverOpenApiAuth,
|
|
1926
|
+
extractMcpToolManifest,
|
|
1927
|
+
fetchGraphqlIntrospection,
|
|
1928
|
+
fetchIntegrationSourceDocument,
|
|
1929
|
+
fetchWithDeadline,
|
|
1930
|
+
filterOpenApiDocumentForPreset,
|
|
1931
|
+
googleDiscoveryToOpenApi,
|
|
1932
|
+
immutableRevisionId,
|
|
1933
|
+
integrationFeaturesForPreset,
|
|
1934
|
+
invokeGraphqlOperation,
|
|
1935
|
+
invokeOpenApiOperation,
|
|
1936
|
+
parseOpenApiDocument,
|
|
1937
|
+
providerDomainForPreset,
|
|
1938
|
+
providerPresetById,
|
|
1939
|
+
readIntegrationResponse,
|
|
1940
|
+
sha256Hex,
|
|
1941
|
+
stableToolId,
|
|
1942
|
+
validateGraphqlSelection
|
|
1943
|
+
};
|
|
1944
|
+
//# sourceMappingURL=index.js.map
|