skybridge 0.0.0-dev.c0c2e3f → 0.0.0-dev.c0d2fad

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 (95) hide show
  1. package/README.md +1 -0
  2. package/dist/cli/build-helpers.d.ts +6 -2
  3. package/dist/cli/build-helpers.js +37 -12
  4. package/dist/cli/build-helpers.js.map +1 -1
  5. package/dist/cli/build-helpers.test.js +69 -3
  6. package/dist/cli/build-helpers.test.js.map +1 -1
  7. package/dist/cli/build-steps.d.ts +2 -0
  8. package/dist/cli/build-steps.js +73 -0
  9. package/dist/cli/build-steps.js.map +1 -0
  10. package/dist/cli/build-steps.test.d.ts +1 -0
  11. package/dist/cli/build-steps.test.js +52 -0
  12. package/dist/cli/build-steps.test.js.map +1 -0
  13. package/dist/cli/header.d.ts +1 -1
  14. package/dist/cli/resolve-skybridge-config.d.ts +5 -0
  15. package/dist/cli/{resolve-views-dir.js → resolve-skybridge-config.js} +4 -3
  16. package/dist/cli/resolve-skybridge-config.js.map +1 -0
  17. package/dist/cli/run-plain.d.ts +18 -0
  18. package/dist/cli/run-plain.js +89 -0
  19. package/dist/cli/run-plain.js.map +1 -0
  20. package/dist/cli/use-nodemon.d.ts +14 -0
  21. package/dist/cli/use-nodemon.js +71 -60
  22. package/dist/cli/use-nodemon.js.map +1 -1
  23. package/dist/cli/use-typescript-check.d.ts +8 -2
  24. package/dist/cli/use-typescript-check.js +70 -67
  25. package/dist/cli/use-typescript-check.js.map +1 -1
  26. package/dist/commands/build.d.ts +0 -2
  27. package/dist/commands/build.js +2 -61
  28. package/dist/commands/build.js.map +1 -1
  29. package/dist/commands/dev.d.ts +1 -0
  30. package/dist/commands/dev.js +34 -2
  31. package/dist/commands/dev.js.map +1 -1
  32. package/dist/server/auth/providers/custom.d.ts +8 -0
  33. package/dist/server/auth/providers/custom.js +19 -7
  34. package/dist/server/auth/providers/custom.js.map +1 -1
  35. package/dist/server/auth/providers/custom.test.js +53 -4
  36. package/dist/server/auth/providers/custom.test.js.map +1 -1
  37. package/dist/server/auth/providers/descope.js +13 -2
  38. package/dist/server/auth/providers/descope.js.map +1 -1
  39. package/dist/server/auth/providers/descope.test.js +38 -15
  40. package/dist/server/auth/providers/descope.test.js.map +1 -1
  41. package/dist/server/auth/security-schemes.d.ts +10 -0
  42. package/dist/server/auth/security-schemes.js +19 -10
  43. package/dist/server/auth/security-schemes.js.map +1 -1
  44. package/dist/server/auth/security-schemes.test.js +13 -3
  45. package/dist/server/auth/security-schemes.test.js.map +1 -1
  46. package/dist/server/auth/setup.d.ts +2 -1
  47. package/dist/server/auth/setup.js +53 -52
  48. package/dist/server/auth/setup.js.map +1 -1
  49. package/dist/server/auth/setup.test.js +66 -21
  50. package/dist/server/auth/setup.test.js.map +1 -1
  51. package/dist/server/host.d.ts +2 -0
  52. package/dist/server/host.js +11 -0
  53. package/dist/server/host.js.map +1 -0
  54. package/dist/server/host.test.d.ts +1 -0
  55. package/dist/server/host.test.js +13 -0
  56. package/dist/server/host.test.js.map +1 -0
  57. package/dist/server/index.d.ts +2 -1
  58. package/dist/server/index.js +2 -1
  59. package/dist/server/index.js.map +1 -1
  60. package/dist/server/server.d.ts +36 -6
  61. package/dist/server/server.js +112 -34
  62. package/dist/server/server.js.map +1 -1
  63. package/dist/server/skills-integration.test.d.ts +1 -0
  64. package/dist/server/skills-integration.test.js +63 -0
  65. package/dist/server/skills-integration.test.js.map +1 -0
  66. package/dist/server/skills.d.ts +23 -0
  67. package/dist/server/skills.js +178 -0
  68. package/dist/server/skills.js.map +1 -0
  69. package/dist/server/skills.test.d.ts +1 -0
  70. package/dist/server/skills.test.js +210 -0
  71. package/dist/server/skills.test.js.map +1 -0
  72. package/dist/server/view-resource-resolution.test.js +69 -1
  73. package/dist/server/view-resource-resolution.test.js.map +1 -1
  74. package/dist/server/viewsDevServer.js +4 -1
  75. package/dist/server/viewsDevServer.js.map +1 -1
  76. package/dist/test/view.test.js +3 -3
  77. package/dist/test/view.test.js.map +1 -1
  78. package/dist/web/bridges/adaptor.js +22 -2
  79. package/dist/web/bridges/adaptor.js.map +1 -1
  80. package/dist/web/bridges/adaptor.test.js +11 -0
  81. package/dist/web/bridges/adaptor.test.js.map +1 -1
  82. package/dist/web/components/modal-provider.d.ts +1 -1
  83. package/dist/web/data-llm.d.ts +1 -1
  84. package/dist/web/data-llm.test.js +8 -0
  85. package/dist/web/data-llm.test.js.map +1 -1
  86. package/dist/web/hooks/use-view-state.js +11 -9
  87. package/dist/web/hooks/use-view-state.js.map +1 -1
  88. package/dist/web/hooks/use-view-state.test.js +10 -0
  89. package/dist/web/hooks/use-view-state.test.js.map +1 -1
  90. package/dist/web/plugin/plugin.d.ts +6 -0
  91. package/dist/web/plugin/plugin.js +3 -3
  92. package/dist/web/plugin/plugin.js.map +1 -1
  93. package/package.json +21 -20
  94. package/dist/cli/resolve-views-dir.d.ts +0 -1
  95. package/dist/cli/resolve-views-dir.js.map +0 -1
@@ -1,17 +1,16 @@
1
+ import { hostFromUserAgent } from "../host.js";
1
2
  export function authToSecuritySchemes(auth) {
2
- if (auth === "public") {
3
- return [{ type: "noauth" }, { type: "oauth2" }];
4
- }
5
- if (auth === "required") {
6
- return [{ type: "oauth2" }];
7
- }
8
- return [{ type: "oauth2", scopes: auth.scopes }];
3
+ const oauth2 = {
4
+ type: "oauth2",
5
+ ...(auth.scopes?.length ? { scopes: auth.scopes } : {}),
6
+ };
7
+ return auth.allowsAnonymous ? [{ type: "noauth" }, oauth2] : [oauth2];
9
8
  }
10
9
  export function securitySchemesAllowAnonymous(schemes) {
11
10
  return !!schemes?.some((scheme) => scheme.type === "noauth");
12
11
  }
13
12
  export function evaluateSecuritySchemes(schemes, authInfo) {
14
- if (securitySchemesAllowAnonymous(schemes)) {
13
+ if (securitySchemesAllowAnonymous(schemes) && !authInfo) {
15
14
  return undefined;
16
15
  }
17
16
  const oauth2 = (schemes ?? []).filter((scheme) => scheme.type === "oauth2");
@@ -39,8 +38,7 @@ export function httpStatusForFailure(failure) {
39
38
  return failure.error === "insufficient_scope" ? 403 : 401;
40
39
  }
41
40
  export function clientPrefersInBandChallenge(userAgent) {
42
- const ua = (userAgent ?? "").toLowerCase();
43
- return ua.includes("openai") || ua.includes("chatgpt");
41
+ return hostFromUserAgent(userAgent) === "openai";
44
42
  }
45
43
  export function wwwAuthenticateHeader(failure, resourceMetadataUrl) {
46
44
  let header = `Bearer error="${failure.error}", error_description="${failure.description}"`;
@@ -52,4 +50,15 @@ export function wwwAuthenticateHeader(failure, resourceMetadataUrl) {
52
50
  }
53
51
  return header;
54
52
  }
53
+ export function inBandChallengeResult(failure, resourceMetadataUrl) {
54
+ return {
55
+ isError: true,
56
+ content: [{ type: "text", text: failure.description }],
57
+ _meta: {
58
+ "mcp/www_authenticate": [
59
+ wwwAuthenticateHeader(failure, resourceMetadataUrl),
60
+ ],
61
+ },
62
+ };
63
+ }
55
64
  //# sourceMappingURL=security-schemes.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"security-schemes.js","sourceRoot":"","sources":["../../../src/server/auth/security-schemes.ts"],"names":[],"mappings":"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","sourcesContent":["import type { AuthInfo } from \"../auth.js\";\nimport type { SecurityScheme, ToolAuth } from \"../server.js\";\n\nexport function authToSecuritySchemes(auth: ToolAuth): SecurityScheme[] {\n if (auth === \"public\") {\n return [{ type: \"noauth\" }, { type: \"oauth2\" }];\n }\n if (auth === \"required\") {\n return [{ type: \"oauth2\" }];\n }\n return [{ type: \"oauth2\", scopes: auth.scopes }];\n}\n\nexport function securitySchemesAllowAnonymous(\n schemes: SecurityScheme[] | undefined,\n): boolean {\n return !!schemes?.some((scheme) => scheme.type === \"noauth\");\n}\n\nexport type SchemeFailure = {\n error: \"invalid_token\" | \"insufficient_scope\";\n description: string;\n scopes: string[];\n};\n\nexport function evaluateSecuritySchemes(\n schemes: SecurityScheme[] | undefined,\n authInfo: AuthInfo | undefined,\n): SchemeFailure | undefined {\n if (securitySchemesAllowAnonymous(schemes)) {\n return undefined;\n }\n const oauth2 = (schemes ?? []).filter((scheme) => scheme.type === \"oauth2\");\n const required = oauth2.length ? oauth2 : [{ type: \"oauth2\" as const }];\n const scopes = [\n ...new Set(required.flatMap((scheme) => scheme.scopes ?? [])),\n ];\n if (!authInfo) {\n return {\n error: \"invalid_token\",\n description: \"Sign in to use this tool.\",\n scopes,\n };\n }\n const satisfied = required.some((scheme) =>\n (scheme.scopes ?? []).every((scope) => authInfo.scopes.includes(scope)),\n );\n return satisfied\n ? undefined\n : {\n error: \"insufficient_scope\",\n description: \"Missing required scope for this tool.\",\n scopes,\n };\n}\n\nexport function httpStatusForFailure(failure: SchemeFailure): 401 | 403 {\n return failure.error === \"insufficient_scope\" ? 403 : 401;\n}\n\nexport function clientPrefersInBandChallenge(\n userAgent: string | undefined,\n): boolean {\n const ua = (userAgent ?? \"\").toLowerCase();\n return ua.includes(\"openai\") || ua.includes(\"chatgpt\");\n}\n\nexport function wwwAuthenticateHeader(\n failure: SchemeFailure,\n resourceMetadataUrl?: string,\n): string {\n let header = `Bearer error=\"${failure.error}\", error_description=\"${failure.description}\"`;\n if (failure.scopes.length) {\n header += `, scope=\"${failure.scopes.join(\" \")}\"`;\n }\n if (resourceMetadataUrl) {\n header += `, resource_metadata=\"${resourceMetadataUrl}\"`;\n }\n return header;\n}\n"]}
1
+ {"version":3,"file":"security-schemes.js","sourceRoot":"","sources":["../../../src/server/auth/security-schemes.ts"],"names":[],"mappings":"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","sourcesContent":["import type { AuthInfo } from \"../auth.js\";\nimport { hostFromUserAgent } from \"../host.js\";\nimport type { SecurityScheme, ToolAuth } from \"../server.js\";\n\nexport function authToSecuritySchemes(auth: ToolAuth): SecurityScheme[] {\n const oauth2: SecurityScheme = {\n type: \"oauth2\",\n ...(auth.scopes?.length ? { scopes: auth.scopes } : {}),\n };\n return auth.allowsAnonymous ? [{ type: \"noauth\" }, oauth2] : [oauth2];\n}\n\nexport function securitySchemesAllowAnonymous(\n schemes: SecurityScheme[] | undefined,\n): boolean {\n return !!schemes?.some((scheme) => scheme.type === \"noauth\");\n}\n\nexport type SchemeFailure = {\n error: \"invalid_token\" | \"insufficient_scope\";\n description: string;\n scopes: string[];\n};\n\nexport function evaluateSecuritySchemes(\n schemes: SecurityScheme[] | undefined,\n authInfo: AuthInfo | undefined,\n): SchemeFailure | undefined {\n if (securitySchemesAllowAnonymous(schemes) && !authInfo) {\n return undefined;\n }\n const oauth2 = (schemes ?? []).filter((scheme) => scheme.type === \"oauth2\");\n const required = oauth2.length ? oauth2 : [{ type: \"oauth2\" as const }];\n const scopes = [\n ...new Set(required.flatMap((scheme) => scheme.scopes ?? [])),\n ];\n if (!authInfo) {\n return {\n error: \"invalid_token\",\n description: \"Sign in to use this tool.\",\n scopes,\n };\n }\n const satisfied = required.some((scheme) =>\n (scheme.scopes ?? []).every((scope) => authInfo.scopes.includes(scope)),\n );\n return satisfied\n ? undefined\n : {\n error: \"insufficient_scope\",\n description: \"Missing required scope for this tool.\",\n scopes,\n };\n}\n\nexport function httpStatusForFailure(failure: SchemeFailure): 401 | 403 {\n return failure.error === \"insufficient_scope\" ? 403 : 401;\n}\n\nexport function clientPrefersInBandChallenge(\n userAgent: string | undefined,\n): boolean {\n return hostFromUserAgent(userAgent) === \"openai\";\n}\n\nexport function wwwAuthenticateHeader(\n failure: SchemeFailure,\n resourceMetadataUrl?: string,\n): string {\n let header = `Bearer error=\"${failure.error}\", error_description=\"${failure.description}\"`;\n if (failure.scopes.length) {\n header += `, scope=\"${failure.scopes.join(\" \")}\"`;\n }\n if (resourceMetadataUrl) {\n header += `, resource_metadata=\"${resourceMetadataUrl}\"`;\n }\n return header;\n}\n\nexport function inBandChallengeResult(\n failure: SchemeFailure,\n resourceMetadataUrl?: string,\n) {\n return {\n isError: true,\n content: [{ type: \"text\" as const, text: failure.description }],\n _meta: {\n \"mcp/www_authenticate\": [\n wwwAuthenticateHeader(failure, resourceMetadataUrl),\n ],\n },\n };\n}\n"]}
@@ -9,15 +9,16 @@ describe("clientPrefersInBandChallenge", () => {
9
9
  });
10
10
  });
11
11
  describe("authToSecuritySchemes", () => {
12
- it("maps the three auth shorthands", () => {
13
- expect(authToSecuritySchemes("public")).toEqual([
12
+ it("maps the auth shorthand", () => {
13
+ expect(authToSecuritySchemes({ allowsAnonymous: true })).toEqual([
14
14
  { type: "noauth" },
15
15
  { type: "oauth2" },
16
16
  ]);
17
- expect(authToSecuritySchemes("required")).toEqual([{ type: "oauth2" }]);
17
+ expect(authToSecuritySchemes({})).toEqual([{ type: "oauth2" }]);
18
18
  expect(authToSecuritySchemes({ scopes: ["checkout"] })).toEqual([
19
19
  { type: "oauth2", scopes: ["checkout"] },
20
20
  ]);
21
+ expect(authToSecuritySchemes({ allowsAnonymous: true, scopes: ["checkout"] })).toEqual([{ type: "noauth" }, { type: "oauth2", scopes: ["checkout"] }]);
21
22
  });
22
23
  });
23
24
  const authInfo = (scopes) => ({
@@ -38,6 +39,15 @@ describe("evaluateSecuritySchemes", () => {
38
39
  const schemes = [{ type: "noauth" }, { type: "oauth2" }];
39
40
  expect(evaluateSecuritySchemes(schemes, undefined)).toBeUndefined();
40
41
  });
42
+ it("enforces oauth2 scopes on a public tool once a token is present", () => {
43
+ const schemes = [
44
+ { type: "noauth" },
45
+ { type: "oauth2", scopes: ["checkout"] },
46
+ ];
47
+ expect(evaluateSecuritySchemes(schemes, undefined)).toBeUndefined();
48
+ expect(evaluateSecuritySchemes(schemes, authInfo(["openid"]))?.error).toBe("insufficient_scope");
49
+ expect(evaluateSecuritySchemes(schemes, authInfo(["checkout"]))).toBeUndefined();
50
+ });
41
51
  it("fails an oauth2-only tool with no token (invalid_token / 401)", () => {
42
52
  expect(evaluateSecuritySchemes([{ type: "oauth2" }], undefined)?.error).toBe("invalid_token");
43
53
  });
@@ -1 +1 @@
1
- {"version":3,"file":"security-schemes.test.js","sourceRoot":"","sources":["../../../src/server/auth/security-schemes.test.ts"],"names":[],"mappings":"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","sourcesContent":["import { describe, expect, it } from \"vitest\";\nimport type { AuthInfo } from \"../auth.js\";\nimport type { SecurityScheme } from \"../server.js\";\nimport {\n authToSecuritySchemes,\n clientPrefersInBandChallenge,\n evaluateSecuritySchemes,\n securitySchemesAllowAnonymous,\n wwwAuthenticateHeader,\n} from \"./security-schemes.js\";\n\ndescribe(\"clientPrefersInBandChallenge\", () => {\n it(\"detects ChatGPT / OpenAI user agents\", () => {\n expect(clientPrefersInBandChallenge(\"openai-mcp/1.0\")).toBe(true);\n expect(clientPrefersInBandChallenge(\"ChatGPT\")).toBe(true);\n expect(clientPrefersInBandChallenge(\"Claude-User/1.0\")).toBe(false);\n expect(clientPrefersInBandChallenge(undefined)).toBe(false);\n });\n});\n\ndescribe(\"authToSecuritySchemes\", () => {\n it(\"maps the three auth shorthands\", () => {\n expect(authToSecuritySchemes(\"public\")).toEqual([\n { type: \"noauth\" },\n { type: \"oauth2\" },\n ]);\n expect(authToSecuritySchemes(\"required\")).toEqual([{ type: \"oauth2\" }]);\n expect(authToSecuritySchemes({ scopes: [\"checkout\"] })).toEqual([\n { type: \"oauth2\", scopes: [\"checkout\"] },\n ]);\n });\n});\n\nconst authInfo = (scopes: string[]): AuthInfo => ({\n token: \"t\",\n clientId: \"c\",\n scopes,\n});\n\ndescribe(\"securitySchemesAllowAnonymous\", () => {\n it(\"is true only when a noauth scheme is present\", () => {\n expect(securitySchemesAllowAnonymous([{ type: \"noauth\" }])).toBe(true);\n expect(\n securitySchemesAllowAnonymous([{ type: \"noauth\" }, { type: \"oauth2\" }]),\n ).toBe(true);\n expect(securitySchemesAllowAnonymous([{ type: \"oauth2\" }])).toBe(false);\n expect(securitySchemesAllowAnonymous(undefined)).toBe(false);\n });\n});\n\ndescribe(\"evaluateSecuritySchemes\", () => {\n it(\"lets anonymous callers through a noauth tool\", () => {\n const schemes: SecurityScheme[] = [{ type: \"noauth\" }, { type: \"oauth2\" }];\n expect(evaluateSecuritySchemes(schemes, undefined)).toBeUndefined();\n });\n\n it(\"fails an oauth2-only tool with no token (invalid_token / 401)\", () => {\n expect(\n evaluateSecuritySchemes([{ type: \"oauth2\" }], undefined)?.error,\n ).toBe(\"invalid_token\");\n });\n\n it(\"fails when the token is missing a required scope (insufficient_scope / 403)\", () => {\n const schemes: SecurityScheme[] = [\n { type: \"oauth2\", scopes: [\"checkout\"] },\n ];\n expect(evaluateSecuritySchemes(schemes, authInfo([\"openid\"]))?.error).toBe(\n \"insufficient_scope\",\n );\n });\n\n it(\"passes when the token carries every required scope\", () => {\n const schemes: SecurityScheme[] = [\n { type: \"oauth2\", scopes: [\"checkout\"] },\n ];\n expect(\n evaluateSecuritySchemes(schemes, authInfo([\"openid\", \"checkout\"])),\n ).toBeUndefined();\n });\n\n it(\"treats an undeclared tool as auth-required, satisfied by any valid token\", () => {\n expect(evaluateSecuritySchemes(undefined, undefined)?.error).toBe(\n \"invalid_token\",\n );\n expect(evaluateSecuritySchemes(undefined, authInfo([]))).toBeUndefined();\n });\n\n it(\"carries the tool's required scopes on the failure\", () => {\n const schemes: SecurityScheme[] = [\n { type: \"oauth2\", scopes: [\"checkout\"] },\n ];\n expect(evaluateSecuritySchemes(schemes, undefined)?.scopes).toEqual([\n \"checkout\",\n ]);\n expect(\n evaluateSecuritySchemes(schemes, authInfo([\"openid\"]))?.scopes,\n ).toEqual([\"checkout\"]);\n });\n});\n\ndescribe(\"wwwAuthenticateHeader\", () => {\n it(\"names the required scopes so the client can step up\", () => {\n const header = wwwAuthenticateHeader({\n error: \"insufficient_scope\",\n description: \"Missing required scope for this tool.\",\n scopes: [\"checkout\"],\n });\n expect(header).toContain('error=\"insufficient_scope\"');\n expect(header).toContain('scope=\"checkout\"');\n });\n\n it(\"omits the scope parameter when there are none\", () => {\n const header = wwwAuthenticateHeader({\n error: \"invalid_token\",\n description: \"Sign in to use this tool.\",\n scopes: [],\n });\n expect(header).not.toContain(\"scope=\");\n });\n});\n"]}
1
+ {"version":3,"file":"security-schemes.test.js","sourceRoot":"","sources":["../../../src/server/auth/security-schemes.test.ts"],"names":[],"mappings":"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","sourcesContent":["import { describe, expect, it } from \"vitest\";\nimport type { AuthInfo } from \"../auth.js\";\nimport type { SecurityScheme } from \"../server.js\";\nimport {\n authToSecuritySchemes,\n clientPrefersInBandChallenge,\n evaluateSecuritySchemes,\n securitySchemesAllowAnonymous,\n wwwAuthenticateHeader,\n} from \"./security-schemes.js\";\n\ndescribe(\"clientPrefersInBandChallenge\", () => {\n it(\"detects ChatGPT / OpenAI user agents\", () => {\n expect(clientPrefersInBandChallenge(\"openai-mcp/1.0\")).toBe(true);\n expect(clientPrefersInBandChallenge(\"ChatGPT\")).toBe(true);\n expect(clientPrefersInBandChallenge(\"Claude-User/1.0\")).toBe(false);\n expect(clientPrefersInBandChallenge(undefined)).toBe(false);\n });\n});\n\ndescribe(\"authToSecuritySchemes\", () => {\n it(\"maps the auth shorthand\", () => {\n expect(authToSecuritySchemes({ allowsAnonymous: true })).toEqual([\n { type: \"noauth\" },\n { type: \"oauth2\" },\n ]);\n expect(authToSecuritySchemes({})).toEqual([{ type: \"oauth2\" }]);\n expect(authToSecuritySchemes({ scopes: [\"checkout\"] })).toEqual([\n { type: \"oauth2\", scopes: [\"checkout\"] },\n ]);\n expect(\n authToSecuritySchemes({ allowsAnonymous: true, scopes: [\"checkout\"] }),\n ).toEqual([{ type: \"noauth\" }, { type: \"oauth2\", scopes: [\"checkout\"] }]);\n });\n});\n\nconst authInfo = (scopes: string[]): AuthInfo => ({\n token: \"t\",\n clientId: \"c\",\n scopes,\n});\n\ndescribe(\"securitySchemesAllowAnonymous\", () => {\n it(\"is true only when a noauth scheme is present\", () => {\n expect(securitySchemesAllowAnonymous([{ type: \"noauth\" }])).toBe(true);\n expect(\n securitySchemesAllowAnonymous([{ type: \"noauth\" }, { type: \"oauth2\" }]),\n ).toBe(true);\n expect(securitySchemesAllowAnonymous([{ type: \"oauth2\" }])).toBe(false);\n expect(securitySchemesAllowAnonymous(undefined)).toBe(false);\n });\n});\n\ndescribe(\"evaluateSecuritySchemes\", () => {\n it(\"lets anonymous callers through a noauth tool\", () => {\n const schemes: SecurityScheme[] = [{ type: \"noauth\" }, { type: \"oauth2\" }];\n expect(evaluateSecuritySchemes(schemes, undefined)).toBeUndefined();\n });\n\n it(\"enforces oauth2 scopes on a public tool once a token is present\", () => {\n const schemes: SecurityScheme[] = [\n { type: \"noauth\" },\n { type: \"oauth2\", scopes: [\"checkout\"] },\n ];\n expect(evaluateSecuritySchemes(schemes, undefined)).toBeUndefined();\n expect(evaluateSecuritySchemes(schemes, authInfo([\"openid\"]))?.error).toBe(\n \"insufficient_scope\",\n );\n expect(\n evaluateSecuritySchemes(schemes, authInfo([\"checkout\"])),\n ).toBeUndefined();\n });\n\n it(\"fails an oauth2-only tool with no token (invalid_token / 401)\", () => {\n expect(\n evaluateSecuritySchemes([{ type: \"oauth2\" }], undefined)?.error,\n ).toBe(\"invalid_token\");\n });\n\n it(\"fails when the token is missing a required scope (insufficient_scope / 403)\", () => {\n const schemes: SecurityScheme[] = [\n { type: \"oauth2\", scopes: [\"checkout\"] },\n ];\n expect(evaluateSecuritySchemes(schemes, authInfo([\"openid\"]))?.error).toBe(\n \"insufficient_scope\",\n );\n });\n\n it(\"passes when the token carries every required scope\", () => {\n const schemes: SecurityScheme[] = [\n { type: \"oauth2\", scopes: [\"checkout\"] },\n ];\n expect(\n evaluateSecuritySchemes(schemes, authInfo([\"openid\", \"checkout\"])),\n ).toBeUndefined();\n });\n\n it(\"treats an undeclared tool as auth-required, satisfied by any valid token\", () => {\n expect(evaluateSecuritySchemes(undefined, undefined)?.error).toBe(\n \"invalid_token\",\n );\n expect(evaluateSecuritySchemes(undefined, authInfo([]))).toBeUndefined();\n });\n\n it(\"carries the tool's required scopes on the failure\", () => {\n const schemes: SecurityScheme[] = [\n { type: \"oauth2\", scopes: [\"checkout\"] },\n ];\n expect(evaluateSecuritySchemes(schemes, undefined)?.scopes).toEqual([\n \"checkout\",\n ]);\n expect(\n evaluateSecuritySchemes(schemes, authInfo([\"openid\"]))?.scopes,\n ).toEqual([\"checkout\"]);\n });\n});\n\ndescribe(\"wwwAuthenticateHeader\", () => {\n it(\"names the required scopes so the client can step up\", () => {\n const header = wwwAuthenticateHeader({\n error: \"insufficient_scope\",\n description: \"Missing required scope for this tool.\",\n scopes: [\"checkout\"],\n });\n expect(header).toContain('error=\"insufficient_scope\"');\n expect(header).toContain('scope=\"checkout\"');\n });\n\n it(\"omits the scope parameter when there are none\", () => {\n const header = wwwAuthenticateHeader({\n error: \"invalid_token\",\n description: \"Sign in to use this tool.\",\n scopes: [],\n });\n expect(header).not.toContain(\"scope=\");\n });\n});\n"]}
@@ -1,5 +1,6 @@
1
1
  import type { Express } from "express";
2
2
  import type { SecurityScheme } from "../server.js";
3
3
  import type { OAuthConfig } from "./index.js";
4
+ export type ResourceMetadataUrlResolver = (getHeader: (key: string) => string | undefined) => string;
4
5
  /** Mounts the well-known OAuth metadata and bearer auth on `/mcp`. */
5
- export declare function setupOAuth(app: Express, config: OAuthConfig, schemesByTool: Map<string, SecurityScheme[] | undefined>): void;
6
+ export declare function setupOAuth(app: Express, config: OAuthConfig, schemesByTool: Map<string, SecurityScheme[] | undefined>): ResourceMetadataUrlResolver;
@@ -2,7 +2,7 @@ import { getOAuthProtectedResourceMetadataUrl, mcpAuthMetadataRouter, } from "@m
2
2
  import cors from "cors";
3
3
  import { optionalBearerAuth, requireBearerAuth } from "../auth.js";
4
4
  import { resolveServerOrigin } from "../requestOrigin.js";
5
- import { clientPrefersInBandChallenge, evaluateSecuritySchemes, httpStatusForFailure, securitySchemesAllowAnonymous, wwwAuthenticateHeader, } from "./security-schemes.js";
5
+ import { clientPrefersInBandChallenge, evaluateSecuritySchemes, httpStatusForFailure, inBandChallengeResult, securitySchemesAllowAnonymous, wwwAuthenticateHeader, } from "./security-schemes.js";
6
6
  import { createJwksVerifier } from "./verify.js";
7
7
  /** Mounts the well-known OAuth metadata and bearer auth on `/mcp`. */
8
8
  export function setupOAuth(app, config, schemesByTool) {
@@ -16,35 +16,7 @@ export function setupOAuth(app, config, schemesByTool) {
16
16
  const optional = optionalBearerAuth(options);
17
17
  return (req, res, next) => (acceptsAnonymous() ? optional : required)(req, res, next);
18
18
  };
19
- const perToolGate = (resourceMetadataUrl) => (req, res, next) => {
20
- const body = req.body;
21
- const tool = body?.params?.name;
22
- if (body?.method !== "tools/call" || !tool || !schemesByTool.has(tool)) {
23
- return next();
24
- }
25
- const failure = evaluateSecuritySchemes(schemesByTool.get(tool), req.auth);
26
- if (!failure) {
27
- return next();
28
- }
29
- const challenge = wwwAuthenticateHeader(failure, resourceMetadataUrl(req));
30
- if (clientPrefersInBandChallenge(req.get("user-agent"))) {
31
- res.json({
32
- jsonrpc: "2.0",
33
- id: body.id ?? null,
34
- result: {
35
- content: [{ type: "text", text: failure.description }],
36
- isError: true,
37
- _meta: { "mcp/www_authenticate": [challenge] },
38
- },
39
- });
40
- return;
41
- }
42
- res.set("WWW-Authenticate", challenge);
43
- res.status(httpStatusForFailure(failure)).json({
44
- error: failure.error,
45
- error_description: failure.description,
46
- });
47
- };
19
+ let resourceMetadataUrl;
48
20
  // baseUrl known at boot: bake the resource URLs once, no Host-header trust.
49
21
  if (config.baseUrl !== undefined) {
50
22
  let baseUrl;
@@ -54,38 +26,67 @@ export function setupOAuth(app, config, schemesByTool) {
54
26
  catch {
55
27
  throw new Error(`oauth.baseUrl must be a valid absolute URL, got: ${JSON.stringify(config.baseUrl)}`);
56
28
  }
57
- const resourceMetadataUrl = getOAuthProtectedResourceMetadataUrl(baseUrl);
58
29
  app.use(mcpAuthMetadataRouter({
59
30
  oauthMetadata: config.oauthMetadata,
60
31
  resourceServerUrl: baseUrl,
61
32
  scopesSupported: config.scopesSupported,
62
33
  }));
63
- app.use("/mcp", bearer({
64
- verifier,
65
- requiredScopes: config.requiredScopes,
66
- resourceMetadataUrl,
67
- }));
68
- app.use("/mcp", perToolGate(() => resourceMetadataUrl));
69
- return;
34
+ const url = getOAuthProtectedResourceMetadataUrl(baseUrl);
35
+ resourceMetadataUrl = () => url;
70
36
  }
71
- // No baseUrl: resolve the resource origin per request from forwarded headers
72
- // (same precedence the framework uses for view serverUrl). Origin headers are
73
- // pathless, so the PRM path stays at root, matching the SDK's static layout.
74
- const resolveOrigin = (req) => new URL(resolveServerOrigin((key) => req.get(key)));
75
- const resourceMetadataUrl = (req) => getOAuthProtectedResourceMetadataUrl(resolveOrigin(req));
76
- app.use("/.well-known/oauth-authorization-server", cors(), (_req, res) => void res.json(config.oauthMetadata));
77
- app.use("/.well-known/oauth-protected-resource", cors(), (req, res) => {
78
- res.json({
79
- resource: resolveOrigin(req).href,
80
- authorization_servers: [config.oauthMetadata.issuer],
81
- scopes_supported: config.scopesSupported,
37
+ else {
38
+ // No baseUrl: resolve the resource origin per request from forwarded headers
39
+ // (same precedence the framework uses for view serverUrl). Origin headers are
40
+ // pathless, so the PRM path stays at root, matching the SDK's static layout.
41
+ const resolveOrigin = (getHeader) => new URL(resolveServerOrigin(getHeader));
42
+ app.use("/.well-known/oauth-authorization-server", cors(), (_req, res) => void res.json(config.oauthMetadata));
43
+ app.use("/.well-known/oauth-protected-resource", cors(), (req, res) => {
44
+ res.json({
45
+ resource: resolveOrigin((key) => req.get(key)).href,
46
+ authorization_servers: [config.oauthMetadata.issuer],
47
+ scopes_supported: config.scopesSupported,
48
+ });
82
49
  });
83
- });
50
+ resourceMetadataUrl = (getHeader) => getOAuthProtectedResourceMetadataUrl(resolveOrigin(getHeader));
51
+ }
84
52
  app.use("/mcp", (req, res, next) => bearer({
85
53
  verifier,
86
54
  requiredScopes: config.requiredScopes,
87
- resourceMetadataUrl: resourceMetadataUrl(req),
55
+ resourceMetadataUrl: resourceMetadataUrl((key) => req.get(key)),
88
56
  })(req, res, next));
89
- app.use("/mcp", perToolGate(resourceMetadataUrl));
57
+ app.use("/mcp", (req, res, next) => {
58
+ const body = req.body;
59
+ const calls = Array.isArray(body) ? body : [body];
60
+ const auth = req.auth;
61
+ let failure;
62
+ for (const call of calls) {
63
+ const tool = call?.params?.name;
64
+ if (call?.method === "tools/call" && tool && schemesByTool.has(tool)) {
65
+ failure = evaluateSecuritySchemes(schemesByTool.get(tool), auth);
66
+ if (failure) {
67
+ break;
68
+ }
69
+ }
70
+ }
71
+ if (!failure) {
72
+ return next();
73
+ }
74
+ const url = resourceMetadataUrl((key) => req.get(key));
75
+ if (!Array.isArray(body) &&
76
+ clientPrefersInBandChallenge(req.get("user-agent"))) {
77
+ res.json({
78
+ jsonrpc: "2.0",
79
+ id: body?.id ?? null,
80
+ result: inBandChallengeResult(failure, url),
81
+ });
82
+ return;
83
+ }
84
+ res.set("WWW-Authenticate", wwwAuthenticateHeader(failure, url));
85
+ res.status(httpStatusForFailure(failure)).json({
86
+ error: failure.error,
87
+ error_description: failure.description,
88
+ });
89
+ });
90
+ return resourceMetadataUrl;
90
91
  }
91
92
  //# sourceMappingURL=setup.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"setup.js","sourceRoot":"","sources":["../../../src/server/auth/setup.ts"],"names":[],"mappings":"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","sourcesContent":["import type { BearerAuthMiddlewareOptions } from \"@modelcontextprotocol/sdk/server/auth/middleware/bearerAuth.js\";\nimport {\n getOAuthProtectedResourceMetadataUrl,\n mcpAuthMetadataRouter,\n} from \"@modelcontextprotocol/sdk/server/auth/router.js\";\nimport cors from \"cors\";\nimport type { Express, Request, RequestHandler } from \"express\";\nimport type { AuthInfo } from \"../auth.js\";\nimport { optionalBearerAuth, requireBearerAuth } from \"../auth.js\";\nimport { resolveServerOrigin } from \"../requestOrigin.js\";\nimport type { SecurityScheme } from \"../server.js\";\nimport type { OAuthConfig } from \"./index.js\";\nimport {\n clientPrefersInBandChallenge,\n evaluateSecuritySchemes,\n httpStatusForFailure,\n securitySchemesAllowAnonymous,\n wwwAuthenticateHeader,\n} from \"./security-schemes.js\";\nimport { createJwksVerifier } from \"./verify.js\";\n\n/** Mounts the well-known OAuth metadata and bearer auth on `/mcp`. */\nexport function setupOAuth(\n app: Express,\n config: OAuthConfig,\n schemesByTool: Map<string, SecurityScheme[] | undefined>,\n): void {\n if (!config.verify?.issuer) {\n throw new Error(\"oauth.verify requires an `issuer`\");\n }\n\n const acceptsAnonymous = () =>\n [...schemesByTool.values()].some(securitySchemesAllowAnonymous);\n const verifier = createJwksVerifier(config.verify);\n const bearer = (options: BearerAuthMiddlewareOptions): RequestHandler => {\n const required = requireBearerAuth(options);\n const optional = optionalBearerAuth(options);\n return (req, res, next) =>\n (acceptsAnonymous() ? optional : required)(req, res, next);\n };\n\n const perToolGate =\n (resourceMetadataUrl: (req: Request) => string): RequestHandler =>\n (req, res, next) => {\n const body = req.body as\n | {\n id?: string | number | null;\n method?: string;\n params?: { name?: string };\n }\n | undefined;\n const tool = body?.params?.name;\n if (body?.method !== \"tools/call\" || !tool || !schemesByTool.has(tool)) {\n return next();\n }\n const failure = evaluateSecuritySchemes(\n schemesByTool.get(tool),\n (req as Request & { auth?: AuthInfo }).auth,\n );\n if (!failure) {\n return next();\n }\n const challenge = wwwAuthenticateHeader(\n failure,\n resourceMetadataUrl(req),\n );\n if (clientPrefersInBandChallenge(req.get(\"user-agent\"))) {\n res.json({\n jsonrpc: \"2.0\",\n id: body.id ?? null,\n result: {\n content: [{ type: \"text\", text: failure.description }],\n isError: true,\n _meta: { \"mcp/www_authenticate\": [challenge] },\n },\n });\n return;\n }\n res.set(\"WWW-Authenticate\", challenge);\n res.status(httpStatusForFailure(failure)).json({\n error: failure.error,\n error_description: failure.description,\n });\n };\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 const resourceMetadataUrl = getOAuthProtectedResourceMetadataUrl(baseUrl);\n app.use(\n mcpAuthMetadataRouter({\n oauthMetadata: config.oauthMetadata,\n resourceServerUrl: baseUrl,\n scopesSupported: config.scopesSupported,\n }),\n );\n app.use(\n \"/mcp\",\n bearer({\n verifier,\n requiredScopes: config.requiredScopes,\n resourceMetadataUrl,\n }),\n );\n app.use(\n \"/mcp\",\n perToolGate(() => resourceMetadataUrl),\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 const resourceMetadataUrl = (req: Request) =>\n getOAuthProtectedResourceMetadataUrl(resolveOrigin(req));\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 bearer({\n verifier,\n requiredScopes: config.requiredScopes,\n resourceMetadataUrl: resourceMetadataUrl(req),\n })(req, res, next),\n );\n app.use(\"/mcp\", perToolGate(resourceMetadataUrl));\n}\n"]}
1
+ {"version":3,"file":"setup.js","sourceRoot":"","sources":["../../../src/server/auth/setup.ts"],"names":[],"mappings":"AACA,OAAO,EACL,oCAAoC,EACpC,qBAAqB,GACtB,MAAM,iDAAiD,CAAC;AACzD,OAAO,IAAI,MAAM,MAAM,CAAC;AAGxB,OAAO,EAAE,kBAAkB,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC;AACnE,OAAO,EAAE,mBAAmB,EAAE,MAAM,qBAAqB,CAAC;AAG1D,OAAO,EACL,4BAA4B,EAC5B,uBAAuB,EACvB,oBAAoB,EACpB,qBAAqB,EAErB,6BAA6B,EAC7B,qBAAqB,GACtB,MAAM,uBAAuB,CAAC;AAC/B,OAAO,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AAMjD,sEAAsE;AACtE,MAAM,UAAU,UAAU,CACxB,GAAY,EACZ,MAAmB,EACnB,aAAwD;IAExD,IAAI,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC;QAC3B,MAAM,IAAI,KAAK,CAAC,mCAAmC,CAAC,CAAC;IACvD,CAAC;IAED,MAAM,gBAAgB,GAAG,GAAG,EAAE,CAC5B,CAAC,GAAG,aAAa,CAAC,MAAM,EAAE,CAAC,CAAC,IAAI,CAAC,6BAA6B,CAAC,CAAC;IAClE,MAAM,QAAQ,GAAG,kBAAkB,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;IACnD,MAAM,MAAM,GAAG,CAAC,OAAoC,EAAkB,EAAE;QACtE,MAAM,QAAQ,GAAG,iBAAiB,CAAC,OAAO,CAAC,CAAC;QAC5C,MAAM,QAAQ,GAAG,kBAAkB,CAAC,OAAO,CAAC,CAAC;QAC7C,OAAO,CAAC,GAAG,EAAE,GAAG,EAAE,IAAI,EAAE,EAAE,CACxB,CAAC,gBAAgB,EAAE,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,GAAG,EAAE,GAAG,EAAE,IAAI,CAAC,CAAC;IAC/D,CAAC,CAAC;IAEF,IAAI,mBAAgD,CAAC;IAErD,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,MAAM,GAAG,GAAG,oCAAoC,CAAC,OAAO,CAAC,CAAC;QAC1D,mBAAmB,GAAG,GAAG,EAAE,CAAC,GAAG,CAAC;IAClC,CAAC;SAAM,CAAC;QACN,6EAA6E;QAC7E,8EAA8E;QAC9E,6EAA6E;QAC7E,MAAM,aAAa,GAAG,CAAC,SAA8C,EAAE,EAAE,CACvE,IAAI,GAAG,CAAC,mBAAmB,CAAC,SAAS,CAAC,CAAC,CAAC;QAC1C,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;QACF,GAAG,CAAC,GAAG,CAAC,uCAAuC,EAAE,IAAI,EAAE,EAAE,CAAC,GAAG,EAAE,GAAG,EAAE,EAAE;YACpE,GAAG,CAAC,IAAI,CAAC;gBACP,QAAQ,EAAE,aAAa,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI;gBACnD,qBAAqB,EAAE,CAAC,MAAM,CAAC,aAAa,CAAC,MAAM,CAAC;gBACpD,gBAAgB,EAAE,MAAM,CAAC,eAAe;aACzC,CAAC,CAAC;QACL,CAAC,CAAC,CAAC;QACH,mBAAmB,GAAG,CAAC,SAAS,EAAE,EAAE,CAClC,oCAAoC,CAAC,aAAa,CAAC,SAAS,CAAC,CAAC,CAAC;IACnE,CAAC;IAED,GAAG,CAAC,GAAG,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,GAAG,EAAE,IAAI,EAAE,EAAE,CACjC,MAAM,CAAC;QACL,QAAQ;QACR,cAAc,EAAE,MAAM,CAAC,cAAc;QACrC,mBAAmB,EAAE,mBAAmB,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;KAChE,CAAC,CAAC,GAAG,EAAE,GAAG,EAAE,IAAI,CAAC,CACnB,CAAC;IAEF,GAAG,CAAC,GAAG,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,GAAG,EAAE,IAAI,EAAE,EAAE;QAMjC,MAAM,IAAI,GAAG,GAAG,CAAC,IAAiC,CAAC;QACnD,MAAM,KAAK,GAAG,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC;QAClD,MAAM,IAAI,GAAI,GAAqC,CAAC,IAAI,CAAC;QACzD,IAAI,OAAkC,CAAC;QACvC,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;YACzB,MAAM,IAAI,GAAG,IAAI,EAAE,MAAM,EAAE,IAAI,CAAC;YAChC,IAAI,IAAI,EAAE,MAAM,KAAK,YAAY,IAAI,IAAI,IAAI,aAAa,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC;gBACrE,OAAO,GAAG,uBAAuB,CAAC,aAAa,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,IAAI,CAAC,CAAC;gBACjE,IAAI,OAAO,EAAE,CAAC;oBACZ,MAAM;gBACR,CAAC;YACH,CAAC;QACH,CAAC;QACD,IAAI,CAAC,OAAO,EAAE,CAAC;YACb,OAAO,IAAI,EAAE,CAAC;QAChB,CAAC;QACD,MAAM,GAAG,GAAG,mBAAmB,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC;QACvD,IACE,CAAC,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC;YACpB,4BAA4B,CAAC,GAAG,CAAC,GAAG,CAAC,YAAY,CAAC,CAAC,EACnD,CAAC;YACD,GAAG,CAAC,IAAI,CAAC;gBACP,OAAO,EAAE,KAAK;gBACd,EAAE,EAAE,IAAI,EAAE,EAAE,IAAI,IAAI;gBACpB,MAAM,EAAE,qBAAqB,CAAC,OAAO,EAAE,GAAG,CAAC;aAC5C,CAAC,CAAC;YACH,OAAO;QACT,CAAC;QACD,GAAG,CAAC,GAAG,CAAC,kBAAkB,EAAE,qBAAqB,CAAC,OAAO,EAAE,GAAG,CAAC,CAAC,CAAC;QACjE,GAAG,CAAC,MAAM,CAAC,oBAAoB,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC;YAC7C,KAAK,EAAE,OAAO,CAAC,KAAK;YACpB,iBAAiB,EAAE,OAAO,CAAC,WAAW;SACvC,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;IAEH,OAAO,mBAAmB,CAAC;AAC7B,CAAC","sourcesContent":["import type { BearerAuthMiddlewareOptions } from \"@modelcontextprotocol/sdk/server/auth/middleware/bearerAuth.js\";\nimport {\n getOAuthProtectedResourceMetadataUrl,\n mcpAuthMetadataRouter,\n} from \"@modelcontextprotocol/sdk/server/auth/router.js\";\nimport cors from \"cors\";\nimport type { Express, Request, RequestHandler } from \"express\";\nimport type { AuthInfo } from \"../auth.js\";\nimport { optionalBearerAuth, requireBearerAuth } from \"../auth.js\";\nimport { resolveServerOrigin } from \"../requestOrigin.js\";\nimport type { SecurityScheme } from \"../server.js\";\nimport type { OAuthConfig } from \"./index.js\";\nimport {\n clientPrefersInBandChallenge,\n evaluateSecuritySchemes,\n httpStatusForFailure,\n inBandChallengeResult,\n type SchemeFailure,\n securitySchemesAllowAnonymous,\n wwwAuthenticateHeader,\n} from \"./security-schemes.js\";\nimport { createJwksVerifier } from \"./verify.js\";\n\nexport type ResourceMetadataUrlResolver = (\n getHeader: (key: string) => string | undefined,\n) => string;\n\n/** Mounts the well-known OAuth metadata and bearer auth on `/mcp`. */\nexport function setupOAuth(\n app: Express,\n config: OAuthConfig,\n schemesByTool: Map<string, SecurityScheme[] | undefined>,\n): ResourceMetadataUrlResolver {\n if (!config.verify?.issuer) {\n throw new Error(\"oauth.verify requires an `issuer`\");\n }\n\n const acceptsAnonymous = () =>\n [...schemesByTool.values()].some(securitySchemesAllowAnonymous);\n const verifier = createJwksVerifier(config.verify);\n const bearer = (options: BearerAuthMiddlewareOptions): RequestHandler => {\n const required = requireBearerAuth(options);\n const optional = optionalBearerAuth(options);\n return (req, res, next) =>\n (acceptsAnonymous() ? optional : required)(req, res, next);\n };\n\n let resourceMetadataUrl: ResourceMetadataUrlResolver;\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 const url = getOAuthProtectedResourceMetadataUrl(baseUrl);\n resourceMetadataUrl = () => url;\n } else {\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 = (getHeader: (key: string) => string | undefined) =>\n new URL(resolveServerOrigin(getHeader));\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((key) => req.get(key)).href,\n authorization_servers: [config.oauthMetadata.issuer],\n scopes_supported: config.scopesSupported,\n });\n });\n resourceMetadataUrl = (getHeader) =>\n getOAuthProtectedResourceMetadataUrl(resolveOrigin(getHeader));\n }\n\n app.use(\"/mcp\", (req, res, next) =>\n bearer({\n verifier,\n requiredScopes: config.requiredScopes,\n resourceMetadataUrl: resourceMetadataUrl((key) => req.get(key)),\n })(req, res, next),\n );\n\n app.use(\"/mcp\", (req, res, next) => {\n type Call = {\n id?: string | number | null;\n method?: string;\n params?: { name?: string };\n };\n const body = req.body as Call | Call[] | undefined;\n const calls = Array.isArray(body) ? body : [body];\n const auth = (req as Request & { auth?: AuthInfo }).auth;\n let failure: SchemeFailure | undefined;\n for (const call of calls) {\n const tool = call?.params?.name;\n if (call?.method === \"tools/call\" && tool && schemesByTool.has(tool)) {\n failure = evaluateSecuritySchemes(schemesByTool.get(tool), auth);\n if (failure) {\n break;\n }\n }\n }\n if (!failure) {\n return next();\n }\n const url = resourceMetadataUrl((key) => req.get(key));\n if (\n !Array.isArray(body) &&\n clientPrefersInBandChallenge(req.get(\"user-agent\"))\n ) {\n res.json({\n jsonrpc: \"2.0\",\n id: body?.id ?? null,\n result: inBandChallengeResult(failure, url),\n });\n return;\n }\n res.set(\"WWW-Authenticate\", wwwAuthenticateHeader(failure, url));\n res.status(httpStatusForFailure(failure)).json({\n error: failure.error,\n error_description: failure.description,\n });\n });\n\n return resourceMetadataUrl;\n}\n"]}
@@ -183,7 +183,7 @@ async function bootMixedServer(jwksUri) {
183
183
  name: "public-whoami",
184
184
  description: "Public.",
185
185
  inputSchema: {},
186
- auth: "public",
186
+ auth: { allowsAnonymous: true },
187
187
  }, (_args, extra) => ({
188
188
  content: [{ type: "text", text: extra.authInfo?.clientId ?? "anon" }],
189
189
  }))
@@ -191,7 +191,13 @@ async function bootMixedServer(jwksUri) {
191
191
  name: "private-whoami",
192
192
  description: "Private.",
193
193
  inputSchema: {},
194
- auth: "required",
194
+ auth: {},
195
+ }, (_args, extra) => ({
196
+ content: [{ type: "text", text: extra.authInfo?.clientId ?? "anon" }],
197
+ }));
198
+ server.registerTool("legacy-whoami", {
199
+ description: "Registered via the legacy string overload.",
200
+ inputSchema: {},
195
201
  }, (_args, extra) => ({
196
202
  content: [{ type: "text", text: extra.authInfo?.clientId ?? "anon" }],
197
203
  }));
@@ -265,9 +271,30 @@ describe("mixed-auth door", () => {
265
271
  },
266
272
  ]),
267
273
  });
274
+ expect(res.status).toBe(401);
275
+ const wwwAuthenticate = res.headers.get("www-authenticate") ?? "";
276
+ expect(wwwAuthenticate).toMatch(/^Bearer /);
277
+ expect(wwwAuthenticate).toContain("resource_metadata=");
268
278
  const text = await res.text();
269
279
  expect(text).not.toContain("client-1");
270
- expect(text).toContain("Sign in to use this tool.");
280
+ });
281
+ it("gates a tool registered via the legacy string overload (secure default)", async () => {
282
+ const { jwksUri } = await startJwks();
283
+ const base = await bootMixedServer(jwksUri);
284
+ const res = await fetch(`${base}/mcp`, {
285
+ method: "POST",
286
+ headers: {
287
+ "Content-Type": "application/json",
288
+ Accept: "application/json, text/event-stream",
289
+ },
290
+ body: JSON.stringify({
291
+ jsonrpc: "2.0",
292
+ id: 1,
293
+ method: "tools/call",
294
+ params: { name: "legacy-whoami", arguments: {} },
295
+ }),
296
+ });
297
+ expect(res.status).toBe(401);
271
298
  });
272
299
  it("returns 401 + WWW-Authenticate for a protected tool while anonymous", async () => {
273
300
  const { jwksUri } = await startJwks();
@@ -316,6 +343,30 @@ describe("mixed-auth door", () => {
316
343
  expect(challenge[0]).toMatch(/error="invalid_token"/);
317
344
  expect(challenge[0]).toMatch(/resource_metadata=/);
318
345
  });
346
+ it("fails closed on a ChatGPT batch (transport 401, no in-band challenge)", async () => {
347
+ const { jwksUri } = await startJwks();
348
+ const base = await bootMixedServer(jwksUri);
349
+ const res = await fetch(`${base}/mcp`, {
350
+ method: "POST",
351
+ headers: {
352
+ "Content-Type": "application/json",
353
+ Accept: "application/json, text/event-stream",
354
+ "User-Agent": "openai-mcp/1.0",
355
+ },
356
+ body: JSON.stringify([
357
+ {
358
+ jsonrpc: "2.0",
359
+ id: 1,
360
+ method: "tools/call",
361
+ params: { name: "private-whoami", arguments: {} },
362
+ },
363
+ ]),
364
+ });
365
+ expect(res.status).toBe(401);
366
+ expect(res.headers.get("www-authenticate")).toMatch(/^Bearer /);
367
+ const text = await res.text();
368
+ expect(text).not.toContain("client-1");
369
+ });
319
370
  });
320
371
  async function bootScopedServer(jwksUri) {
321
372
  const { createApp } = await import("../express.js");
@@ -385,28 +436,22 @@ describe("per-tool scope enforcement in fully-authenticated mode", () => {
385
436
  });
386
437
  });
387
438
  describe("auth shorthand validation", () => {
388
- const oauth = {
389
- baseUrl: "https://app.example.test",
390
- oauthMetadata: {
391
- issuer: ISSUER,
392
- authorization_endpoint: `${ISSUER}/authorize`,
393
- token_endpoint: `${ISSUER}/token`,
394
- response_types_supported: ["code"],
395
- },
396
- verify: { issuer: ISSUER, audience: AUDIENCE },
397
- };
398
- it("throws when `auth` is set but no oauth provider is configured", () => {
399
- expect(() => new McpServer({ name: "t", version: "0" }).registerTool({ name: "x", inputSchema: {}, auth: "required" }, () => ({ content: [{ type: "text", text: "" }] }))).toThrow(/no `oauth` provider/);
439
+ it("throws when `auth` requires sign-in but no oauth provider is configured", () => {
440
+ expect(() => new McpServer({ name: "t", version: "0" }).registerTool({ name: "x", inputSchema: {}, auth: {} }, () => ({ content: [{ type: "text", text: "" }] }))).toThrow(/no `oauth` provider/);
400
441
  });
401
- it("throws when both `auth` and `securitySchemes` are set", () => {
402
- expect(() => new McpServer({ name: "t", version: "0" }, undefined, {
403
- oauth,
404
- }).registerTool({
442
+ it("throws when `auth` sets scopes but no oauth provider is configured", () => {
443
+ expect(() => new McpServer({ name: "t", version: "0" }).registerTool({ name: "x", inputSchema: {}, auth: { scopes: ["checkout"] } }, () => ({ content: [{ type: "text", text: "" }] }))).toThrow(/no `oauth` provider/);
444
+ });
445
+ it("allows `auth: { allowsAnonymous: true }` without an oauth provider", () => {
446
+ expect(() => new McpServer({ name: "t", version: "0" }).registerTool({ name: "x", inputSchema: {}, auth: { allowsAnonymous: true } }, () => ({ content: [{ type: "text", text: "" }] }))).not.toThrow();
447
+ });
448
+ it("prefers `securitySchemes` over `auth` when both are set (untyped path)", () => {
449
+ expect(() => new McpServer({ name: "t", version: "0" }).registerTool({
405
450
  name: "x",
406
451
  inputSchema: {},
407
- auth: "required",
452
+ auth: {},
408
453
  securitySchemes: [{ type: "oauth2" }],
409
- }, () => ({ content: [{ type: "text", text: "" }] }))).toThrow(/use one/);
454
+ }, () => ({ content: [{ type: "text", text: "" }] }))).not.toThrow();
410
455
  });
411
456
  });
412
457
  describe("oauth config validation", () => {