skybridge 0.0.0-dev.c8ed98d → 0.0.0-dev.c8fa6b8
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/README.md +1 -9
- package/dist/cli/build-helpers.d.ts +3 -0
- package/dist/cli/build-helpers.js +15 -4
- package/dist/cli/build-helpers.js.map +1 -1
- package/dist/cli/build-helpers.test.js +26 -1
- package/dist/cli/build-helpers.test.js.map +1 -1
- package/dist/cli/build-steps.d.ts +2 -0
- package/dist/cli/build-steps.js +68 -0
- package/dist/cli/build-steps.js.map +1 -0
- package/dist/cli/build-steps.test.d.ts +1 -0
- package/dist/cli/build-steps.test.js +52 -0
- package/dist/cli/build-steps.test.js.map +1 -0
- package/dist/cli/header.d.ts +1 -1
- package/dist/cli/run-plain.d.ts +18 -0
- package/dist/cli/run-plain.js +89 -0
- package/dist/cli/run-plain.js.map +1 -0
- package/dist/cli/use-execute-steps.d.ts +1 -1
- package/dist/cli/use-nodemon.d.ts +14 -0
- package/dist/cli/use-nodemon.js +71 -60
- package/dist/cli/use-nodemon.js.map +1 -1
- package/dist/cli/use-typescript-check.d.ts +8 -2
- package/dist/cli/use-typescript-check.js +70 -67
- package/dist/cli/use-typescript-check.js.map +1 -1
- package/dist/commands/build.d.ts +0 -2
- package/dist/commands/build.js +2 -61
- package/dist/commands/build.js.map +1 -1
- package/dist/commands/dev.d.ts +1 -0
- package/dist/commands/dev.js +32 -0
- package/dist/commands/dev.js.map +1 -1
- package/dist/server/auth/discovery.js +13 -1
- package/dist/server/auth/discovery.js.map +1 -1
- package/dist/server/auth/discovery.test.js +22 -0
- package/dist/server/auth/discovery.test.js.map +1 -1
- package/dist/server/auth/providers/auth0.d.ts +18 -0
- package/dist/server/auth/providers/auth0.js +31 -0
- package/dist/server/auth/providers/auth0.js.map +1 -0
- package/dist/server/auth/providers/auth0.test.d.ts +1 -0
- package/dist/server/auth/providers/auth0.test.js +48 -0
- package/dist/server/auth/providers/auth0.test.js.map +1 -0
- package/dist/server/auth/providers/clerk.d.ts +14 -0
- package/dist/server/auth/providers/clerk.js +16 -0
- package/dist/server/auth/providers/clerk.js.map +1 -0
- package/dist/server/auth/providers/clerk.test.d.ts +1 -0
- package/dist/server/auth/providers/clerk.test.js +28 -0
- package/dist/server/auth/providers/clerk.test.js.map +1 -0
- package/dist/server/auth/providers/custom.d.ts +16 -4
- package/dist/server/auth/providers/custom.js +18 -9
- package/dist/server/auth/providers/custom.js.map +1 -1
- package/dist/server/auth/providers/custom.test.js +20 -4
- package/dist/server/auth/providers/custom.test.js.map +1 -1
- package/dist/server/auth/providers/descope.d.ts +15 -0
- package/dist/server/auth/providers/descope.js +33 -0
- package/dist/server/auth/providers/descope.js.map +1 -0
- package/dist/server/auth/providers/descope.test.d.ts +1 -0
- package/dist/server/auth/providers/descope.test.js +37 -0
- package/dist/server/auth/providers/descope.test.js.map +1 -0
- package/dist/server/auth/providers/shared.d.ts +2 -0
- package/dist/server/auth/providers/shared.js +6 -0
- package/dist/server/auth/providers/shared.js.map +1 -0
- package/dist/server/auth/providers/shared.test.d.ts +1 -0
- package/dist/server/auth/providers/shared.test.js +10 -0
- package/dist/server/auth/providers/shared.test.js.map +1 -0
- package/dist/server/auth/providers/stytch.d.ts +12 -0
- package/dist/server/auth/providers/stytch.js +13 -0
- package/dist/server/auth/providers/stytch.js.map +1 -0
- package/dist/server/auth/providers/workos.d.ts +11 -0
- package/dist/server/auth/providers/workos.js +12 -0
- package/dist/server/auth/providers/workos.js.map +1 -0
- package/dist/server/auth/setup.js +2 -2
- package/dist/server/auth/setup.js.map +1 -1
- package/dist/server/auth/verify.d.ts +3 -2
- package/dist/server/auth/verify.js +2 -1
- package/dist/server/auth/verify.js.map +1 -1
- package/dist/server/auth/verify.test.js +13 -0
- package/dist/server/auth/verify.test.js.map +1 -1
- package/dist/server/index.d.ts +5 -0
- package/dist/server/index.js +5 -0
- package/dist/server/index.js.map +1 -1
- package/dist/server/server.d.ts +10 -1
- package/dist/server/server.js +47 -14
- package/dist/server/server.js.map +1 -1
- package/dist/server/view-name.test-d.d.ts +1 -0
- package/dist/server/view-name.test-d.js +8 -0
- package/dist/server/view-name.test-d.js.map +1 -0
- package/dist/server/view-resource-resolution.test.js +84 -1
- package/dist/server/view-resource-resolution.test.js.map +1 -1
- package/dist/test/view.test.js +20 -10
- package/dist/test/view.test.js.map +1 -1
- package/dist/web/bridges/adaptor.d.ts +4 -4
- package/dist/web/bridges/adaptor.js +29 -28
- package/dist/web/bridges/adaptor.js.map +1 -1
- package/dist/web/bridges/adaptor.test.js +0 -8
- package/dist/web/bridges/adaptor.test.js.map +1 -1
- package/dist/web/bridges/apps-sdk/bridge.d.ts +5 -1
- package/dist/web/bridges/apps-sdk/bridge.js +12 -0
- package/dist/web/bridges/apps-sdk/bridge.js.map +1 -1
- package/dist/web/bridges/get-adaptor.d.ts +0 -2
- package/dist/web/bridges/get-adaptor.js +1 -13
- package/dist/web/bridges/get-adaptor.js.map +1 -1
- package/dist/web/bridges/get-adaptor.test.js +3 -3
- package/dist/web/bridges/get-adaptor.test.js.map +1 -1
- package/dist/web/bridges/mcp-app/bridge.d.ts +12 -1
- package/dist/web/bridges/mcp-app/bridge.js +45 -0
- package/dist/web/bridges/mcp-app/bridge.js.map +1 -1
- package/dist/web/bridges/types.d.ts +0 -5
- package/dist/web/bridges/types.js.map +1 -1
- package/dist/web/components/modal-provider.d.ts +1 -1
- package/dist/web/create-store.test.js +4 -3
- package/dist/web/create-store.test.js.map +1 -1
- package/dist/web/data-llm.d.ts +1 -1
- package/dist/web/data-llm.test.js +4 -3
- package/dist/web/data-llm.test.js.map +1 -1
- package/dist/web/hooks/use-call-tool.test.js +6 -5
- package/dist/web/hooks/use-call-tool.test.js.map +1 -1
- package/dist/web/hooks/use-display-mode.test.js +4 -3
- package/dist/web/hooks/use-display-mode.test.js.map +1 -1
- package/dist/web/hooks/use-download.test.js +5 -5
- package/dist/web/hooks/use-download.test.js.map +1 -1
- package/dist/web/hooks/use-files.test.js +3 -3
- package/dist/web/hooks/use-files.test.js.map +1 -1
- package/dist/web/hooks/use-layout.test.js +5 -5
- package/dist/web/hooks/use-layout.test.js.map +1 -1
- package/dist/web/hooks/use-open-external.test.js +7 -11
- package/dist/web/hooks/use-open-external.test.js.map +1 -1
- package/dist/web/hooks/use-request-close.test.js +5 -4
- package/dist/web/hooks/use-request-close.test.js.map +1 -1
- package/dist/web/hooks/use-request-modal.test.js +3 -3
- package/dist/web/hooks/use-request-modal.test.js.map +1 -1
- package/dist/web/hooks/use-request-size.test.js +3 -3
- package/dist/web/hooks/use-request-size.test.js.map +1 -1
- package/dist/web/hooks/use-set-open-in-app-url.test.js +2 -2
- package/dist/web/hooks/use-set-open-in-app-url.test.js.map +1 -1
- package/dist/web/hooks/use-tool-info.test.js +3 -3
- package/dist/web/hooks/use-tool-info.test.js.map +1 -1
- package/dist/web/hooks/use-user.test.js +5 -5
- package/dist/web/hooks/use-user.test.js.map +1 -1
- package/dist/web/hooks/use-view-state.test.js +3 -3
- package/dist/web/hooks/use-view-state.test.js.map +1 -1
- package/package.json +20 -20
- package/dist/web/bridges/host-context-stores.d.ts +0 -27
- package/dist/web/bridges/host-context-stores.js +0 -73
- package/dist/web/bridges/host-context-stores.js.map +0 -1
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { OAuthConfig } from "../index.js";
|
|
2
|
+
import { type CustomProviderOptions } from "./custom.js";
|
|
3
|
+
/**
|
|
4
|
+
* OAuth provider for a Descope MCP Server. `url` is the MCP Server's **Discovery
|
|
5
|
+
* URL** (a.k.a. Issuer) from the console's Connection Information, e.g.
|
|
6
|
+
* `https://api.descope.com/v1/apps/agentic/<projectId>/<mcpServerId>` (or your
|
|
7
|
+
* custom domain). Requires DCR enabled on the MCP Server. For Descope with DCR
|
|
8
|
+
* disabled and Alpic's DCR proxy, use {@link customProvider} with `serverUrl`
|
|
9
|
+
* instead (see `examples/auth-descope-alpic`). The token `audience` defaults to
|
|
10
|
+
* the **Project ID** derived from the URL — Descope binds `aud` to [DCR client
|
|
11
|
+
* id, project id], not the server URL; pass `audience` to override.
|
|
12
|
+
*/
|
|
13
|
+
export declare function descopeProvider(opts: {
|
|
14
|
+
url: string;
|
|
15
|
+
} & Omit<CustomProviderOptions, "issuer">): Promise<OAuthConfig>;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { customProvider } from "./custom.js";
|
|
2
|
+
/**
|
|
3
|
+
* Derives the Descope Project ID from an MCP Server URL
|
|
4
|
+
* (`…/agentic/<projectId>/<mcpServerId>`). Descope binds the token `aud` to the
|
|
5
|
+
* project id, so it doubles as the audience.
|
|
6
|
+
*/
|
|
7
|
+
function projectIdFromUrl(url) {
|
|
8
|
+
const projectId = url.match(/\/agentic\/([^/]+)\/[^/]+/)?.[1];
|
|
9
|
+
if (!projectId) {
|
|
10
|
+
throw new Error(`Could not derive the Descope project id from "${url}"; pass an explicit \`audience\`.`);
|
|
11
|
+
}
|
|
12
|
+
return projectId;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* OAuth provider for a Descope MCP Server. `url` is the MCP Server's **Discovery
|
|
16
|
+
* URL** (a.k.a. Issuer) from the console's Connection Information, e.g.
|
|
17
|
+
* `https://api.descope.com/v1/apps/agentic/<projectId>/<mcpServerId>` (or your
|
|
18
|
+
* custom domain). Requires DCR enabled on the MCP Server. For Descope with DCR
|
|
19
|
+
* disabled and Alpic's DCR proxy, use {@link customProvider} with `serverUrl`
|
|
20
|
+
* instead (see `examples/auth-descope-alpic`). The token `audience` defaults to
|
|
21
|
+
* the **Project ID** derived from the URL — Descope binds `aud` to [DCR client
|
|
22
|
+
* id, project id], not the server URL; pass `audience` to override.
|
|
23
|
+
*/
|
|
24
|
+
export function descopeProvider(opts) {
|
|
25
|
+
const { url, audience, ...rest } = opts;
|
|
26
|
+
const issuer = url.replace(/\/\.well-known\/[^?#]*$/, "").replace(/\/$/, "");
|
|
27
|
+
return customProvider({
|
|
28
|
+
issuer,
|
|
29
|
+
audience: audience ?? projectIdFromUrl(issuer),
|
|
30
|
+
...rest,
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
//# sourceMappingURL=descope.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"descope.js","sourceRoot":"","sources":["../../../../src/server/auth/providers/descope.ts"],"names":[],"mappings":"AACA,OAAO,EAA8B,cAAc,EAAE,MAAM,aAAa,CAAC;AAEzE;;;;GAIG;AACH,SAAS,gBAAgB,CAAC,GAAW;IACnC,MAAM,SAAS,GAAG,GAAG,CAAC,KAAK,CAAC,2BAA2B,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;IAC9D,IAAI,CAAC,SAAS,EAAE,CAAC;QACf,MAAM,IAAI,KAAK,CACb,iDAAiD,GAAG,mCAAmC,CACxF,CAAC;IACJ,CAAC;IACD,OAAO,SAAS,CAAC;AACnB,CAAC;AAED;;;;;;;;;GASG;AACH,MAAM,UAAU,eAAe,CAC7B,IAA6D;IAE7D,MAAM,EAAE,GAAG,EAAE,QAAQ,EAAE,GAAG,IAAI,EAAE,GAAG,IAAI,CAAC;IACxC,MAAM,MAAM,GAAG,GAAG,CAAC,OAAO,CAAC,yBAAyB,EAAE,EAAE,CAAC,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;IAC7E,OAAO,cAAc,CAAC;QACpB,MAAM;QACN,QAAQ,EAAE,QAAQ,IAAI,gBAAgB,CAAC,MAAM,CAAC;QAC9C,GAAG,IAAI;KACR,CAAC,CAAC;AACL,CAAC","sourcesContent":["import type { OAuthConfig } from \"../index.js\";\nimport { type CustomProviderOptions, customProvider } from \"./custom.js\";\n\n/**\n * Derives the Descope Project ID from an MCP Server URL\n * (`…/agentic/<projectId>/<mcpServerId>`). Descope binds the token `aud` to the\n * project id, so it doubles as the audience.\n */\nfunction projectIdFromUrl(url: string): string {\n const projectId = url.match(/\\/agentic\\/([^/]+)\\/[^/]+/)?.[1];\n if (!projectId) {\n throw new Error(\n `Could not derive the Descope project id from \"${url}\"; pass an explicit \\`audience\\`.`,\n );\n }\n return projectId;\n}\n\n/**\n * OAuth provider for a Descope MCP Server. `url` is the MCP Server's **Discovery\n * URL** (a.k.a. Issuer) from the console's Connection Information, e.g.\n * `https://api.descope.com/v1/apps/agentic/<projectId>/<mcpServerId>` (or your\n * custom domain). Requires DCR enabled on the MCP Server. For Descope with DCR\n * disabled and Alpic's DCR proxy, use {@link customProvider} with `serverUrl`\n * instead (see `examples/auth-descope-alpic`). The token `audience` defaults to\n * the **Project ID** derived from the URL — Descope binds `aud` to [DCR client\n * id, project id], not the server URL; pass `audience` to override.\n */\nexport function descopeProvider(\n opts: { url: string } & Omit<CustomProviderOptions, \"issuer\">,\n): Promise<OAuthConfig> {\n const { url, audience, ...rest } = opts;\n const issuer = url.replace(/\\/\\.well-known\\/[^?#]*$/, \"\").replace(/\\/$/, \"\");\n return customProvider({\n issuer,\n audience: audience ?? projectIdFromUrl(issuer),\n ...rest,\n });\n}\n"]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
// @vitest-environment node
|
|
2
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
3
|
+
import { descopeProvider } from "./descope.js";
|
|
4
|
+
afterEach(() => vi.restoreAllMocks());
|
|
5
|
+
function discoveryDoc(issuer) {
|
|
6
|
+
return {
|
|
7
|
+
issuer,
|
|
8
|
+
authorization_endpoint: `${issuer}/authorize`,
|
|
9
|
+
token_endpoint: `${issuer}/token`,
|
|
10
|
+
registration_endpoint: `${issuer}/register`,
|
|
11
|
+
response_types_supported: ["code"],
|
|
12
|
+
scopes_supported: ["openid"],
|
|
13
|
+
jwks_uri: `${issuer}/.well-known/jwks.json`,
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
function mockDiscovery(issuer) {
|
|
17
|
+
return vi.spyOn(globalThis, "fetch").mockResolvedValue(new Response(JSON.stringify(discoveryDoc(issuer)), {
|
|
18
|
+
headers: { "content-type": "application/json" },
|
|
19
|
+
}));
|
|
20
|
+
}
|
|
21
|
+
describe("descopeProvider", () => {
|
|
22
|
+
it("derives issuer and audience (project id) from the MCP Server URL", async () => {
|
|
23
|
+
const issuer = "https://api.descope.com/v1/apps/agentic/P123/MS456";
|
|
24
|
+
const fetchSpy = mockDiscovery(issuer);
|
|
25
|
+
const config = await descopeProvider({ url: issuer });
|
|
26
|
+
expect(fetchSpy).toHaveBeenCalledWith(`${issuer}/.well-known/openid-configuration`, expect.anything());
|
|
27
|
+
expect(config.verify.issuer).toBe(issuer);
|
|
28
|
+
expect(config.verify.audience).toBe("P123");
|
|
29
|
+
});
|
|
30
|
+
it("lets an explicit audience override the derived project id", async () => {
|
|
31
|
+
const issuer = "https://api.descope.com/v1/apps/agentic/P123/MS456";
|
|
32
|
+
mockDiscovery(issuer);
|
|
33
|
+
const config = await descopeProvider({ url: issuer, audience: "custom" });
|
|
34
|
+
expect(config.verify.audience).toBe("custom");
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
//# sourceMappingURL=descope.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"descope.test.js","sourceRoot":"","sources":["../../../../src/server/auth/providers/descope.test.ts"],"names":[],"mappings":"AAAA,2BAA2B;AAC3B,OAAO,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,EAAE,EAAE,EAAE,MAAM,QAAQ,CAAC;AAC7D,OAAO,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AAE/C,SAAS,CAAC,GAAG,EAAE,CAAC,EAAE,CAAC,eAAe,EAAE,CAAC,CAAC;AAEtC,SAAS,YAAY,CAAC,MAAc;IAClC,OAAO;QACL,MAAM;QACN,sBAAsB,EAAE,GAAG,MAAM,YAAY;QAC7C,cAAc,EAAE,GAAG,MAAM,QAAQ;QACjC,qBAAqB,EAAE,GAAG,MAAM,WAAW;QAC3C,wBAAwB,EAAE,CAAC,MAAM,CAAC;QAClC,gBAAgB,EAAE,CAAC,QAAQ,CAAC;QAC5B,QAAQ,EAAE,GAAG,MAAM,wBAAwB;KAC5C,CAAC;AACJ,CAAC;AAED,SAAS,aAAa,CAAC,MAAc;IACnC,OAAO,EAAE,CAAC,KAAK,CAAC,UAAU,EAAE,OAAO,CAAC,CAAC,iBAAiB,CACpD,IAAI,QAAQ,CAAC,IAAI,CAAC,SAAS,CAAC,YAAY,CAAC,MAAM,CAAC,CAAC,EAAE;QACjD,OAAO,EAAE,EAAE,cAAc,EAAE,kBAAkB,EAAE;KAChD,CAAC,CACH,CAAC;AACJ,CAAC;AAED,QAAQ,CAAC,iBAAiB,EAAE,GAAG,EAAE;IAC/B,EAAE,CAAC,kEAAkE,EAAE,KAAK,IAAI,EAAE;QAChF,MAAM,MAAM,GAAG,oDAAoD,CAAC;QACpE,MAAM,QAAQ,GAAG,aAAa,CAAC,MAAM,CAAC,CAAC;QAEvC,MAAM,MAAM,GAAG,MAAM,eAAe,CAAC,EAAE,GAAG,EAAE,MAAM,EAAE,CAAC,CAAC;QAEtD,MAAM,CAAC,QAAQ,CAAC,CAAC,oBAAoB,CACnC,GAAG,MAAM,mCAAmC,EAC5C,MAAM,CAAC,QAAQ,EAAE,CAClB,CAAC;QACF,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;QAC1C,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;IAC9C,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,2DAA2D,EAAE,KAAK,IAAI,EAAE;QACzE,MAAM,MAAM,GAAG,oDAAoD,CAAC;QACpE,aAAa,CAAC,MAAM,CAAC,CAAC;QAEtB,MAAM,MAAM,GAAG,MAAM,eAAe,CAAC,EAAE,GAAG,EAAE,MAAM,EAAE,QAAQ,EAAE,QAAQ,EAAE,CAAC,CAAC;QAE1E,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;IAChD,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC","sourcesContent":["// @vitest-environment node\nimport { afterEach, describe, expect, it, vi } from \"vitest\";\nimport { descopeProvider } from \"./descope.js\";\n\nafterEach(() => vi.restoreAllMocks());\n\nfunction discoveryDoc(issuer: string) {\n return {\n issuer,\n authorization_endpoint: `${issuer}/authorize`,\n token_endpoint: `${issuer}/token`,\n registration_endpoint: `${issuer}/register`,\n response_types_supported: [\"code\"],\n scopes_supported: [\"openid\"],\n jwks_uri: `${issuer}/.well-known/jwks.json`,\n };\n}\n\nfunction mockDiscovery(issuer: string) {\n return vi.spyOn(globalThis, \"fetch\").mockResolvedValue(\n new Response(JSON.stringify(discoveryDoc(issuer)), {\n headers: { \"content-type\": \"application/json\" },\n }),\n );\n}\n\ndescribe(\"descopeProvider\", () => {\n it(\"derives issuer and audience (project id) from the MCP Server URL\", async () => {\n const issuer = \"https://api.descope.com/v1/apps/agentic/P123/MS456\";\n const fetchSpy = mockDiscovery(issuer);\n\n const config = await descopeProvider({ url: issuer });\n\n expect(fetchSpy).toHaveBeenCalledWith(\n `${issuer}/.well-known/openid-configuration`,\n expect.anything(),\n );\n expect(config.verify.issuer).toBe(issuer);\n expect(config.verify.audience).toBe(\"P123\");\n });\n\n it(\"lets an explicit audience override the derived project id\", async () => {\n const issuer = \"https://api.descope.com/v1/apps/agentic/P123/MS456\";\n mockDiscovery(issuer);\n\n const config = await descopeProvider({ url: issuer, audience: \"custom\" });\n\n expect(config.verify.audience).toBe(\"custom\");\n });\n});\n"]}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/** Normalises a bare host to an https issuer URL and strips a trailing slash. */
|
|
2
|
+
export function toIssuerUrl(domain) {
|
|
3
|
+
const withScheme = /^https?:\/\//.test(domain) ? domain : `https://${domain}`;
|
|
4
|
+
return withScheme.replace(/\/$/, "");
|
|
5
|
+
}
|
|
6
|
+
//# sourceMappingURL=shared.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"shared.js","sourceRoot":"","sources":["../../../../src/server/auth/providers/shared.ts"],"names":[],"mappings":"AAAA,iFAAiF;AACjF,MAAM,UAAU,WAAW,CAAC,MAAc;IACxC,MAAM,UAAU,GAAG,cAAc,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,WAAW,MAAM,EAAE,CAAC;IAC9E,OAAO,UAAU,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;AACvC,CAAC","sourcesContent":["/** Normalises a bare host to an https issuer URL and strips a trailing slash. */\nexport function toIssuerUrl(domain: string): string {\n const withScheme = /^https?:\\/\\//.test(domain) ? domain : `https://${domain}`;\n return withScheme.replace(/\\/$/, \"\");\n}\n"]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { toIssuerUrl } from "./shared.js";
|
|
3
|
+
describe("toIssuerUrl", () => {
|
|
4
|
+
it("adds https to a bare host, preserves an explicit scheme, strips a trailing slash", () => {
|
|
5
|
+
expect(toIssuerUrl("acme.authkit.app")).toBe("https://acme.authkit.app");
|
|
6
|
+
expect(toIssuerUrl("https://acme.authkit.app/")).toBe("https://acme.authkit.app");
|
|
7
|
+
expect(toIssuerUrl("http://localhost:3000")).toBe("http://localhost:3000");
|
|
8
|
+
});
|
|
9
|
+
});
|
|
10
|
+
//# sourceMappingURL=shared.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"shared.test.js","sourceRoot":"","sources":["../../../../src/server/auth/providers/shared.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,QAAQ,CAAC;AAC9C,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAE1C,QAAQ,CAAC,aAAa,EAAE,GAAG,EAAE;IAC3B,EAAE,CAAC,kFAAkF,EAAE,GAAG,EAAE;QAC1F,MAAM,CAAC,WAAW,CAAC,kBAAkB,CAAC,CAAC,CAAC,IAAI,CAAC,0BAA0B,CAAC,CAAC;QACzE,MAAM,CAAC,WAAW,CAAC,2BAA2B,CAAC,CAAC,CAAC,IAAI,CACnD,0BAA0B,CAC3B,CAAC;QACF,MAAM,CAAC,WAAW,CAAC,uBAAuB,CAAC,CAAC,CAAC,IAAI,CAAC,uBAAuB,CAAC,CAAC;IAC7E,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC","sourcesContent":["import { describe, expect, it } from \"vitest\";\nimport { toIssuerUrl } from \"./shared.js\";\n\ndescribe(\"toIssuerUrl\", () => {\n it(\"adds https to a bare host, preserves an explicit scheme, strips a trailing slash\", () => {\n expect(toIssuerUrl(\"acme.authkit.app\")).toBe(\"https://acme.authkit.app\");\n expect(toIssuerUrl(\"https://acme.authkit.app/\")).toBe(\n \"https://acme.authkit.app\",\n );\n expect(toIssuerUrl(\"http://localhost:3000\")).toBe(\"http://localhost:3000\");\n });\n});\n"]}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { OAuthConfig } from "../index.js";
|
|
2
|
+
import { type CustomProviderOptions } from "./custom.js";
|
|
3
|
+
/**
|
|
4
|
+
* OAuth provider for Stytch Connected Apps. `domain` is the project domain,
|
|
5
|
+
* e.g. `acme.customers.stytch.dev`, or a configured custom domain. Requires
|
|
6
|
+
* DCR enabled in the Stytch dashboard. `audience` is the Stytch Project ID
|
|
7
|
+
* (the default token audience).
|
|
8
|
+
*/
|
|
9
|
+
export declare function stytchProvider(opts: {
|
|
10
|
+
domain: string;
|
|
11
|
+
audience: string;
|
|
12
|
+
} & Omit<CustomProviderOptions, "issuer" | "audience">): Promise<OAuthConfig>;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { customProvider } from "./custom.js";
|
|
2
|
+
import { toIssuerUrl } from "./shared.js";
|
|
3
|
+
/**
|
|
4
|
+
* OAuth provider for Stytch Connected Apps. `domain` is the project domain,
|
|
5
|
+
* e.g. `acme.customers.stytch.dev`, or a configured custom domain. Requires
|
|
6
|
+
* DCR enabled in the Stytch dashboard. `audience` is the Stytch Project ID
|
|
7
|
+
* (the default token audience).
|
|
8
|
+
*/
|
|
9
|
+
export function stytchProvider(opts) {
|
|
10
|
+
const { domain, ...rest } = opts;
|
|
11
|
+
return customProvider({ issuer: toIssuerUrl(domain), ...rest });
|
|
12
|
+
}
|
|
13
|
+
//# sourceMappingURL=stytch.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"stytch.js","sourceRoot":"","sources":["../../../../src/server/auth/providers/stytch.ts"],"names":[],"mappings":"AACA,OAAO,EAA8B,cAAc,EAAE,MAAM,aAAa,CAAC;AACzE,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAE1C;;;;;GAKG;AACH,MAAM,UAAU,cAAc,CAC5B,IAGC;IAED,MAAM,EAAE,MAAM,EAAE,GAAG,IAAI,EAAE,GAAG,IAAI,CAAC;IACjC,OAAO,cAAc,CAAC,EAAE,MAAM,EAAE,WAAW,CAAC,MAAM,CAAC,EAAE,GAAG,IAAI,EAAE,CAAC,CAAC;AAClE,CAAC","sourcesContent":["import type { OAuthConfig } from \"../index.js\";\nimport { type CustomProviderOptions, customProvider } from \"./custom.js\";\nimport { toIssuerUrl } from \"./shared.js\";\n\n/**\n * OAuth provider for Stytch Connected Apps. `domain` is the project domain,\n * e.g. `acme.customers.stytch.dev`, or a configured custom domain. Requires\n * DCR enabled in the Stytch dashboard. `audience` is the Stytch Project ID\n * (the default token audience).\n */\nexport function stytchProvider(\n opts: { domain: string; audience: string } & Omit<\n CustomProviderOptions,\n \"issuer\" | \"audience\"\n >,\n): Promise<OAuthConfig> {\n const { domain, ...rest } = opts;\n return customProvider({ issuer: toIssuerUrl(domain), ...rest });\n}\n"]}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { OAuthConfig } from "../index.js";
|
|
2
|
+
import { type CustomProviderOptions } from "./custom.js";
|
|
3
|
+
/**
|
|
4
|
+
* OAuth provider for WorkOS AuthKit. `domain` is the AuthKit domain, e.g.
|
|
5
|
+
* `acme.authkit.app`. Requires DCR enabled in the WorkOS dashboard
|
|
6
|
+
* (Connect → Configuration). `audience` is the MCP server's Resource Indicator.
|
|
7
|
+
*/
|
|
8
|
+
export declare function workosProvider(opts: {
|
|
9
|
+
domain: string;
|
|
10
|
+
audience: string;
|
|
11
|
+
} & Omit<CustomProviderOptions, "issuer" | "audience">): Promise<OAuthConfig>;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { customProvider } from "./custom.js";
|
|
2
|
+
import { toIssuerUrl } from "./shared.js";
|
|
3
|
+
/**
|
|
4
|
+
* OAuth provider for WorkOS AuthKit. `domain` is the AuthKit domain, e.g.
|
|
5
|
+
* `acme.authkit.app`. Requires DCR enabled in the WorkOS dashboard
|
|
6
|
+
* (Connect → Configuration). `audience` is the MCP server's Resource Indicator.
|
|
7
|
+
*/
|
|
8
|
+
export function workosProvider(opts) {
|
|
9
|
+
const { domain, ...rest } = opts;
|
|
10
|
+
return customProvider({ issuer: toIssuerUrl(domain), ...rest });
|
|
11
|
+
}
|
|
12
|
+
//# sourceMappingURL=workos.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"workos.js","sourceRoot":"","sources":["../../../../src/server/auth/providers/workos.ts"],"names":[],"mappings":"AACA,OAAO,EAA8B,cAAc,EAAE,MAAM,aAAa,CAAC;AACzE,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAE1C;;;;GAIG;AACH,MAAM,UAAU,cAAc,CAC5B,IAGC;IAED,MAAM,EAAE,MAAM,EAAE,GAAG,IAAI,EAAE,GAAG,IAAI,CAAC;IACjC,OAAO,cAAc,CAAC,EAAE,MAAM,EAAE,WAAW,CAAC,MAAM,CAAC,EAAE,GAAG,IAAI,EAAE,CAAC,CAAC;AAClE,CAAC","sourcesContent":["import type { OAuthConfig } from \"../index.js\";\nimport { type CustomProviderOptions, customProvider } from \"./custom.js\";\nimport { toIssuerUrl } from \"./shared.js\";\n\n/**\n * OAuth provider for WorkOS AuthKit. `domain` is the AuthKit domain, e.g.\n * `acme.authkit.app`. Requires DCR enabled in the WorkOS dashboard\n * (Connect → Configuration). `audience` is the MCP server's Resource Indicator.\n */\nexport function workosProvider(\n opts: { domain: string; audience: string } & Omit<\n CustomProviderOptions,\n \"issuer\" | \"audience\"\n >,\n): Promise<OAuthConfig> {\n const { domain, ...rest } = opts;\n return customProvider({ issuer: toIssuerUrl(domain), ...rest });\n}\n"]}
|
|
@@ -5,8 +5,8 @@ import { resolveServerOrigin } from "../requestOrigin.js";
|
|
|
5
5
|
import { createJwksVerifier } from "./verify.js";
|
|
6
6
|
/** Mounts the well-known OAuth metadata and bearer auth on `/mcp`. */
|
|
7
7
|
export function setupOAuth(app, config) {
|
|
8
|
-
if (!config.verify?.issuer
|
|
9
|
-
throw new Error("oauth.verify requires
|
|
8
|
+
if (!config.verify?.issuer) {
|
|
9
|
+
throw new Error("oauth.verify requires an `issuer`");
|
|
10
10
|
}
|
|
11
11
|
const verifier = createJwksVerifier(config.verify);
|
|
12
12
|
// baseUrl known at boot: bake the resource URLs once, no Host-header trust.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"setup.js","sourceRoot":"","sources":["../../../src/server/auth/setup.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,oCAAoC,EACpC,qBAAqB,GACtB,MAAM,iDAAiD,CAAC;AACzD,OAAO,IAAI,MAAM,MAAM,CAAC;AAExB,OAAO,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC;AAC/C,OAAO,EAAE,mBAAmB,EAAE,MAAM,qBAAqB,CAAC;AAE1D,OAAO,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AAEjD,sEAAsE;AACtE,MAAM,UAAU,UAAU,CAAC,GAAY,EAAE,MAAmB;IAC1D,IAAI,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,
|
|
1
|
+
{"version":3,"file":"setup.js","sourceRoot":"","sources":["../../../src/server/auth/setup.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,oCAAoC,EACpC,qBAAqB,GACtB,MAAM,iDAAiD,CAAC;AACzD,OAAO,IAAI,MAAM,MAAM,CAAC;AAExB,OAAO,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC;AAC/C,OAAO,EAAE,mBAAmB,EAAE,MAAM,qBAAqB,CAAC;AAE1D,OAAO,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AAEjD,sEAAsE;AACtE,MAAM,UAAU,UAAU,CAAC,GAAY,EAAE,MAAmB;IAC1D,IAAI,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC;QAC3B,MAAM,IAAI,KAAK,CAAC,mCAAmC,CAAC,CAAC;IACvD,CAAC;IAED,MAAM,QAAQ,GAAG,kBAAkB,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;IAEnD,4EAA4E;IAC5E,IAAI,MAAM,CAAC,OAAO,KAAK,SAAS,EAAE,CAAC;QACjC,IAAI,OAAY,CAAC;QACjB,IAAI,CAAC;YACH,OAAO,GAAG,IAAI,GAAG,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;QACpC,CAAC;QAAC,MAAM,CAAC;YACP,MAAM,IAAI,KAAK,CACb,oDAAoD,IAAI,CAAC,SAAS,CAChE,MAAM,CAAC,OAAO,CACf,EAAE,CACJ,CAAC;QACJ,CAAC;QACD,GAAG,CAAC,GAAG,CACL,qBAAqB,CAAC;YACpB,aAAa,EAAE,MAAM,CAAC,aAAa;YACnC,iBAAiB,EAAE,OAAO;YAC1B,eAAe,EAAE,MAAM,CAAC,eAAe;SACxC,CAAC,CACH,CAAC;QACF,GAAG,CAAC,GAAG,CACL,MAAM,EACN,iBAAiB,CAAC;YAChB,QAAQ;YACR,cAAc,EAAE,MAAM,CAAC,cAAc;YACrC,mBAAmB,EAAE,oCAAoC,CAAC,OAAO,CAAC;SACnE,CAAC,CACH,CAAC;QACF,OAAO;IACT,CAAC;IAED,6EAA6E;IAC7E,8EAA8E;IAC9E,6EAA6E;IAC7E,MAAM,aAAa,GAAG,CAAC,GAAY,EAAE,EAAE,CACrC,IAAI,GAAG,CAAC,mBAAmB,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;IAEtD,GAAG,CAAC,GAAG,CACL,yCAAyC,EACzC,IAAI,EAAE,EACN,CAAC,IAAI,EAAE,GAAG,EAAE,EAAE,CAAC,KAAK,GAAG,CAAC,IAAI,CAAC,MAAM,CAAC,aAAa,CAAC,CACnD,CAAC;IACF,GAAG,CAAC,GAAG,CAAC,uCAAuC,EAAE,IAAI,EAAE,EAAE,CAAC,GAAG,EAAE,GAAG,EAAE,EAAE;QACpE,GAAG,CAAC,IAAI,CAAC;YACP,QAAQ,EAAE,aAAa,CAAC,GAAG,CAAC,CAAC,IAAI;YACjC,qBAAqB,EAAE,CAAC,MAAM,CAAC,aAAa,CAAC,MAAM,CAAC;YACpD,gBAAgB,EAAE,MAAM,CAAC,eAAe;SACzC,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;IACH,GAAG,CAAC,GAAG,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,GAAG,EAAE,IAAI,EAAE,EAAE,CACjC,iBAAiB,CAAC;QAChB,QAAQ;QACR,cAAc,EAAE,MAAM,CAAC,cAAc;QACrC,mBAAmB,EAAE,oCAAoC,CACvD,aAAa,CAAC,GAAG,CAAC,CACnB;KACF,CAAC,CAAC,GAAG,EAAE,GAAG,EAAE,IAAI,CAAC,CACnB,CAAC;AACJ,CAAC","sourcesContent":["import {\n getOAuthProtectedResourceMetadataUrl,\n mcpAuthMetadataRouter,\n} from \"@modelcontextprotocol/sdk/server/auth/router.js\";\nimport cors from \"cors\";\nimport type { Express, Request } from \"express\";\nimport { requireBearerAuth } from \"../auth.js\";\nimport { resolveServerOrigin } from \"../requestOrigin.js\";\nimport type { OAuthConfig } from \"./index.js\";\nimport { createJwksVerifier } from \"./verify.js\";\n\n/** Mounts the well-known OAuth metadata and bearer auth on `/mcp`. */\nexport function setupOAuth(app: Express, config: OAuthConfig): void {\n if (!config.verify?.issuer) {\n throw new Error(\"oauth.verify requires an `issuer`\");\n }\n\n const verifier = createJwksVerifier(config.verify);\n\n // baseUrl known at boot: bake the resource URLs once, no Host-header trust.\n if (config.baseUrl !== undefined) {\n let baseUrl: URL;\n try {\n baseUrl = new URL(config.baseUrl);\n } catch {\n throw new Error(\n `oauth.baseUrl must be a valid absolute URL, got: ${JSON.stringify(\n config.baseUrl,\n )}`,\n );\n }\n app.use(\n mcpAuthMetadataRouter({\n oauthMetadata: config.oauthMetadata,\n resourceServerUrl: baseUrl,\n scopesSupported: config.scopesSupported,\n }),\n );\n app.use(\n \"/mcp\",\n requireBearerAuth({\n verifier,\n requiredScopes: config.requiredScopes,\n resourceMetadataUrl: getOAuthProtectedResourceMetadataUrl(baseUrl),\n }),\n );\n return;\n }\n\n // No baseUrl: resolve the resource origin per request from forwarded headers\n // (same precedence the framework uses for view serverUrl). Origin headers are\n // pathless, so the PRM path stays at root, matching the SDK's static layout.\n const resolveOrigin = (req: Request) =>\n new URL(resolveServerOrigin((key) => req.get(key)));\n\n app.use(\n \"/.well-known/oauth-authorization-server\",\n cors(),\n (_req, res) => void res.json(config.oauthMetadata),\n );\n app.use(\"/.well-known/oauth-protected-resource\", cors(), (req, res) => {\n res.json({\n resource: resolveOrigin(req).href,\n authorization_servers: [config.oauthMetadata.issuer],\n scopes_supported: config.scopesSupported,\n });\n });\n app.use(\"/mcp\", (req, res, next) =>\n requireBearerAuth({\n verifier,\n requiredScopes: config.requiredScopes,\n resourceMetadataUrl: getOAuthProtectedResourceMetadataUrl(\n resolveOrigin(req),\n ),\n })(req, res, next),\n );\n}\n"]}
|
|
@@ -2,8 +2,9 @@ import type { OAuthTokenVerifier } from "@modelcontextprotocol/sdk/server/auth/p
|
|
|
2
2
|
export type JwksVerifyConfig = {
|
|
3
3
|
/** Expected `iss` claim. */
|
|
4
4
|
issuer: string;
|
|
5
|
-
/** Expected `aud` claim.
|
|
6
|
-
|
|
5
|
+
/** Expected `aud` claim. Omit to skip audience verification — for IdPs that
|
|
6
|
+
* don't bind an audience to their access tokens (e.g. Clerk). */
|
|
7
|
+
audience?: string;
|
|
7
8
|
/** Defaults to `${issuer}/.well-known/jwks.json`. */
|
|
8
9
|
jwksUri?: string;
|
|
9
10
|
};
|
|
@@ -11,7 +11,8 @@ export function createJwksVerifier(config) {
|
|
|
11
11
|
try {
|
|
12
12
|
({ payload } = await jose.jwtVerify(token, jwks, {
|
|
13
13
|
issuer: config.issuer,
|
|
14
|
-
audience
|
|
14
|
+
// jose skips the aud check when `audience` is undefined.
|
|
15
|
+
...(config.audience !== undefined && { audience: config.audience }),
|
|
15
16
|
}));
|
|
16
17
|
}
|
|
17
18
|
catch (err) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"verify.js","sourceRoot":"","sources":["../../../src/server/auth/verify.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,MAAM,iDAAiD,CAAC;AAGpF,OAAO,KAAK,IAAI,MAAM,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"verify.js","sourceRoot":"","sources":["../../../src/server/auth/verify.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,MAAM,iDAAiD,CAAC;AAGpF,OAAO,KAAK,IAAI,MAAM,MAAM,CAAC;AAY7B,oGAAoG;AACpG,MAAM,UAAU,kBAAkB,CAChC,MAAwB;IAExB,MAAM,OAAO,GACX,MAAM,CAAC,OAAO;QACd,GAAG,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,wBAAwB,CAAC;IAC9D,MAAM,IAAI,GAAG,IAAI,CAAC,kBAAkB,CAAC,IAAI,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC;IAEvD,OAAO;QACL,KAAK,CAAC,iBAAiB,CAAC,KAAa;YACnC,IAAI,OAAwB,CAAC;YAC7B,IAAI,CAAC;gBACH,CAAC,EAAE,OAAO,EAAE,GAAG,MAAM,IAAI,CAAC,SAAS,CAAC,KAAK,EAAE,IAAI,EAAE;oBAC/C,MAAM,EAAE,MAAM,CAAC,MAAM;oBACrB,yDAAyD;oBACzD,GAAG,CAAC,MAAM,CAAC,QAAQ,KAAK,SAAS,IAAI,EAAE,QAAQ,EAAE,MAAM,CAAC,QAAQ,EAAE,CAAC;iBACpE,CAAC,CAAC,CAAC;YACN,CAAC;YAAC,OAAO,GAAG,EAAE,CAAC;gBACb,MAAM,OAAO,GAAG,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;gBACjE,MAAM,IAAI,iBAAiB,CAAC,8BAA8B,OAAO,EAAE,CAAC,CAAC;YACvE,CAAC;YAED,MAAM,EAAE,SAAS,EAAE,KAAK,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,IAAI,EAAE,GAAG,OAQ/C,CAAC;YAEF,MAAM,MAAM,GAAG,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC;gBACjC,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,MAAM,CAAC;gBACnB,CAAC,CAAC,OAAO,KAAK,KAAK,QAAQ;oBACzB,CAAC,CAAC,KAAK,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC;oBACpC,CAAC,CAAC,EAAE,CAAC;YAET,OAAO;gBACL,KAAK;gBACL,QAAQ,EAAE,SAAS,IAAI,EAAE;gBACzB,MAAM;gBACN,SAAS,EAAE,GAAG;gBACd,KAAK,EAAE,EAAE,OAAO,EAAE,GAAG,EAAE,GAAG,IAAI,EAAE;aACd,CAAC;QACvB,CAAC;KACF,CAAC;AACJ,CAAC","sourcesContent":["import { InvalidTokenError } from \"@modelcontextprotocol/sdk/server/auth/errors.js\";\nimport type { OAuthTokenVerifier } from \"@modelcontextprotocol/sdk/server/auth/provider.js\";\nimport type { AuthInfo } from \"@modelcontextprotocol/sdk/server/auth/types.js\";\nimport * as jose from \"jose\";\n\nexport type JwksVerifyConfig = {\n /** Expected `iss` claim. */\n issuer: string;\n /** Expected `aud` claim. Omit to skip audience verification — for IdPs that\n * don't bind an audience to their access tokens (e.g. Clerk). */\n audience?: string;\n /** Defaults to `${issuer}/.well-known/jwks.json`. */\n jwksUri?: string;\n};\n\n/** Builds an `OAuthTokenVerifier` validating JWTs against a remote JWKS. Internal, not exported. */\nexport function createJwksVerifier(\n config: JwksVerifyConfig,\n): OAuthTokenVerifier {\n const jwksUri =\n config.jwksUri ??\n `${config.issuer.replace(/\\/$/, \"\")}/.well-known/jwks.json`;\n const jwks = jose.createRemoteJWKSet(new URL(jwksUri));\n\n return {\n async verifyAccessToken(token: string): Promise<AuthInfo> {\n let payload: jose.JWTPayload;\n try {\n ({ payload } = await jose.jwtVerify(token, jwks, {\n issuer: config.issuer,\n // jose skips the aud check when `audience` is undefined.\n ...(config.audience !== undefined && { audience: config.audience }),\n }));\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err);\n throw new InvalidTokenError(`Token verification failed: ${message}`);\n }\n\n const { client_id, scope, exp, sub, ...rest } = payload as Record<\n string,\n unknown\n > & {\n client_id?: string;\n scope?: string | string[];\n exp?: number;\n sub?: string;\n };\n\n const scopes = Array.isArray(scope)\n ? scope.map(String)\n : typeof scope === \"string\"\n ? scope.split(/\\s+/).filter(Boolean)\n : [];\n\n return {\n token,\n clientId: client_id ?? \"\",\n scopes,\n expiresAt: exp,\n extra: { subject: sub, ...rest },\n } satisfies AuthInfo;\n },\n };\n}\n"]}
|
|
@@ -63,6 +63,19 @@ describe("createJwksVerifier", () => {
|
|
|
63
63
|
const token = await sign(privateKey, { client_id: "c" }, { audience: "api://other" });
|
|
64
64
|
await expect(verifier.verifyAccessToken(token)).rejects.toThrow(/Token verification failed/);
|
|
65
65
|
});
|
|
66
|
+
it("skips the aud check when no audience is configured (e.g. Clerk)", async () => {
|
|
67
|
+
const { privateKey, jwksUri } = await startJwks();
|
|
68
|
+
const verifier = createJwksVerifier({ issuer: ISSUER, jwksUri });
|
|
69
|
+
// Clerk-shaped access token: no `aud` claim at all.
|
|
70
|
+
const token = await new jose.SignJWT({ client_id: "c", sub: "u" })
|
|
71
|
+
.setProtectedHeader({ alg: "RS256", kid: "test-key" })
|
|
72
|
+
.setIssuer(ISSUER)
|
|
73
|
+
.setExpirationTime("1h")
|
|
74
|
+
.sign(privateKey);
|
|
75
|
+
const auth = await verifier.verifyAccessToken(token);
|
|
76
|
+
expect(auth.clientId).toBe("c");
|
|
77
|
+
expect(auth.extra?.subject).toBe("u");
|
|
78
|
+
});
|
|
66
79
|
it("parses array scope claims and trims extra whitespace", async () => {
|
|
67
80
|
const { privateKey, jwksUri } = await startJwks();
|
|
68
81
|
const verifier = createJwksVerifier({
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"verify.test.js","sourceRoot":"","sources":["../../../src/server/auth/verify.test.ts"],"names":[],"mappings":"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","sourcesContent":["// @vitest-environment node\nimport http from \"node:http\";\nimport * as jose from \"jose\";\nimport { afterEach, describe, expect, it } from \"vitest\";\nimport { createJwksVerifier } from \"./verify.js\";\n\nconst ISSUER = \"https://issuer.test\";\nconst AUDIENCE = \"api://default\";\n\nlet jwksServer: http.Server | undefined;\nafterEach(() => jwksServer?.close());\n\nasync function startJwks() {\n const { publicKey, privateKey } = await jose.generateKeyPair(\"RS256\");\n const jwk = {\n ...(await jose.exportJWK(publicKey)),\n kid: \"test-key\",\n alg: \"RS256\",\n use: \"sig\",\n };\n const server = http.createServer((_req, res) => {\n res.setHeader(\"content-type\", \"application/json\");\n res.end(JSON.stringify({ keys: [jwk] }));\n });\n await new Promise<void>((resolve) => server.listen(0, resolve));\n jwksServer = server;\n const port = (server.address() as { port: number }).port;\n return { privateKey, jwksUri: `http://localhost:${port}/jwks` };\n}\n\nfunction sign(\n key: CryptoKey,\n claims: Record<string, unknown>,\n opts: { issuer?: string; audience?: string; expiresAt?: string } = {},\n) {\n return new jose.SignJWT(claims)\n .setProtectedHeader({ alg: \"RS256\", kid: \"test-key\" })\n .setIssuer(opts.issuer ?? ISSUER)\n .setAudience(opts.audience ?? AUDIENCE)\n .setExpirationTime(opts.expiresAt ?? \"1h\")\n .sign(key);\n}\n\ndescribe(\"createJwksVerifier\", () => {\n it(\"verifies a valid token and maps claims to AuthInfo\", async () => {\n const { privateKey, jwksUri } = await startJwks();\n const verifier = createJwksVerifier({\n issuer: ISSUER,\n audience: AUDIENCE,\n jwksUri,\n });\n const token = await sign(privateKey, {\n client_id: \"client-1\",\n scope: \"openid email\",\n sub: \"user-1\",\n email: \"a@b.test\",\n });\n\n const auth = await verifier.verifyAccessToken(token);\n\n expect(auth.token).toBe(token);\n expect(auth.clientId).toBe(\"client-1\");\n expect(auth.scopes).toEqual([\"openid\", \"email\"]);\n expect(auth.extra?.subject).toBe(\"user-1\");\n expect(auth.extra?.email).toBe(\"a@b.test\");\n });\n\n it(\"rejects a token with the wrong audience\", async () => {\n const { privateKey, jwksUri } = await startJwks();\n const verifier = createJwksVerifier({\n issuer: ISSUER,\n audience: AUDIENCE,\n jwksUri,\n });\n const token = await sign(\n privateKey,\n { client_id: \"c\" },\n { audience: \"api://other\" },\n );\n\n await expect(verifier.verifyAccessToken(token)).rejects.toThrow(\n /Token verification failed/,\n );\n });\n\n it(\"parses array scope claims and trims extra whitespace\", async () => {\n const { privateKey, jwksUri } = await startJwks();\n const verifier = createJwksVerifier({\n issuer: ISSUER,\n audience: AUDIENCE,\n jwksUri,\n });\n\n const arrayToken = await sign(privateKey, {\n scope: [\"openid\", \"email\"],\n });\n expect((await verifier.verifyAccessToken(arrayToken)).scopes).toEqual([\n \"openid\",\n \"email\",\n ]);\n\n const messyToken = await sign(privateKey, { scope: \" openid email \" });\n expect((await verifier.verifyAccessToken(messyToken)).scopes).toEqual([\n \"openid\",\n \"email\",\n ]);\n });\n});\n"]}
|
|
1
|
+
{"version":3,"file":"verify.test.js","sourceRoot":"","sources":["../../../src/server/auth/verify.test.ts"],"names":[],"mappings":"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","sourcesContent":["// @vitest-environment node\nimport http from \"node:http\";\nimport * as jose from \"jose\";\nimport { afterEach, describe, expect, it } from \"vitest\";\nimport { createJwksVerifier } from \"./verify.js\";\n\nconst ISSUER = \"https://issuer.test\";\nconst AUDIENCE = \"api://default\";\n\nlet jwksServer: http.Server | undefined;\nafterEach(() => jwksServer?.close());\n\nasync function startJwks() {\n const { publicKey, privateKey } = await jose.generateKeyPair(\"RS256\");\n const jwk = {\n ...(await jose.exportJWK(publicKey)),\n kid: \"test-key\",\n alg: \"RS256\",\n use: \"sig\",\n };\n const server = http.createServer((_req, res) => {\n res.setHeader(\"content-type\", \"application/json\");\n res.end(JSON.stringify({ keys: [jwk] }));\n });\n await new Promise<void>((resolve) => server.listen(0, resolve));\n jwksServer = server;\n const port = (server.address() as { port: number }).port;\n return { privateKey, jwksUri: `http://localhost:${port}/jwks` };\n}\n\nfunction sign(\n key: CryptoKey,\n claims: Record<string, unknown>,\n opts: { issuer?: string; audience?: string; expiresAt?: string } = {},\n) {\n return new jose.SignJWT(claims)\n .setProtectedHeader({ alg: \"RS256\", kid: \"test-key\" })\n .setIssuer(opts.issuer ?? ISSUER)\n .setAudience(opts.audience ?? AUDIENCE)\n .setExpirationTime(opts.expiresAt ?? \"1h\")\n .sign(key);\n}\n\ndescribe(\"createJwksVerifier\", () => {\n it(\"verifies a valid token and maps claims to AuthInfo\", async () => {\n const { privateKey, jwksUri } = await startJwks();\n const verifier = createJwksVerifier({\n issuer: ISSUER,\n audience: AUDIENCE,\n jwksUri,\n });\n const token = await sign(privateKey, {\n client_id: \"client-1\",\n scope: \"openid email\",\n sub: \"user-1\",\n email: \"a@b.test\",\n });\n\n const auth = await verifier.verifyAccessToken(token);\n\n expect(auth.token).toBe(token);\n expect(auth.clientId).toBe(\"client-1\");\n expect(auth.scopes).toEqual([\"openid\", \"email\"]);\n expect(auth.extra?.subject).toBe(\"user-1\");\n expect(auth.extra?.email).toBe(\"a@b.test\");\n });\n\n it(\"rejects a token with the wrong audience\", async () => {\n const { privateKey, jwksUri } = await startJwks();\n const verifier = createJwksVerifier({\n issuer: ISSUER,\n audience: AUDIENCE,\n jwksUri,\n });\n const token = await sign(\n privateKey,\n { client_id: \"c\" },\n { audience: \"api://other\" },\n );\n\n await expect(verifier.verifyAccessToken(token)).rejects.toThrow(\n /Token verification failed/,\n );\n });\n\n it(\"skips the aud check when no audience is configured (e.g. Clerk)\", async () => {\n const { privateKey, jwksUri } = await startJwks();\n const verifier = createJwksVerifier({ issuer: ISSUER, jwksUri });\n // Clerk-shaped access token: no `aud` claim at all.\n const token = await new jose.SignJWT({ client_id: \"c\", sub: \"u\" })\n .setProtectedHeader({ alg: \"RS256\", kid: \"test-key\" })\n .setIssuer(ISSUER)\n .setExpirationTime(\"1h\")\n .sign(privateKey);\n\n const auth = await verifier.verifyAccessToken(token);\n expect(auth.clientId).toBe(\"c\");\n expect(auth.extra?.subject).toBe(\"u\");\n });\n\n it(\"parses array scope claims and trims extra whitespace\", async () => {\n const { privateKey, jwksUri } = await startJwks();\n const verifier = createJwksVerifier({\n issuer: ISSUER,\n audience: AUDIENCE,\n jwksUri,\n });\n\n const arrayToken = await sign(privateKey, {\n scope: [\"openid\", \"email\"],\n });\n expect((await verifier.verifyAccessToken(arrayToken)).scopes).toEqual([\n \"openid\",\n \"email\",\n ]);\n\n const messyToken = await sign(privateKey, { scope: \" openid email \" });\n expect((await verifier.verifyAccessToken(messyToken)).scopes).toEqual([\n \"openid\",\n \"email\",\n ]);\n });\n});\n"]}
|
package/dist/server/index.d.ts
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
export type { OAuthConfig } from "./auth/index.js";
|
|
2
|
+
export { auth0Provider } from "./auth/providers/auth0.js";
|
|
3
|
+
export { clerkProvider } from "./auth/providers/clerk.js";
|
|
2
4
|
export { customProvider } from "./auth/providers/custom.js";
|
|
5
|
+
export { descopeProvider } from "./auth/providers/descope.js";
|
|
6
|
+
export { stytchProvider } from "./auth/providers/stytch.js";
|
|
7
|
+
export { workosProvider } from "./auth/providers/workos.js";
|
|
3
8
|
export { type AuthInfo, type AuthMetadataOptions, type BearerAuthMiddlewareOptions, InvalidTokenError, mcpAuthMetadataRouter, optionalBearerAuth, requireBearerAuth, } from "./auth.js";
|
|
4
9
|
export { audio, embeddedResource, image, resourceLink, text, } from "./content-helpers.js";
|
|
5
10
|
export { FileRef } from "./file-ref.js";
|
package/dist/server/index.js
CHANGED
|
@@ -1,4 +1,9 @@
|
|
|
1
|
+
export { auth0Provider } from "./auth/providers/auth0.js";
|
|
2
|
+
export { clerkProvider } from "./auth/providers/clerk.js";
|
|
1
3
|
export { customProvider } from "./auth/providers/custom.js";
|
|
4
|
+
export { descopeProvider } from "./auth/providers/descope.js";
|
|
5
|
+
export { stytchProvider } from "./auth/providers/stytch.js";
|
|
6
|
+
export { workosProvider } from "./auth/providers/workos.js";
|
|
2
7
|
export { InvalidTokenError, mcpAuthMetadataRouter, optionalBearerAuth, requireBearerAuth, } from "./auth.js";
|
|
3
8
|
export { audio, embeddedResource, image, resourceLink, text, } from "./content-helpers.js";
|
|
4
9
|
export { FileRef } from "./file-ref.js";
|
package/dist/server/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/server/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,cAAc,EAAE,MAAM,4BAA4B,CAAC;AAC5D,OAAO,EAIL,iBAAiB,EACjB,qBAAqB,EACrB,kBAAkB,EAClB,iBAAiB,GAClB,MAAM,WAAW,CAAC;AACnB,OAAO,EACL,KAAK,EACL,gBAAgB,EAChB,KAAK,EACL,YAAY,EACZ,IAAI,GACL,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EAAE,OAAO,EAAE,MAAM,eAAe,CAAC;AAiCxC,OAAO,EACL,kBAAkB,EAClB,SAAS,EACT,gBAAgB,GACjB,MAAM,aAAa,CAAC;AACrB,OAAO,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC","sourcesContent":["export type { OAuthConfig } from \"./auth/index.js\";\nexport { customProvider } from \"./auth/providers/custom.js\";\nexport {\n type AuthInfo,\n type AuthMetadataOptions,\n type BearerAuthMiddlewareOptions,\n InvalidTokenError,\n mcpAuthMetadataRouter,\n optionalBearerAuth,\n requireBearerAuth,\n} from \"./auth.js\";\nexport {\n audio,\n embeddedResource,\n image,\n resourceLink,\n text,\n} from \"./content-helpers.js\";\nexport { FileRef } from \"./file-ref.js\";\nexport type {\n AnyToolRegistry,\n InferTools,\n ToolInput,\n ToolNames,\n ToolOutput,\n ToolResponseMetadata,\n} from \"./inferUtilityTypes.js\";\nexport type {\n McpExtra,\n McpMethodString,\n McpMiddlewareFilter,\n McpMiddlewareFn,\n McpResultFor,\n McpTypedMiddlewareFn,\n McpWildcard,\n} from \"./middleware.js\";\nexport type {\n HandlerContent,\n JsonOptions,\n KnownToolMeta,\n McpServerTypes,\n SecurityScheme,\n SkybridgeServerOptions,\n ToolDef,\n ToolMeta,\n ViewConfig,\n ViewCsp,\n ViewHostType,\n ViewName,\n ViewNameRegistry,\n} from \"./server.js\";\nexport {\n __setBuildManifest,\n McpServer,\n normalizeContent,\n} from \"./server.js\";\nexport { viewsDevServer } from \"./viewsDevServer.js\";\n"]}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/server/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,aAAa,EAAE,MAAM,2BAA2B,CAAC;AAC1D,OAAO,EAAE,aAAa,EAAE,MAAM,2BAA2B,CAAC;AAC1D,OAAO,EAAE,cAAc,EAAE,MAAM,4BAA4B,CAAC;AAC5D,OAAO,EAAE,eAAe,EAAE,MAAM,6BAA6B,CAAC;AAC9D,OAAO,EAAE,cAAc,EAAE,MAAM,4BAA4B,CAAC;AAC5D,OAAO,EAAE,cAAc,EAAE,MAAM,4BAA4B,CAAC;AAC5D,OAAO,EAIL,iBAAiB,EACjB,qBAAqB,EACrB,kBAAkB,EAClB,iBAAiB,GAClB,MAAM,WAAW,CAAC;AACnB,OAAO,EACL,KAAK,EACL,gBAAgB,EAChB,KAAK,EACL,YAAY,EACZ,IAAI,GACL,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EAAE,OAAO,EAAE,MAAM,eAAe,CAAC;AAiCxC,OAAO,EACL,kBAAkB,EAClB,SAAS,EACT,gBAAgB,GACjB,MAAM,aAAa,CAAC;AACrB,OAAO,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC","sourcesContent":["export type { OAuthConfig } from \"./auth/index.js\";\nexport { auth0Provider } from \"./auth/providers/auth0.js\";\nexport { clerkProvider } from \"./auth/providers/clerk.js\";\nexport { customProvider } from \"./auth/providers/custom.js\";\nexport { descopeProvider } from \"./auth/providers/descope.js\";\nexport { stytchProvider } from \"./auth/providers/stytch.js\";\nexport { workosProvider } from \"./auth/providers/workos.js\";\nexport {\n type AuthInfo,\n type AuthMetadataOptions,\n type BearerAuthMiddlewareOptions,\n InvalidTokenError,\n mcpAuthMetadataRouter,\n optionalBearerAuth,\n requireBearerAuth,\n} from \"./auth.js\";\nexport {\n audio,\n embeddedResource,\n image,\n resourceLink,\n text,\n} from \"./content-helpers.js\";\nexport { FileRef } from \"./file-ref.js\";\nexport type {\n AnyToolRegistry,\n InferTools,\n ToolInput,\n ToolNames,\n ToolOutput,\n ToolResponseMetadata,\n} from \"./inferUtilityTypes.js\";\nexport type {\n McpExtra,\n McpMethodString,\n McpMiddlewareFilter,\n McpMiddlewareFn,\n McpResultFor,\n McpTypedMiddlewareFn,\n McpWildcard,\n} from \"./middleware.js\";\nexport type {\n HandlerContent,\n JsonOptions,\n KnownToolMeta,\n McpServerTypes,\n SecurityScheme,\n SkybridgeServerOptions,\n ToolDef,\n ToolMeta,\n ViewConfig,\n ViewCsp,\n ViewHostType,\n ViewName,\n ViewNameRegistry,\n} from \"./server.js\";\nexport {\n __setBuildManifest,\n McpServer,\n normalizeContent,\n} from \"./server.js\";\nexport { viewsDevServer } from \"./viewsDevServer.js\";\n"]}
|
package/dist/server/server.d.ts
CHANGED
|
@@ -49,8 +49,16 @@ export interface ViewCsp {
|
|
|
49
49
|
*/
|
|
50
50
|
export interface ViewNameRegistry {
|
|
51
51
|
}
|
|
52
|
+
/**
|
|
53
|
+
* Resolve view component names from a registry: the union of its keys, or
|
|
54
|
+
* `string` when the registry is empty. The empty case happens before
|
|
55
|
+
* `.skybridge/views.d.ts` is generated; falling back to `string` keeps valid
|
|
56
|
+
* view names from erroring on a fresh checkout, and narrowing kicks in once
|
|
57
|
+
* the generated file augments the registry.
|
|
58
|
+
*/
|
|
59
|
+
export type ViewNameFor<Registry> = [keyof Registry & string] extends [never] ? string : keyof Registry & string;
|
|
52
60
|
/** Union of valid view component names. Narrowed by {@link ViewNameRegistry}. */
|
|
53
|
-
export type ViewName =
|
|
61
|
+
export type ViewName = ViewNameFor<ViewNameRegistry>;
|
|
54
62
|
/**
|
|
55
63
|
* Pass under `view` in a tool's `registerTool` config to render the tool's
|
|
56
64
|
* result through a Skybridge view instead of a plain text response.
|
|
@@ -357,6 +365,7 @@ export declare class McpServer<TTools extends Record<string, ToolDef> = Record<n
|
|
|
357
365
|
private resolveViewRequestContext;
|
|
358
366
|
private registerViewResources;
|
|
359
367
|
private registerViewResource;
|
|
368
|
+
private serveLegacyAppsSdkUrl;
|
|
360
369
|
private wrapHandler;
|
|
361
370
|
private computeViewVersionParam;
|
|
362
371
|
private lookupViewFile;
|
package/dist/server/server.js
CHANGED
|
@@ -19,6 +19,19 @@ const mergeWithUnion = (target, source) => {
|
|
|
19
19
|
}
|
|
20
20
|
});
|
|
21
21
|
};
|
|
22
|
+
/**
|
|
23
|
+
* Normalize an `x-forwarded-prefix` value into a leading-slash, no-trailing-slash
|
|
24
|
+
* path. Takes the first hop of a comma-separated proxy chain.
|
|
25
|
+
* "/v1/", "v1", "/v1, /internal" → "/v1"; "", "/", undefined → "".
|
|
26
|
+
*/
|
|
27
|
+
function normalizeForwardedPrefix(raw) {
|
|
28
|
+
const firstHop = raw?.split(",")[0]?.trim() ?? "";
|
|
29
|
+
const trimmed = firstHop.replace(/\/+$/, "");
|
|
30
|
+
if (trimmed === "") {
|
|
31
|
+
return "";
|
|
32
|
+
}
|
|
33
|
+
return trimmed.startsWith("/") ? trimmed : `/${trimmed}`;
|
|
34
|
+
}
|
|
22
35
|
/**
|
|
23
36
|
* Drop the query string from a `ui://` view URI, leaving the bare path. The
|
|
24
37
|
* `?v=` cache key is the only query we append, so a plain split is enough and
|
|
@@ -236,7 +249,7 @@ export class McpServer extends McpServerBaseOmitted {
|
|
|
236
249
|
const result = (await next());
|
|
237
250
|
for (const content of result.contents ?? []) {
|
|
238
251
|
if (typeof content.uri === "string" &&
|
|
239
|
-
stripQuery(content.uri) ===
|
|
252
|
+
stripQuery(content.uri) === stripQuery(canonical)) {
|
|
240
253
|
content.uri = requested;
|
|
241
254
|
}
|
|
242
255
|
}
|
|
@@ -383,6 +396,11 @@ export class McpServer extends McpServerBaseOmitted {
|
|
|
383
396
|
};
|
|
384
397
|
const isClaude = header("user-agent") === "Claude-User";
|
|
385
398
|
const serverUrl = resolveServerOrigin(header);
|
|
399
|
+
// Path prefix the proxy routed this request under (e.g. `foo.com/v1`). Read
|
|
400
|
+
// per-request so one process can serve many hosts/prefixes at once: the
|
|
401
|
+
// origin is recovered from x-forwarded-host, the prefix from
|
|
402
|
+
// x-forwarded-prefix. Empty when served at the origin root.
|
|
403
|
+
const assetsBasePath = normalizeForwardedPrefix(header("x-forwarded-prefix"));
|
|
386
404
|
const connectDomains = [serverUrl];
|
|
387
405
|
if (!isProduction) {
|
|
388
406
|
const wsUrl = new URL(serverUrl);
|
|
@@ -403,11 +421,9 @@ export class McpServer extends McpServerBaseOmitted {
|
|
|
403
421
|
.slice(0, 32);
|
|
404
422
|
contentMetaOverrides = { domain: `${hash}.claudemcpcontent.com` };
|
|
405
423
|
}
|
|
406
|
-
return { serverUrl, connectDomains, contentMetaOverrides };
|
|
424
|
+
return { serverUrl, assetsBasePath, connectDomains, contentMetaOverrides };
|
|
407
425
|
}
|
|
408
426
|
registerViewResources(toolName, view, toolMeta) {
|
|
409
|
-
// `hosts` is deprecated and ignored — every view emits the single ext-apps
|
|
410
|
-
// resource. Kept for backwards compatibility; see ViewConfig.hosts.
|
|
411
427
|
// Append a content-derived version param so hosts (e.g. ChatGPT) bust
|
|
412
428
|
// their cache when the bundle changes, but keep the URI stable across
|
|
413
429
|
// `tools/list` calls when the bundle hasn't changed.
|
|
@@ -447,15 +463,21 @@ export class McpServer extends McpServerBaseOmitted {
|
|
|
447
463
|
...(view.csp?.baseUriDomains && {
|
|
448
464
|
baseUriDomains: view.csp.baseUriDomains,
|
|
449
465
|
}),
|
|
450
|
-
...(view.csp?.redirectDomains && {
|
|
451
|
-
redirectDomains: view.csp.redirectDomains,
|
|
452
|
-
}),
|
|
453
466
|
},
|
|
454
467
|
},
|
|
455
468
|
};
|
|
456
|
-
const
|
|
469
|
+
const ui = mergeWithUnion(mergeWithUnion(defaults, fromView), {
|
|
457
470
|
ui: overrides,
|
|
458
471
|
});
|
|
472
|
+
const base = {
|
|
473
|
+
...ui,
|
|
474
|
+
...(view.description && {
|
|
475
|
+
"openai/widgetDescription": view.description,
|
|
476
|
+
}),
|
|
477
|
+
...(view.csp?.redirectDomains && {
|
|
478
|
+
"openai/widgetCSP": { redirect_domains: view.csp.redirectDomains },
|
|
479
|
+
}),
|
|
480
|
+
};
|
|
459
481
|
if (view._meta) {
|
|
460
482
|
return { ...base, ...view._meta };
|
|
461
483
|
}
|
|
@@ -463,9 +485,10 @@ export class McpServer extends McpServerBaseOmitted {
|
|
|
463
485
|
},
|
|
464
486
|
};
|
|
465
487
|
this.registerViewResource({ name: toolName, viewResource, view });
|
|
466
|
-
// Advertise the
|
|
467
|
-
//
|
|
468
|
-
|
|
488
|
+
// Advertise via the MCP Apps standard pointer only — ChatGPT renders from
|
|
489
|
+
// ui.resourceUri (verified), and not emitting openai/outputTemplate lets us
|
|
490
|
+
// retire the legacy apps-sdk resource later. The legacy apps-sdk URL is still
|
|
491
|
+
// served (see registerViewResource) so already-published apps keep resolving.
|
|
469
492
|
// @ts-expect-error - For backwards compatibility with Claude current implementation of the specs
|
|
470
493
|
toolMeta["ui/resourceUri"] = viewResource.uri;
|
|
471
494
|
toolMeta.ui = { ...toolMeta.ui, resourceUri: viewResource.uri };
|
|
@@ -483,19 +506,24 @@ export class McpServer extends McpServerBaseOmitted {
|
|
|
483
506
|
};
|
|
484
507
|
this.viewMetaBuilders.set(viewUri, buildMeta);
|
|
485
508
|
this.viewUriByPath.set(stripQuery(viewUri), viewUri);
|
|
509
|
+
this.serveLegacyAppsSdkUrl(view.component, viewUri);
|
|
486
510
|
this.registerResource(name, viewUri, { description: view.description }, async (uri, extra) => {
|
|
487
511
|
const isProduction = process.env.NODE_ENV === "production";
|
|
488
|
-
const { serverUrl } = this.resolveViewRequestContext(extra);
|
|
512
|
+
const { serverUrl, assetsBasePath } = this.resolveViewRequestContext(extra);
|
|
513
|
+
// The view resolves all assets (template imports + runtime lazy chunks
|
|
514
|
+
// via `window.skybridge.serverUrl`) against this base, so it carries the
|
|
515
|
+
// proxy path prefix. CSP domains in `buildMeta` stay the bare origin.
|
|
516
|
+
const viewBase = `${serverUrl}${assetsBasePath}`;
|
|
489
517
|
const html = isProduction
|
|
490
518
|
? templateHelper.renderProduction({
|
|
491
519
|
hostType,
|
|
492
|
-
serverUrl,
|
|
520
|
+
serverUrl: viewBase,
|
|
493
521
|
viewFile: this.lookupViewFile(view.component),
|
|
494
522
|
styleFile: this.lookupDistFile("style.css") ?? "",
|
|
495
523
|
})
|
|
496
524
|
: templateHelper.renderDevelopment({
|
|
497
525
|
hostType,
|
|
498
|
-
serverUrl,
|
|
526
|
+
serverUrl: viewBase,
|
|
499
527
|
viewName: view.component,
|
|
500
528
|
});
|
|
501
529
|
return {
|
|
@@ -505,6 +533,11 @@ export class McpServer extends McpServerBaseOmitted {
|
|
|
505
533
|
};
|
|
506
534
|
});
|
|
507
535
|
}
|
|
536
|
+
serveLegacyAppsSdkUrl(component, canonicalUri) {
|
|
537
|
+
this.viewUriByPath.set(`ui://views/apps-sdk/${component}.html`, canonicalUri);
|
|
538
|
+
this.viewUriByPath.set(`ui://widgets/apps-sdk/${component}.html`, canonicalUri);
|
|
539
|
+
this.viewUriByPath.set(`ui://widgets/ext-apps/${component}.html`, canonicalUri);
|
|
540
|
+
}
|
|
508
541
|
wrapHandler(cb, { attachViewUUID }) {
|
|
509
542
|
return async (args, extra) => {
|
|
510
543
|
const result = await cb(args, extra);
|