@softure-ai/mcp-access 0.1.7 → 0.1.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +16 -0
- package/README.md +39 -3
- package/dist/index.d.ts +9 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -2
- package/dist/index.js.map +1 -1
- package/dist/next/actions.d.ts.map +1 -1
- package/dist/next/actions.js +4 -2
- package/dist/next/actions.js.map +1 -1
- package/dist/next/context.d.ts +7 -1
- package/dist/next/context.d.ts.map +1 -1
- package/dist/next/context.js +12 -10
- package/dist/next/context.js.map +1 -1
- package/dist/next/index.d.ts +1 -0
- package/dist/next/index.d.ts.map +1 -1
- package/dist/next/index.js +1 -0
- package/dist/next/index.js.map +1 -1
- package/dist/next/oauth-page.d.ts.map +1 -1
- package/dist/next/oauth-page.js +3 -2
- package/dist/next/oauth-page.js.map +1 -1
- package/dist/next/oauth-routes.d.ts +5 -2
- package/dist/next/oauth-routes.d.ts.map +1 -1
- package/dist/next/oauth-routes.js +9 -6
- package/dist/next/oauth-routes.js.map +1 -1
- package/dist/options.d.ts +24 -0
- package/dist/options.d.ts.map +1 -1
- package/dist/options.js +66 -0
- package/dist/options.js.map +1 -1
- package/dist/origins.d.ts +27 -0
- package/dist/origins.d.ts.map +1 -0
- package/dist/origins.js +32 -0
- package/dist/origins.js.map +1 -0
- package/dist/server/context.d.ts +4 -0
- package/dist/server/context.d.ts.map +1 -0
- package/dist/server/context.js +13 -0
- package/dist/server/context.js.map +1 -0
- package/dist/server/endpoint.d.ts.map +1 -1
- package/dist/server/endpoint.js +5 -3
- package/dist/server/endpoint.js.map +1 -1
- package/dist/server/index.d.ts +5 -1
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +4 -1
- package/dist/server/index.js.map +1 -1
- package/dist/server/oauth-http.d.ts +30 -16
- package/dist/server/oauth-http.d.ts.map +1 -1
- package/dist/server/oauth-http.js +64 -41
- package/dist/server/oauth-http.js.map +1 -1
- package/dist/server/options.d.ts +3 -2
- package/dist/server/options.d.ts.map +1 -1
- package/dist/server/options.js +3 -3
- package/dist/server/options.js.map +1 -1
- package/dist/server/origins.d.ts +8 -0
- package/dist/server/origins.d.ts.map +1 -0
- package/dist/server/origins.js +15 -0
- package/dist/server/origins.js.map +1 -0
- package/module.json +1 -1
- package/package.json +1 -1
- package/src/index.ts +7 -2
- package/src/next/actions.ts +4 -2
- package/src/next/context.ts +15 -8
- package/src/next/index.ts +1 -0
- package/src/next/oauth-page.tsx +3 -2
- package/src/next/oauth-routes.ts +13 -5
- package/src/options.ts +75 -0
- package/src/origins.ts +48 -0
- package/src/server/context.ts +14 -0
- package/src/server/endpoint.ts +5 -3
- package/src/server/index.ts +6 -0
- package/src/server/oauth-http.ts +90 -41
- package/src/server/options.ts +4 -3
- package/src/server/origins.ts +19 -0
package/src/server/oauth-http.ts
CHANGED
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
// the Next adapter only supplies the context and the session.
|
|
6
6
|
import { errorLogLabel, getModule, type SoftureConfig } from "@softure-ai/core";
|
|
7
7
|
import { consumeRateLimit, identifyClient } from "@softure-ai/security/server";
|
|
8
|
+
import type { McpMetadataExtension } from "../options.js";
|
|
9
|
+
import { findServedResourceOrigin, type McpOriginRequest, type McpOrigins } from "../origins.js";
|
|
8
10
|
import type { OAuthClientRow } from "../schema.js";
|
|
9
11
|
import { MCP_READ_SCOPE, MCP_WRITE_SCOPE } from "./scopes.js";
|
|
10
12
|
import {
|
|
@@ -18,6 +20,7 @@ import {
|
|
|
18
20
|
} from "./oauth.js";
|
|
19
21
|
import { parseClientRegistration } from "./oauth-validation.js";
|
|
20
22
|
import { getMcpAccessOptions, getMcpAccessRoutes, getMcpEndpointUrl } from "./options.js";
|
|
23
|
+
import { resolveMcpOrigins } from "./origins.js";
|
|
21
24
|
import { isValidCodeChallenge, PKCE_METHOD } from "./pkce.js";
|
|
22
25
|
import type { McpAccessContext } from "./tokens.js";
|
|
23
26
|
|
|
@@ -48,8 +51,8 @@ const CORS_HEADERS = {
|
|
|
48
51
|
};
|
|
49
52
|
/** Responses that carry credentials must not be stored (RFC 6749 §5.1). */
|
|
50
53
|
const NO_STORE = { "cache-control": "no-store", pragma: "no-cache" };
|
|
51
|
-
/** Discovery documents change only with a deploy. */
|
|
52
|
-
const DISCOVERY_CACHE = { "cache-control": "public, max-age=300" };
|
|
54
|
+
/** Discovery documents change only with a deploy, and with the host they were asked on. */
|
|
55
|
+
const DISCOVERY_CACHE = { "cache-control": "public, max-age=300", vary: "host, x-forwarded-proto" };
|
|
53
56
|
|
|
54
57
|
function protocolJson(body: unknown, status = 200, extra: Readonly<Record<string, string>> = {}): Response {
|
|
55
58
|
return Response.json(body, { status, headers: { ...CORS_HEADERS, ...NO_STORE, ...extra } });
|
|
@@ -68,14 +71,17 @@ export function answerOAuthPreflight(): Response {
|
|
|
68
71
|
// ---------------------------------------------------------------------------------------------
|
|
69
72
|
// Discovery
|
|
70
73
|
|
|
71
|
-
/**
|
|
72
|
-
|
|
73
|
-
|
|
74
|
+
/**
|
|
75
|
+
* The issuer: the app's origin, without a path or a trailing slash. `origins` are the request's
|
|
76
|
+
* (`resolveMcpOrigins`); without them, the configured `appOrigin`.
|
|
77
|
+
*/
|
|
78
|
+
export function getOAuthIssuer(config: SoftureConfig, origins: McpOrigins = resolveMcpOrigins(config)): string {
|
|
79
|
+
return origins.appOrigin;
|
|
74
80
|
}
|
|
75
81
|
|
|
76
82
|
/** The URL the endpoint's `401` names in `resource_metadata` (RFC 9728 §5.1). */
|
|
77
|
-
export function getProtectedResourceMetadataUrl(config: SoftureConfig): string {
|
|
78
|
-
return `${getOAuthIssuer(config)}${PROTECTED_RESOURCE_METADATA_PATH}${new URL(getMcpEndpointUrl(config)).pathname}`;
|
|
83
|
+
export function getProtectedResourceMetadataUrl(config: SoftureConfig, origins: McpOrigins = resolveMcpOrigins(config)): string {
|
|
84
|
+
return `${getOAuthIssuer(config, origins)}${PROTECTED_RESOURCE_METADATA_PATH}${new URL(getMcpEndpointUrl(config, origins)).pathname}`;
|
|
79
85
|
}
|
|
80
86
|
|
|
81
87
|
/** Whether the app turned OAuth on; the OAuth routes answer 404 otherwise. */
|
|
@@ -83,11 +89,21 @@ export function isOAuthEnabled(config: SoftureConfig): boolean {
|
|
|
83
89
|
return getMcpAccessOptions(config).oauth.enabled;
|
|
84
90
|
}
|
|
85
91
|
|
|
86
|
-
/**
|
|
87
|
-
|
|
88
|
-
|
|
92
|
+
/** The app's extra keys under the generated ones (`oauth.metadata`). */
|
|
93
|
+
function readExtension(extension: McpMetadataExtension | undefined, origins: McpOrigins): Readonly<Record<string, unknown>> {
|
|
94
|
+
if (extension === undefined) return {};
|
|
95
|
+
return typeof extension === "function" ? extension(origins) : extension;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Authorization server metadata (RFC 8414 §2), with the app's `oauth.metadata.authorizationServer`
|
|
100
|
+
* keys added; generated keys win. Tokens are opaque: no OpenID.
|
|
101
|
+
*/
|
|
102
|
+
export function getAuthorizationServerMetadata(config: SoftureConfig, origins: McpOrigins = resolveMcpOrigins(config)) {
|
|
103
|
+
const issuer = getOAuthIssuer(config, origins);
|
|
89
104
|
const routes = getMcpAccessRoutes(config);
|
|
90
105
|
return {
|
|
106
|
+
...readExtension(getMcpAccessOptions(config).oauth.metadata.authorizationServer, origins),
|
|
91
107
|
issuer,
|
|
92
108
|
authorization_endpoint: `${issuer}${routes.oauthConsent}`,
|
|
93
109
|
token_endpoint: `${issuer}${routes.oauthToken}`,
|
|
@@ -104,35 +120,50 @@ export function getAuthorizationServerMetadata(config: SoftureConfig) {
|
|
|
104
120
|
}
|
|
105
121
|
|
|
106
122
|
/**
|
|
107
|
-
* Protected resource metadata (RFC 9728 §2)
|
|
108
|
-
*
|
|
109
|
-
*
|
|
123
|
+
* Protected resource metadata (RFC 9728 §2), with the app's `oauth.metadata.protectedResource`
|
|
124
|
+
* keys added; generated keys win, except the app's `resource_name`. At the endpoint's own path the
|
|
125
|
+
* resource is the endpoint URL; at the root it is the origin the document was asked on
|
|
126
|
+
* (`servedOrigin`, one of `resourceOrigins`) or else the app origin, because RFC 9728 §3.3 ties
|
|
127
|
+
* `resource` to the URL the metadata URL was built from.
|
|
110
128
|
*/
|
|
111
|
-
export function getProtectedResourceMetadata(
|
|
129
|
+
export function getProtectedResourceMetadata(
|
|
130
|
+
config: SoftureConfig,
|
|
131
|
+
variant: "endpoint" | "root",
|
|
132
|
+
origins: McpOrigins = resolveMcpOrigins(config),
|
|
133
|
+
servedOrigin: string | null = null,
|
|
134
|
+
) {
|
|
135
|
+
const issuer = getOAuthIssuer(config, origins);
|
|
112
136
|
return {
|
|
113
|
-
|
|
114
|
-
|
|
137
|
+
resource_name: getMcpAccessOptions(config).serverName,
|
|
138
|
+
...readExtension(getMcpAccessOptions(config).oauth.metadata.protectedResource, origins),
|
|
139
|
+
resource: variant === "endpoint" ? getMcpEndpointUrl(config, origins) : (servedOrigin ?? issuer),
|
|
140
|
+
authorization_servers: [issuer],
|
|
115
141
|
scopes_supported: [...OAUTH_SCOPES],
|
|
116
142
|
bearer_methods_supported: ["header"],
|
|
117
|
-
resource_name: getMcpAccessOptions(config).serverName,
|
|
118
143
|
};
|
|
119
144
|
}
|
|
120
145
|
|
|
121
|
-
/**
|
|
122
|
-
export
|
|
146
|
+
/** Builds a discovery document for the request's origins; the request is null outside a route. */
|
|
147
|
+
export type DiscoveryDocument = (config: SoftureConfig, origins: McpOrigins, request: McpOriginRequest | null) => unknown;
|
|
148
|
+
|
|
149
|
+
/** `GET` of a discovery document for `request` (its origins); 404 while OAuth is off. */
|
|
150
|
+
export function serveDiscoveryDocument(config: SoftureConfig, document: DiscoveryDocument, request: McpOriginRequest | null = null): Response {
|
|
123
151
|
if (!isOAuthEnabled(config)) return Response.json({ error: "not_found" }, { status: 404, headers: CORS_HEADERS });
|
|
124
|
-
return Response.json(document(config), { headers: { ...CORS_HEADERS, ...DISCOVERY_CACHE } });
|
|
152
|
+
return Response.json(document(config, resolveMcpOrigins(config, request), request), { headers: { ...CORS_HEADERS, ...DISCOVERY_CACHE } });
|
|
125
153
|
}
|
|
126
154
|
|
|
155
|
+
/** `serveDiscoveryDocument`'s builder for the root protected resource metadata, on the host it was asked on. */
|
|
156
|
+
export const getRootProtectedResourceMetadata: DiscoveryDocument = (config, origins, request) =>
|
|
157
|
+
getProtectedResourceMetadata(config, "root", origins, request === null ? null : findServedResourceOrigin(origins, request));
|
|
158
|
+
|
|
127
159
|
/**
|
|
128
160
|
* Whether `resource` (RFC 8707) names this server: absent (clients from before RFC 8707), the
|
|
129
|
-
* endpoint URL
|
|
161
|
+
* endpoint URL, the app origin or one of `resourceOrigins`, each with or without a trailing slash.
|
|
130
162
|
*/
|
|
131
|
-
export function isAcceptableResource(config: SoftureConfig, resource: string | null): boolean {
|
|
163
|
+
export function isAcceptableResource(config: SoftureConfig, resource: string | null, origins: McpOrigins = resolveMcpOrigins(config)): boolean {
|
|
132
164
|
if (resource === null || resource === "") return true;
|
|
133
|
-
const endpoint = getMcpEndpointUrl(config).replace(/\/+$/, "");
|
|
134
|
-
|
|
135
|
-
return [endpoint, `${endpoint}/`, origin, `${origin}/`].includes(resource);
|
|
165
|
+
const endpoint = getMcpEndpointUrl(config, origins).replace(/\/+$/, "");
|
|
166
|
+
return [endpoint, getOAuthIssuer(config, origins), ...origins.resourceOrigins].some((accepted) => resource === accepted || resource === `${accepted}/`);
|
|
136
167
|
}
|
|
137
168
|
|
|
138
169
|
/** Whether the client asks to write. Unknown scopes are ignored (RFC 6749 §3.3); every grant reads. */
|
|
@@ -263,6 +294,7 @@ const INVALID_GRANT = "The code or refresh token is invalid, expired, used or is
|
|
|
263
294
|
*/
|
|
264
295
|
export async function handleTokenRequest(ctx: McpAccessContext, request: Request): Promise<Response> {
|
|
265
296
|
if (!isOAuthEnabled(ctx.config)) return protocolError("not_found", "OAuth is not enabled.", 404);
|
|
297
|
+
const origins = resolveMcpOrigins(ctx.config, request);
|
|
266
298
|
const form = new URLSearchParams(await request.text().catch(() => ""));
|
|
267
299
|
const credentials = readClientCredentials(request, form);
|
|
268
300
|
const limited = await limitProtocolRequest(ctx, request, credentials.clientId);
|
|
@@ -273,15 +305,21 @@ export async function handleTokenRequest(ctx: McpAccessContext, request: Request
|
|
|
273
305
|
if (client === null || !isClientSecretValid(client, credentials.clientSecret === "" ? null : credentials.clientSecret)) {
|
|
274
306
|
return protocolError("invalid_client", "Unknown client or wrong client credentials.", 401, credentials.isBasic ? { "www-authenticate": 'Basic realm="mcp"' } : {});
|
|
275
307
|
}
|
|
276
|
-
return await answerGrant(ctx, form, client);
|
|
308
|
+
return await answerGrant(ctx, { form, client, origins });
|
|
277
309
|
} catch (error) {
|
|
278
310
|
console.error(`@softure-ai/mcp-access: the OAuth token endpoint failed: ${errorLogLabel(error)}`);
|
|
279
311
|
return protocolError("server_error", "The token could not be issued.", 500);
|
|
280
312
|
}
|
|
281
313
|
}
|
|
282
314
|
|
|
283
|
-
|
|
284
|
-
|
|
315
|
+
interface GrantRequest {
|
|
316
|
+
readonly form: URLSearchParams;
|
|
317
|
+
readonly client: OAuthClientRow;
|
|
318
|
+
readonly origins: McpOrigins;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
async function answerGrant(ctx: McpAccessContext, { form, client, origins }: GrantRequest): Promise<Response> {
|
|
322
|
+
if (!isAcceptableResource(ctx.config, form.get("resource"), origins)) {
|
|
285
323
|
return protocolError("invalid_target", "The resource parameter does not name this MCP server.");
|
|
286
324
|
}
|
|
287
325
|
const grantType = form.get("grant_type");
|
|
@@ -328,9 +366,14 @@ export type AuthorizationOutcome =
|
|
|
328
366
|
* The client's redirect URI with parameters added, keeping the query it registered (RFC 6749
|
|
329
367
|
* §3.1.2), and always `iss` (RFC 9207), so the client knows the answer came from this server.
|
|
330
368
|
*/
|
|
331
|
-
export function buildClientRedirect(
|
|
369
|
+
export function buildClientRedirect(
|
|
370
|
+
config: SoftureConfig,
|
|
371
|
+
redirectUri: string,
|
|
372
|
+
params: Readonly<Record<string, string | null>>,
|
|
373
|
+
origins: McpOrigins = resolveMcpOrigins(config),
|
|
374
|
+
): string {
|
|
332
375
|
const url = new URL(redirectUri);
|
|
333
|
-
for (const [key, value] of Object.entries({ ...params, iss: getOAuthIssuer(config) })) {
|
|
376
|
+
for (const [key, value] of Object.entries({ ...params, iss: getOAuthIssuer(config, origins) })) {
|
|
334
377
|
if (value !== null) url.searchParams.set(key, value);
|
|
335
378
|
}
|
|
336
379
|
return url.toString();
|
|
@@ -350,9 +393,14 @@ export function readAuthorizationParams(source: { get(name: string): unknown }):
|
|
|
350
393
|
* Validates an authorization request (RFC 6749 §4.1.1), for the consent page and the decision.
|
|
351
394
|
* Order is a security rule (RFC 6749 §4.1.2.1): until the client and its redirect URI are
|
|
352
395
|
* confirmed, an error is shown to the person, never redirected, or the server would redirect
|
|
353
|
-
* anyone anywhere. Only then do errors go back to the client.
|
|
396
|
+
* anyone anywhere. Only then do errors go back to the client. `origins` are the request's
|
|
397
|
+
* (the consent page's and the decision's must agree); without them, the configured ones.
|
|
354
398
|
*/
|
|
355
|
-
export async function validateAuthorizationRequest(
|
|
399
|
+
export async function validateAuthorizationRequest(
|
|
400
|
+
ctx: McpAccessContext,
|
|
401
|
+
params: URLSearchParams,
|
|
402
|
+
origins: McpOrigins = resolveMcpOrigins(ctx.config),
|
|
403
|
+
): Promise<AuthorizationOutcome> {
|
|
356
404
|
const clientId = params.get("client_id");
|
|
357
405
|
const client = clientId === null ? null : await findOAuthClient(ctx, clientId);
|
|
358
406
|
if (client === null) return { kind: "show-error", reason: "unknown_client" };
|
|
@@ -364,14 +412,14 @@ export async function validateAuthorizationRequest(ctx: McpAccessContext, params
|
|
|
364
412
|
const back = (error: string, description: string): AuthorizationOutcome => ({
|
|
365
413
|
kind: "redirect-error",
|
|
366
414
|
redirectUri,
|
|
367
|
-
location: buildClientRedirect(ctx.config, redirectUri, { error, error_description: description, state }),
|
|
415
|
+
location: buildClientRedirect(ctx.config, redirectUri, { error, error_description: description, state }, origins),
|
|
368
416
|
});
|
|
369
417
|
if (params.get("response_type") !== "code") return back("unsupported_response_type", "Only response_type=code is supported.");
|
|
370
418
|
const codeChallenge = params.get("code_challenge");
|
|
371
419
|
if (codeChallenge === null || !isValidCodeChallenge(codeChallenge) || params.get("code_challenge_method") !== PKCE_METHOD) {
|
|
372
420
|
return back("invalid_request", "PKCE is required: code_challenge with code_challenge_method S256.");
|
|
373
421
|
}
|
|
374
|
-
if (!isAcceptableResource(ctx.config, params.get("resource"))) return back("invalid_target", "The resource parameter does not name this MCP server.");
|
|
422
|
+
if (!isAcceptableResource(ctx.config, params.get("resource"), origins)) return back("invalid_target", "The resource parameter does not name this MCP server.");
|
|
375
423
|
return { kind: "valid", request: { client, redirectUri, state, codeChallenge, isWriteRequested: isWriteScopeRequested(params.get("scope")) } };
|
|
376
424
|
}
|
|
377
425
|
|
|
@@ -384,10 +432,10 @@ function seeOther(location: string): Response {
|
|
|
384
432
|
return new Response(null, { status: 303, headers: { location, "cache-control": "no-store" } });
|
|
385
433
|
}
|
|
386
434
|
|
|
387
|
-
/** Whether the form was posted from this app: `Origin` must be the
|
|
388
|
-
function isSameOrigin(
|
|
435
|
+
/** Whether the form was posted from this app: `Origin` must be the request's app origin. */
|
|
436
|
+
function isSameOrigin(request: Request, origins: McpOrigins): boolean {
|
|
389
437
|
const origin = request.headers.get("origin");
|
|
390
|
-
return origin !== null && origin ===
|
|
438
|
+
return origin !== null && origin === origins.appOrigin;
|
|
391
439
|
}
|
|
392
440
|
|
|
393
441
|
/**
|
|
@@ -399,7 +447,8 @@ function isSameOrigin(config: SoftureConfig, request: Request): boolean {
|
|
|
399
447
|
*/
|
|
400
448
|
export async function handleAuthorizationDecision(ctx: McpAccessContext, request: Request, userId: string | null): Promise<Response> {
|
|
401
449
|
if (!isOAuthEnabled(ctx.config)) return new Response(null, { status: 404 });
|
|
402
|
-
|
|
450
|
+
const origins = resolveMcpOrigins(ctx.config, request);
|
|
451
|
+
if (!isSameOrigin(request, origins)) return new Response(null, { status: 403, headers: { "cache-control": "no-store" } });
|
|
403
452
|
const form = await request.formData().catch(() => null);
|
|
404
453
|
if (form === null) return new Response(null, { status: 400, headers: { "cache-control": "no-store" } });
|
|
405
454
|
|
|
@@ -407,13 +456,13 @@ export async function handleAuthorizationDecision(ctx: McpAccessContext, request
|
|
|
407
456
|
const consent = getConsentPath(ctx.config, params);
|
|
408
457
|
if (userId === null) return seeOther(consent);
|
|
409
458
|
try {
|
|
410
|
-
const outcome = await validateAuthorizationRequest(ctx, params);
|
|
459
|
+
const outcome = await validateAuthorizationRequest(ctx, params, origins);
|
|
411
460
|
if (outcome.kind === "show-error") return seeOther(consent);
|
|
412
461
|
if (outcome.kind === "redirect-error") return seeOther(outcome.location);
|
|
413
462
|
|
|
414
463
|
const { client, redirectUri, state, codeChallenge, isWriteRequested } = outcome.request;
|
|
415
464
|
if (form.get("decision") !== "allow") {
|
|
416
|
-
return seeOther(buildClientRedirect(ctx.config, redirectUri, { error: "access_denied", error_description: "The person denied access.", state }));
|
|
465
|
+
return seeOther(buildClientRedirect(ctx.config, redirectUri, { error: "access_denied", error_description: "The person denied access.", state }, origins));
|
|
417
466
|
}
|
|
418
467
|
const code = await createAuthorizationCode(ctx, {
|
|
419
468
|
clientRowId: client.id,
|
|
@@ -423,7 +472,7 @@ export async function handleAuthorizationDecision(ctx: McpAccessContext, request
|
|
|
423
472
|
// Writes need the client's request, the app's allowWrites and the person's tick.
|
|
424
473
|
canWrite: isWriteRequested && getMcpAccessOptions(ctx.config).allowWrites && form.get("canWrite") !== null,
|
|
425
474
|
});
|
|
426
|
-
return seeOther(buildClientRedirect(ctx.config, redirectUri, { code, state }));
|
|
475
|
+
return seeOther(buildClientRedirect(ctx.config, redirectUri, { code, state }, origins));
|
|
427
476
|
} catch (error) {
|
|
428
477
|
console.error(`@softure-ai/mcp-access: recording an OAuth decision failed: ${errorLogLabel(error)}`);
|
|
429
478
|
return new Response(null, { status: 500, headers: { "cache-control": "no-store" } });
|
package/src/server/options.ts
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { getModule, type Locale, type SoftureConfig } from "@softure-ai/core";
|
|
3
3
|
import type { McpAccessMessages } from "../messages/index.js";
|
|
4
4
|
import type { McpAccessOptions } from "../options.js";
|
|
5
|
+
import type { McpOrigins } from "../origins.js";
|
|
5
6
|
|
|
6
7
|
const MODULE_ID = "mcp-access";
|
|
7
8
|
|
|
@@ -52,9 +53,9 @@ export function getMcpAccessRoutes(config: SoftureConfig): McpAccessRoutes {
|
|
|
52
53
|
};
|
|
53
54
|
}
|
|
54
55
|
|
|
55
|
-
/** The absolute URL MCP clients call: the app's origin and the endpoint route. */
|
|
56
|
-
export function getMcpEndpointUrl(config: SoftureConfig): string {
|
|
57
|
-
return new URL(getMcpAccessRoutes(config).endpoint, config.appOrigin).toString();
|
|
56
|
+
/** The absolute URL MCP clients call: the app's origin (the request's, when given) and the endpoint route. */
|
|
57
|
+
export function getMcpEndpointUrl(config: SoftureConfig, origins?: McpOrigins): string {
|
|
58
|
+
return new URL(getMcpAccessRoutes(config).endpoint, origins?.appOrigin ?? config.appOrigin).toString();
|
|
58
59
|
}
|
|
59
60
|
|
|
60
61
|
/** The module's copy in the app's locale, with the app's overrides applied. */
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
// The origins of one request, from the app's options: `resolveAppOrigin` when it answers, else the
|
|
2
|
+
// configured `appOrigin`, plus `resourceOrigins`.
|
|
3
|
+
import type { SoftureConfig } from "@softure-ai/core";
|
|
4
|
+
import { isBareOrigin } from "../options.js";
|
|
5
|
+
import type { McpOriginRequest, McpOrigins } from "../origins.js";
|
|
6
|
+
import { getMcpAccessOptions } from "./options.js";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* The origins for `request`, or the configured ones without a request. A resolver that returns
|
|
10
|
+
* something other than a bare http(s) origin is a setup bug: thrown by name, never served.
|
|
11
|
+
*/
|
|
12
|
+
export function resolveMcpOrigins(config: SoftureConfig, request?: McpOriginRequest | null): McpOrigins {
|
|
13
|
+
const options = getMcpAccessOptions(config);
|
|
14
|
+
const resolved = request === undefined || request === null || options.resolveAppOrigin === undefined ? null : options.resolveAppOrigin(request);
|
|
15
|
+
if (resolved !== null && !isBareOrigin(resolved)) {
|
|
16
|
+
throw new Error(`@softure-ai/mcp-access: resolveAppOrigin returned "${resolved.slice(0, 100)}", which is not an http(s) origin without a path`);
|
|
17
|
+
}
|
|
18
|
+
return { appOrigin: new URL(resolved ?? config.appOrigin).origin, resourceOrigins: options.resourceOrigins };
|
|
19
|
+
}
|