@every-app/sdk 0.3.0-rc.11 → 0.3.0-rc.13

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.
Files changed (80) hide show
  1. package/README.md +6 -4
  2. package/dist/cloudflare/worker.d.ts +1 -1
  3. package/dist/cloudflare/worker.d.ts.map +1 -1
  4. package/dist/cloudflare/worker.js +1 -2
  5. package/dist/internal/aiDefault.d.ts +32 -76
  6. package/dist/internal/aiDefault.d.ts.map +1 -1
  7. package/dist/internal/aiDefault.js +41 -144
  8. package/dist/internal/appSettings.d.ts +26 -19
  9. package/dist/internal/appSettings.d.ts.map +1 -1
  10. package/dist/internal/appSettings.js +24 -31
  11. package/dist/internal/capabilities.d.ts +21 -8
  12. package/dist/internal/capabilities.d.ts.map +1 -1
  13. package/dist/internal/capabilities.js +2 -2
  14. package/dist/internal/catalog/commerce.js +1 -1
  15. package/dist/internal/gatewaySettings.d.ts +6 -16
  16. package/dist/internal/gatewaySettings.d.ts.map +1 -1
  17. package/dist/internal/gatewaySettings.js +5 -23
  18. package/dist/internal/index.d.ts +1 -0
  19. package/dist/internal/index.d.ts.map +1 -1
  20. package/dist/internal/index.js +1 -0
  21. package/dist/internal/mcpOAuth.d.ts +210 -0
  22. package/dist/internal/mcpOAuth.d.ts.map +1 -0
  23. package/dist/internal/mcpOAuth.js +735 -0
  24. package/dist/internal/oauthBuiltIns.d.ts +1 -1
  25. package/dist/internal/oauthBuiltIns.js +1 -1
  26. package/dist/internal/oauthSpec.d.ts +1 -1
  27. package/dist/internal/oauthSpec.d.ts.map +1 -1
  28. package/dist/internal/oauthSpec.js +51 -1
  29. package/dist/internal/protocol.d.ts +5 -24
  30. package/dist/internal/protocol.d.ts.map +1 -1
  31. package/dist/internal/protocol.js +0 -19
  32. package/dist/internal/providerRegistry.d.ts +4 -3
  33. package/dist/internal/providerRegistry.d.ts.map +1 -1
  34. package/dist/internal/providerRegistry.js +11 -3
  35. package/dist/internal/remoteMcp.d.ts +162 -47
  36. package/dist/internal/remoteMcp.d.ts.map +1 -1
  37. package/dist/internal/remoteMcp.js +261 -124
  38. package/dist/internal/tools.d.ts +11 -7
  39. package/dist/internal/tools.d.ts.map +1 -1
  40. package/dist/internal/tools.js +8 -8
  41. package/dist/manifest.d.ts +9 -25
  42. package/dist/manifest.d.ts.map +1 -1
  43. package/dist/server/ai.d.ts +1 -93
  44. package/dist/server/ai.d.ts.map +1 -1
  45. package/dist/server/ai.js +17 -111
  46. package/dist/server/attribution.d.ts +1 -1
  47. package/dist/server/context.d.ts +12 -14
  48. package/dist/server/context.d.ts.map +1 -1
  49. package/dist/server/context.js +23 -25
  50. package/dist/server/everyApp.d.ts +4 -23
  51. package/dist/server/everyApp.d.ts.map +1 -1
  52. package/dist/server/everyApp.js +13 -46
  53. package/dist/server/gatewayFetch.d.ts +1 -1
  54. package/dist/server/gatewayFetch.js +1 -1
  55. package/dist/server/index.d.ts +3 -7
  56. package/dist/server/index.d.ts.map +1 -1
  57. package/dist/server/index.js +3 -7
  58. package/dist/server/integrationFetch.d.ts +1 -1
  59. package/dist/server/refusal.d.ts +0 -9
  60. package/dist/server/refusal.d.ts.map +1 -1
  61. package/dist/server/refusal.js +0 -22
  62. package/dist/server/tools.d.ts +11 -8
  63. package/dist/server/tools.d.ts.map +1 -1
  64. package/dist/server/tools.js +10 -4
  65. package/dist/server/toolsEndpoint.d.ts +2 -3
  66. package/dist/server/toolsEndpoint.d.ts.map +1 -1
  67. package/dist/server/toolsEndpoint.js +6 -11
  68. package/dist/testing/index.d.ts +2 -3
  69. package/dist/testing/index.d.ts.map +1 -1
  70. package/dist/testing/index.js +2 -6
  71. package/package.json +1 -1
  72. package/dist/server/aiBinding.d.ts +0 -68
  73. package/dist/server/aiBinding.d.ts.map +0 -1
  74. package/dist/server/aiBinding.js +0 -294
  75. package/dist/server/emailBinding.d.ts +0 -72
  76. package/dist/server/emailBinding.d.ts.map +0 -1
  77. package/dist/server/emailBinding.js +0 -124
  78. package/dist/server/fromMcpTools.d.ts +0 -89
  79. package/dist/server/fromMcpTools.d.ts.map +0 -1
  80. package/dist/server/fromMcpTools.js +0 -192
@@ -0,0 +1,735 @@
1
+ import { publicHostProblem } from "./providerRegistry.js";
2
+ import { parseProviderSpec, describeSpecProblems } from "./providerSpec.js";
3
+ import { remoteMcpClientIdSetting, remoteMcpClientSecretSetting, remoteMcpHeaderProblem, remoteMcpProviderId, remoteMcpTokenSetting, } from "./remoteMcp.js";
4
+ /** The largest answer a discovery or registration fetch reads: 64 KiB. */
5
+ export const MCP_OAUTH_MAX_BODY = 64 * 1024;
6
+ /** How long one discovery or registration fetch may take. */
7
+ export const MCP_OAUTH_TIMEOUT_MS = 10_000;
8
+ const TOKEN_CHAR = /[!#$%&'*+\-.^_`|~0-9A-Za-z]/;
9
+ /**
10
+ * The Bearer challenge of a `WWW-Authenticate` header, or null when the
11
+ * header carries none. Reads several challenges in one header
12
+ * (`Basic realm="x", Bearer resource_metadata="…"`), quoted and bare
13
+ * values, and parameter names in any case.
14
+ */
15
+ export function parseWwwAuthenticate(header) {
16
+ if (typeof header !== "string" || !header.trim())
17
+ return null;
18
+ const challenges = [];
19
+ let current = null;
20
+ let i = 0;
21
+ const n = header.length;
22
+ const skip = (chars) => {
23
+ while (i < n && chars.test(header[i]))
24
+ i++;
25
+ };
26
+ const token = () => {
27
+ const start = i;
28
+ while (i < n && TOKEN_CHAR.test(header[i]))
29
+ i++;
30
+ return header.slice(start, i);
31
+ };
32
+ while (i < n) {
33
+ skip(/[\s,]/);
34
+ if (i >= n)
35
+ break;
36
+ const name = token();
37
+ if (!name) {
38
+ // Something unreadable: skip to the next comma.
39
+ while (i < n && header[i] !== ",")
40
+ i++;
41
+ continue;
42
+ }
43
+ const afterName = i;
44
+ skip(/[ \t]/);
45
+ if (header[i] === "=" && header[i + 1] !== "=") {
46
+ i++;
47
+ skip(/[ \t]/);
48
+ let value = "";
49
+ if (header[i] === '"') {
50
+ i++;
51
+ while (i < n && header[i] !== '"') {
52
+ if (header[i] === "\\" && i + 1 < n)
53
+ i++;
54
+ value += header[i];
55
+ i++;
56
+ }
57
+ i++;
58
+ }
59
+ else {
60
+ const start = i;
61
+ while (i < n && header[i] !== "," && !/\s/.test(header[i]))
62
+ i++;
63
+ value = header.slice(start, i);
64
+ }
65
+ if (current)
66
+ current.params[name.toLowerCase()] = value;
67
+ continue;
68
+ }
69
+ // A scheme: a new challenge. (A token68 after another scheme reads
70
+ // as a scheme of its own; only the Bearer challenge is looked at.)
71
+ i = afterName;
72
+ current = { scheme: name.toLowerCase(), params: {} };
73
+ challenges.push(current);
74
+ }
75
+ const bearer = challenges.find((challenge) => challenge.scheme === "bearer");
76
+ if (!bearer)
77
+ return null;
78
+ const out = {};
79
+ if (bearer.params["resource_metadata"]) {
80
+ out.resourceMetadata = bearer.params["resource_metadata"];
81
+ }
82
+ if (bearer.params["scope"])
83
+ out.scope = bearer.params["scope"];
84
+ if (bearer.params["error"])
85
+ out.error = bearer.params["error"];
86
+ return out;
87
+ }
88
+ /** Why `url` can't be fetched by discovery, or null when it can. */
89
+ export function mcpOAuthUrlProblem(url, { query = true } = {}) {
90
+ let parsed;
91
+ try {
92
+ parsed = new URL(url);
93
+ }
94
+ catch {
95
+ return `${url} isn't a URL`;
96
+ }
97
+ if (parsed.protocol !== "https:")
98
+ return `${url} isn't https`;
99
+ if (parsed.username || parsed.password) {
100
+ return `${url} carries credentials`;
101
+ }
102
+ if (parsed.hash || url.includes("#"))
103
+ return `${url} has a fragment`;
104
+ if (parsed.port) {
105
+ return `${url} is on port ${parsed.port}; the Gateway's OAuth requests go to port 443 only`;
106
+ }
107
+ if (!query && parsed.search)
108
+ return `${url} carries a query string`;
109
+ const host = parsed.hostname.toLowerCase().replace(/\.$/, "");
110
+ if (host.startsWith("[") || host.includes(":")) {
111
+ return `${host} is an IP address, not a host name`;
112
+ }
113
+ const problem = publicHostProblem(host);
114
+ if (problem)
115
+ return problem;
116
+ if (!host.includes("."))
117
+ return `${host} is not a public host`;
118
+ return null;
119
+ }
120
+ async function readBounded(response) {
121
+ const declared = Number(response.headers.get("content-length") ?? "");
122
+ if (Number.isFinite(declared) && declared > MCP_OAUTH_MAX_BODY)
123
+ return null;
124
+ if (!response.body)
125
+ return "";
126
+ const reader = response.body.getReader();
127
+ const chunks = [];
128
+ let size = 0;
129
+ for (;;) {
130
+ const { done, value } = await reader.read();
131
+ if (done)
132
+ break;
133
+ size += value.byteLength;
134
+ if (size > MCP_OAUTH_MAX_BODY) {
135
+ await reader.cancel().catch(() => { });
136
+ return null;
137
+ }
138
+ chunks.push(value);
139
+ }
140
+ const bytes = new Uint8Array(size);
141
+ let offset = 0;
142
+ for (const chunk of chunks) {
143
+ bytes.set(chunk, offset);
144
+ offset += chunk.byteLength;
145
+ }
146
+ return new TextDecoder().decode(bytes);
147
+ }
148
+ /**
149
+ * One bounded fetch: a JSON object, `missing` for a 4xx or 5xx (the next
150
+ * candidate is tried), `failed` for anything that ends discovery (a
151
+ * redirect, an oversize or unreadable answer, a network error, a refused
152
+ * URL).
153
+ */
154
+ async function fetchJson(fetcher, url, init = {}) {
155
+ const problem = mcpOAuthUrlProblem(url);
156
+ if (problem)
157
+ return { kind: "failed", problem };
158
+ let response;
159
+ try {
160
+ response = await fetcher(url, {
161
+ ...init,
162
+ redirect: "manual",
163
+ signal: AbortSignal.timeout(MCP_OAUTH_TIMEOUT_MS),
164
+ headers: { accept: "application/json", ...(init.headers ?? {}) },
165
+ });
166
+ }
167
+ catch (error) {
168
+ const timeout = error instanceof Error &&
169
+ (error.name === "TimeoutError" || error.name === "AbortError");
170
+ return {
171
+ kind: "failed",
172
+ problem: timeout
173
+ ? `${url} didn't answer within ${MCP_OAUTH_TIMEOUT_MS / 1000} s`
174
+ : `${url} couldn't be fetched (${error instanceof Error ? error.message : String(error)})`,
175
+ };
176
+ }
177
+ if (response.type === "opaqueredirect" ||
178
+ (response.status >= 300 && response.status < 400)) {
179
+ return {
180
+ kind: "failed",
181
+ problem: `${url} answered with a redirect (${response.status || "3xx"}); discovery follows none`,
182
+ };
183
+ }
184
+ if (!response.ok) {
185
+ await response.body?.cancel().catch(() => { });
186
+ return { kind: "missing", status: response.status };
187
+ }
188
+ const text = await readBounded(response);
189
+ if (text === null) {
190
+ return {
191
+ kind: "failed",
192
+ problem: `${url} answered with more than ${MCP_OAUTH_MAX_BODY / 1024} KiB`,
193
+ };
194
+ }
195
+ let body;
196
+ try {
197
+ body = JSON.parse(text);
198
+ }
199
+ catch {
200
+ return { kind: "failed", problem: `${url} didn't answer with JSON` };
201
+ }
202
+ if (typeof body !== "object" || body === null || Array.isArray(body)) {
203
+ return {
204
+ kind: "failed",
205
+ problem: `${url} didn't answer with a JSON object`,
206
+ };
207
+ }
208
+ return { kind: "json", body: body };
209
+ }
210
+ function stringList(value) {
211
+ return Array.isArray(value)
212
+ ? value.filter((item) => typeof item === "string")
213
+ : [];
214
+ }
215
+ /** `/.well-known/<name>` inserted before `url`'s path (none for a bare origin). */
216
+ function pathInserted(url, name) {
217
+ const path = url.pathname === "/" ? "" : url.pathname.replace(/\/+$/, "");
218
+ return `${url.origin}/.well-known/${name}${path}`;
219
+ }
220
+ /** Whether the metadata's `resource` is `mcpUrl` or a path prefix of it on the same origin. */
221
+ function resourceCovers(resource, mcpUrl) {
222
+ let parsed;
223
+ try {
224
+ parsed = new URL(resource);
225
+ }
226
+ catch {
227
+ return false;
228
+ }
229
+ if (parsed.origin !== mcpUrl.origin || parsed.hash)
230
+ return false;
231
+ if (parsed.search && parsed.search !== mcpUrl.search)
232
+ return false;
233
+ const base = parsed.pathname;
234
+ const path = mcpUrl.pathname;
235
+ if (base === path || base === "/" || base === "")
236
+ return true;
237
+ const trimmed = base.replace(/\/+$/, "");
238
+ return (path === trimmed || path.startsWith(`${trimmed}/`) || path === `${trimmed}/`);
239
+ }
240
+ function sameIssuer(a, b) {
241
+ return a.replace(/\/+$/, "") === b.replace(/\/+$/, "");
242
+ }
243
+ /** The issuer's metadata URLs, in the MCP spec's order. */
244
+ function metadataUrls(issuer) {
245
+ const path = issuer.pathname === "/" ? "" : issuer.pathname.replace(/\/+$/, "");
246
+ if (!path) {
247
+ return [
248
+ `${issuer.origin}/.well-known/oauth-authorization-server`,
249
+ `${issuer.origin}/.well-known/openid-configuration`,
250
+ ];
251
+ }
252
+ return [
253
+ `${issuer.origin}/.well-known/oauth-authorization-server${path}`,
254
+ `${issuer.origin}/.well-known/openid-configuration${path}`,
255
+ `${issuer.origin}${path}/.well-known/openid-configuration`,
256
+ ];
257
+ }
258
+ function scopeList(value) {
259
+ const raw = typeof value === "string" ? value.split(/\s+/) : [...(value ?? [])];
260
+ const out = [];
261
+ for (const scope of raw) {
262
+ // What a provider spec's scope list takes: printable, no space or comma, bounded.
263
+ if (!scope ||
264
+ scope.length > 200 ||
265
+ /[\s,]/.test(scope) ||
266
+ /[\p{Cc}\p{Cf}]/u.test(scope) ||
267
+ out.includes(scope)) {
268
+ continue;
269
+ }
270
+ out.push(scope);
271
+ if (out.length === 40)
272
+ break;
273
+ }
274
+ return out;
275
+ }
276
+ async function authorizationServer(fetcher, issuer) {
277
+ const problem = mcpOAuthUrlProblem(issuer, { query: false });
278
+ if (problem) {
279
+ return { ok: false, problem: `the authorization server: ${problem}` };
280
+ }
281
+ const tried = [];
282
+ for (const url of metadataUrls(new URL(issuer))) {
283
+ tried.push(url);
284
+ const fetched = await fetchJson(fetcher, url);
285
+ if (fetched.kind === "failed")
286
+ return { ok: false, problem: fetched.problem };
287
+ if (fetched.kind === "missing")
288
+ continue;
289
+ const said = fetched.body["issuer"];
290
+ if (typeof said !== "string" || !sameIssuer(said, issuer)) {
291
+ return {
292
+ ok: false,
293
+ problem: `${url} names the issuer ${typeof said === "string" ? said : "(none)"}, not ${issuer}; the Gateway refuses metadata for another server`,
294
+ };
295
+ }
296
+ return { ok: true, metadata: fetched.body, issuer: said };
297
+ }
298
+ return {
299
+ ok: false,
300
+ problem: `no authorization server metadata for ${issuer} (tried ${tried.join(", ")})`,
301
+ };
302
+ }
303
+ function endpointOf(metadata, field, { required, query }) {
304
+ const value = metadata[field];
305
+ if (value === undefined || value === null || value === "") {
306
+ return required
307
+ ? {
308
+ ok: false,
309
+ problem: `the authorization server's metadata has no ${field}`,
310
+ }
311
+ : { ok: true };
312
+ }
313
+ if (typeof value !== "string" || value.length > 500) {
314
+ return {
315
+ ok: false,
316
+ problem: `the authorization server's ${field} isn't a URL`,
317
+ };
318
+ }
319
+ const problem = mcpOAuthUrlProblem(value, { query });
320
+ return problem
321
+ ? { ok: false, problem: `the authorization server's ${field}: ${problem}` }
322
+ : { ok: true, url: value };
323
+ }
324
+ /**
325
+ * Find how `url`'s MCP server takes OAuth: its protected resource, its
326
+ * authorization server, the endpoints, the scopes to ask for and the
327
+ * client authentication it accepts. Refuses (with the reason) anything
328
+ * the Gateway would not run: no PKCE S256, a resource that isn't this
329
+ * server, metadata naming another issuer, an endpoint off https or on a
330
+ * private host, a redirect anywhere.
331
+ */
332
+ export async function discoverMcpOAuth(input) {
333
+ const urlProblem = mcpOAuthUrlProblem(input.url);
334
+ if (urlProblem)
335
+ return { ok: false, problem: urlProblem };
336
+ const mcpUrl = new URL(input.url);
337
+ const candidates = input.resourceMetadataUrl
338
+ ? [input.resourceMetadataUrl]
339
+ : [
340
+ ...(mcpUrl.pathname === "/" || mcpUrl.pathname === ""
341
+ ? []
342
+ : [pathInserted(mcpUrl, "oauth-protected-resource")]),
343
+ `${mcpUrl.origin}/.well-known/oauth-protected-resource`,
344
+ ];
345
+ let resourceMetadata = null;
346
+ let resourceMetadataAt = "";
347
+ for (const candidate of candidates) {
348
+ const fetched = await fetchJson(input.fetch, candidate);
349
+ if (fetched.kind === "failed") {
350
+ return { ok: false, problem: fetched.problem };
351
+ }
352
+ if (fetched.kind === "missing")
353
+ continue;
354
+ resourceMetadata = fetched.body;
355
+ resourceMetadataAt = candidate;
356
+ break;
357
+ }
358
+ if (!resourceMetadata && input.resourceMetadataUrl) {
359
+ return {
360
+ ok: false,
361
+ problem: `the server named its resource metadata at ${input.resourceMetadataUrl}, which didn't answer`,
362
+ };
363
+ }
364
+ let issuer;
365
+ let resource;
366
+ let supported = [];
367
+ if (resourceMetadata) {
368
+ const said = resourceMetadata["resource"];
369
+ if (typeof said !== "string" || !resourceCovers(said, mcpUrl)) {
370
+ return {
371
+ ok: false,
372
+ problem: `${resourceMetadataAt} describes ${typeof said === "string" ? said : "no resource"}, not ${input.url}; the Gateway refuses metadata for another server`,
373
+ };
374
+ }
375
+ const servers = stringList(resourceMetadata["authorization_servers"]);
376
+ if (servers.length === 0) {
377
+ return {
378
+ ok: false,
379
+ problem: `${resourceMetadataAt} names no authorization server`,
380
+ };
381
+ }
382
+ issuer = servers[0];
383
+ resource = said;
384
+ supported = stringList(resourceMetadata["scopes_supported"]);
385
+ }
386
+ else {
387
+ // The 2025-03-26 spec: the authorization server is at the MCP origin.
388
+ issuer = mcpUrl.origin;
389
+ resource = input.url;
390
+ }
391
+ const server = await authorizationServer(input.fetch, issuer);
392
+ if (!server.ok) {
393
+ return {
394
+ ok: false,
395
+ problem: resourceMetadata
396
+ ? server.problem
397
+ : `${input.url} has no OAuth metadata: no protected resource metadata, and ${server.problem}`,
398
+ };
399
+ }
400
+ const metadata = server.metadata;
401
+ const methods = stringList(metadata["code_challenge_methods_supported"]);
402
+ if (!methods.includes("S256")) {
403
+ return {
404
+ ok: false,
405
+ problem: `${server.issuer} doesn't support PKCE with S256 (code_challenge_methods_supported), which the Gateway requires`,
406
+ };
407
+ }
408
+ const authorize = endpointOf(metadata, "authorization_endpoint", {
409
+ required: true,
410
+ query: true,
411
+ });
412
+ if (!authorize.ok)
413
+ return authorize;
414
+ const token = endpointOf(metadata, "token_endpoint", {
415
+ required: true,
416
+ query: false,
417
+ });
418
+ if (!token.ok)
419
+ return token;
420
+ const registration = endpointOf(metadata, "registration_endpoint", {
421
+ required: false,
422
+ query: false,
423
+ });
424
+ if (!registration.ok)
425
+ return registration;
426
+ const revocation = endpointOf(metadata, "revocation_endpoint", {
427
+ required: false,
428
+ query: false,
429
+ });
430
+ if (!revocation.ok)
431
+ return revocation;
432
+ const authMethods = stringList(metadata["token_endpoint_auth_methods_supported"]);
433
+ const scopes = input.scope ? scopeList(input.scope) : scopeList(supported);
434
+ return {
435
+ ok: true,
436
+ discovery: {
437
+ resource,
438
+ issuer: server.issuer,
439
+ authorizationEndpoint: authorize.url,
440
+ tokenEndpoint: token.url,
441
+ ...(registration.url ? { registrationEndpoint: registration.url } : {}),
442
+ ...(revocation.url ? { revocationEndpoint: revocation.url } : {}),
443
+ scopes,
444
+ ...(scopeList(supported).length > 0
445
+ ? { supportedScopes: scopeList(supported) }
446
+ : {}),
447
+ tokenEndpointAuthMethods: authMethods.length > 0 ? authMethods : ["client_secret_basic"],
448
+ resourceMetadata: resourceMetadata !== null,
449
+ ...(metadata["client_id_metadata_document_supported"] === true
450
+ ? { clientIdMetadataDocument: true }
451
+ : {}),
452
+ },
453
+ };
454
+ }
455
+ /** The token endpoint auth method to ask for: Basic when offered, else the form, else a public client. */
456
+ export function chooseTokenEndpointAuthMethod(supported) {
457
+ if (supported.includes("client_secret_basic"))
458
+ return "client_secret_basic";
459
+ if (supported.includes("client_secret_post"))
460
+ return "client_secret_post";
461
+ if (supported.includes("none"))
462
+ return "none";
463
+ return null;
464
+ }
465
+ /**
466
+ * Register a client with the server's authorization server (RFC 7591
467
+ * dynamic client registration): the Gateway's callback as the one
468
+ * redirect URI, the authorization-code and refresh grants, and the token
469
+ * endpoint auth method {@link chooseTokenEndpointAuthMethod} picks. Reads
470
+ * back what the server registered: its method decides `inject`.
471
+ */
472
+ export async function registerMcpClient(input) {
473
+ const endpoint = input.discovery.registrationEndpoint;
474
+ if (!endpoint) {
475
+ return {
476
+ ok: false,
477
+ unsupported: true,
478
+ problem: `${input.discovery.issuer} doesn't offer dynamic client registration; register a client with it and give its id`,
479
+ };
480
+ }
481
+ const method = chooseTokenEndpointAuthMethod(input.discovery.tokenEndpointAuthMethods);
482
+ if (!method) {
483
+ return {
484
+ ok: false,
485
+ problem: `${input.discovery.issuer} takes clients by ${input.discovery.tokenEndpointAuthMethods.join(", ")} only; the Gateway's clients use client_secret_basic, client_secret_post or none`,
486
+ };
487
+ }
488
+ const body = {
489
+ client_name: input.clientName.slice(0, 100),
490
+ redirect_uris: [input.redirectUri],
491
+ grant_types: ["authorization_code", "refresh_token"],
492
+ response_types: ["code"],
493
+ token_endpoint_auth_method: method,
494
+ ...(input.discovery.scopes.length > 0
495
+ ? { scope: input.discovery.scopes.join(" ") }
496
+ : {}),
497
+ };
498
+ const fetched = await fetchJson(input.fetch, endpoint, {
499
+ method: "POST",
500
+ headers: { "content-type": "application/json" },
501
+ body: JSON.stringify(body),
502
+ });
503
+ if (fetched.kind === "failed")
504
+ return { ok: false, problem: fetched.problem };
505
+ if (fetched.kind === "missing") {
506
+ return {
507
+ ok: false,
508
+ problem: `${endpoint} refused the registration (${fetched.status})`,
509
+ };
510
+ }
511
+ const answer = fetched.body;
512
+ const clientId = answer["client_id"];
513
+ if (typeof clientId !== "string" || !clientId || clientId.length > 500) {
514
+ return { ok: false, problem: `${endpoint} answered without a client_id` };
515
+ }
516
+ const said = answer["token_endpoint_auth_method"];
517
+ const authMethod = typeof said === "string" && said ? said : method;
518
+ if (!["client_secret_basic", "client_secret_post", "none"].includes(authMethod)) {
519
+ return {
520
+ ok: false,
521
+ problem: `${endpoint} registered the client for ${authMethod}, which the Gateway doesn't use`,
522
+ };
523
+ }
524
+ const secret = answer["client_secret"];
525
+ const clientSecret = typeof secret === "string" && secret.length > 0 ? secret : undefined;
526
+ if (authMethod !== "none" && !clientSecret) {
527
+ return {
528
+ ok: false,
529
+ problem: `${endpoint} registered a confidential client (${authMethod}) but returned no client_secret`,
530
+ };
531
+ }
532
+ return {
533
+ ok: true,
534
+ clientId,
535
+ ...(clientSecret === undefined ? {} : { clientSecret }),
536
+ inject: authMethod === "client_secret_basic" ? "basic" : "form",
537
+ authMethod,
538
+ };
539
+ }
540
+ /**
541
+ * The client metadata document the Gateway serves at `clientId` (CIMD):
542
+ * a public client (`token_endpoint_auth_method: none`, so PKCE is its
543
+ * proof) with the Gateway's callback for the app as its one redirect URI.
544
+ */
545
+ export function mcpClientMetadataDocument(input) {
546
+ return {
547
+ client_id: input.clientId,
548
+ client_name: input.clientName.slice(0, 100),
549
+ client_uri: input.clientUri,
550
+ redirect_uris: [input.redirectUri],
551
+ grant_types: ["authorization_code", "refresh_token"],
552
+ response_types: ["code"],
553
+ token_endpoint_auth_method: "none",
554
+ };
555
+ }
556
+ /**
557
+ * A client for the Gateway at the server: the one it registers (RFC 7591),
558
+ * else, when it registers none, the Gateway's client metadata document's
559
+ * URL (`documentUrl`, null for a Gateway not on https) where the server
560
+ * takes CIMD and public clients. `unsupported` when neither can be had: a
561
+ * client must be registered by hand and pasted.
562
+ */
563
+ export async function obtainMcpClient(input) {
564
+ const { discovery } = input;
565
+ if (discovery.registrationEndpoint || !discovery.clientIdMetadataDocument) {
566
+ const registered = await registerMcpClient(input);
567
+ return registered.ok ? { ...registered, via: "dcr" } : registered;
568
+ }
569
+ if (!input.documentUrl) {
570
+ return {
571
+ ok: false,
572
+ unsupported: true,
573
+ problem: `${discovery.issuer} takes clients by metadata document only, which the Gateway serves on https alone; register a client with it and give its id`,
574
+ };
575
+ }
576
+ if (!discovery.tokenEndpointAuthMethods.includes("none")) {
577
+ return {
578
+ ok: false,
579
+ unsupported: true,
580
+ problem: `${discovery.issuer} takes clients by metadata document but no public clients (token_endpoint_auth_methods_supported has no "none"); register a client with it and give its id`,
581
+ };
582
+ }
583
+ return {
584
+ ok: true,
585
+ clientId: input.documentUrl,
586
+ inject: "form",
587
+ authMethod: "none",
588
+ via: "cimd",
589
+ };
590
+ }
591
+ function specName(name, suffix) {
592
+ const room = 64 - suffix.length;
593
+ const trimmed = name.trim() || "MCP";
594
+ return `${trimmed.length > room ? trimmed.slice(0, room).trimEnd() : trimmed}${suffix}`;
595
+ }
596
+ function checked(id, spec) {
597
+ const parsed = parseProviderSpec(spec);
598
+ if (!parsed.ok) {
599
+ return { ok: false, problem: describeSpecProblems(parsed.problems) };
600
+ }
601
+ return { ok: true, id, spec };
602
+ }
603
+ function hostAndOrigin(url) {
604
+ let parsed;
605
+ try {
606
+ parsed = new URL(url);
607
+ }
608
+ catch {
609
+ return { ok: false, problem: `${url} isn't a URL` };
610
+ }
611
+ if (parsed.protocol !== "https:") {
612
+ return {
613
+ ok: false,
614
+ problem: `${url} isn't https: a credential goes to an https server only`,
615
+ };
616
+ }
617
+ return {
618
+ ok: true,
619
+ host: parsed.hostname.toLowerCase().replace(/\.$/, ""),
620
+ origin: parsed.origin,
621
+ };
622
+ }
623
+ /**
624
+ * The `oauth2-user` spec of an app's own OAuth row (`<appId>-mcp`): the
625
+ * MCP URL's host as its one API host (the token goes there and nowhere
626
+ * else), the discovered endpoints, the client in `<APPID>_MCP_CLIENT_ID`
627
+ * (and `_SECRET` for a confidential client), PKCE, the `resource` and
628
+ * `issuer`. The scopes the connect asks for are the app's (its manifest's
629
+ * entry, from discovery); the ones the server says it supports are
630
+ * tolerated on a token.
631
+ */
632
+ export function mcpOAuthProviderSpec(input) {
633
+ const where = hostAndOrigin(input.url);
634
+ if (!where.ok)
635
+ return where;
636
+ const id = remoteMcpProviderId(input.appId);
637
+ // Bounded as a spec's scope list is (40).
638
+ const tolerated = [
639
+ ...new Set([
640
+ ...input.discovery.scopes,
641
+ ...(input.discovery.supportedScopes ?? []),
642
+ ]),
643
+ ].slice(0, 40);
644
+ const spec = {
645
+ id,
646
+ name: specName(input.name || input.appId, " MCP server"),
647
+ docs: where.origin,
648
+ hosts: [where.host],
649
+ auth: {
650
+ kind: "oauth2-user",
651
+ authorizeUrl: input.discovery.authorizationEndpoint,
652
+ tokenUrl: input.discovery.tokenEndpoint,
653
+ ...(input.discovery.revocationEndpoint
654
+ ? { revokeUrl: input.discovery.revocationEndpoint }
655
+ : {}),
656
+ clientId: remoteMcpClientIdSetting(input.appId),
657
+ ...(input.client.hasSecret
658
+ ? { clientSecret: remoteMcpClientSecretSetting(input.appId) }
659
+ : {}),
660
+ inject: input.client.inject,
661
+ pkce: true,
662
+ scopeSeparator: "space",
663
+ resource: input.discovery.resource,
664
+ issuer: input.discovery.issuer,
665
+ ...(tolerated.length > 0 ? { tolerated } : {}),
666
+ },
667
+ tags: ["mcp"],
668
+ };
669
+ return checked(id, spec);
670
+ }
671
+ /**
672
+ * The API-key spec of an app's own static credential (`<appId>-mcp`): the
673
+ * MCP URL's host as its one host, the value in `<APPID>_MCP_TOKEN`, placed
674
+ * as `kind` says: `Authorization: Bearer` (or `<prefix>` as the scheme),
675
+ * a header (`header`, `prefix`), a query parameter (`param`), or HTTP
676
+ * Basic with the value as `user:password`.
677
+ */
678
+ export function mcpStaticProviderSpec(input) {
679
+ const where = hostAndOrigin(input.url);
680
+ if (!where.ok)
681
+ return where;
682
+ const id = remoteMcpProviderId(input.appId);
683
+ const secret = remoteMcpTokenSetting(input.appId);
684
+ let auth;
685
+ switch (input.kind) {
686
+ case "bearer": {
687
+ const scheme = (input.prefix ?? "").trim();
688
+ auth = {
689
+ kind: "bearer",
690
+ secret,
691
+ ...(scheme && scheme.toLowerCase() !== "bearer" ? { scheme } : {}),
692
+ };
693
+ break;
694
+ }
695
+ case "header": {
696
+ const problem = remoteMcpHeaderProblem(input.header);
697
+ if (problem)
698
+ return { ok: false, problem };
699
+ auth = {
700
+ kind: "header",
701
+ secret,
702
+ header: input.header,
703
+ ...(input.prefix ? { prefix: input.prefix } : {}),
704
+ };
705
+ break;
706
+ }
707
+ case "query": {
708
+ if (!input.param) {
709
+ return { ok: false, problem: "name the query parameter (--param)" };
710
+ }
711
+ auth = { kind: "query", secret, param: input.param };
712
+ break;
713
+ }
714
+ case "basic":
715
+ auth = { kind: "basic", secret, as: "pair" };
716
+ break;
717
+ default:
718
+ return {
719
+ ok: false,
720
+ problem: `${String(input.kind)} isn't a static credential kind`,
721
+ };
722
+ }
723
+ const name = input.name.trim() || input.appId;
724
+ const spec = {
725
+ id,
726
+ name: specName(name, " MCP server"),
727
+ docs: where.origin,
728
+ hosts: [where.host],
729
+ auth,
730
+ data: "per-call",
731
+ hint: `The credential ${name}'s MCP server takes${input.kind === "basic" ? ", as user:password" : ""}. The Gateway sends it to ${where.host} only.`.slice(0, 500),
732
+ tags: ["mcp"],
733
+ };
734
+ return checked(id, spec);
735
+ }