oauth-provider.test.ts 4.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102
  1. import { test, expect, describe } from "bun:test"
  2. import { determineScope } from "@modelcontextprotocol/sdk/client/auth.js"
  3. import { McpOAuthProvider, OAUTH_CALLBACK_PORT, OAUTH_CALLBACK_PATH } from "../../src/mcp/oauth-provider"
  4. import type { McpAuth } from "../../src/mcp/auth"
  5. // Stub auth — only synchronous getters are exercised in these tests
  6. const stubAuth = {} as McpAuth.Interface
  7. const makeProvider = (config: ConstructorParameters<typeof McpOAuthProvider>[2]) =>
  8. new McpOAuthProvider("test-server", "https://mcp.example.com/mcp", config, { onRedirect: async () => {} }, stubAuth)
  9. describe("McpOAuthProvider.redirectUrl", () => {
  10. test("defaults to 127.0.0.1:19876/mcp/oauth/callback", () => {
  11. const provider = makeProvider({})
  12. expect(provider.redirectUrl).toBe(`http://127.0.0.1:${OAUTH_CALLBACK_PORT}${OAUTH_CALLBACK_PATH}`)
  13. })
  14. test("uses callbackPort when set", () => {
  15. const provider = makeProvider({ callbackPort: 6620 })
  16. expect(provider.redirectUrl).toBe(`http://127.0.0.1:6620${OAUTH_CALLBACK_PATH}`)
  17. })
  18. test("redirectUri takes precedence over callbackPort", () => {
  19. const provider = makeProvider({
  20. callbackPort: 6620,
  21. redirectUri: "http://127.0.0.1:9999/custom/callback",
  22. })
  23. expect(provider.redirectUrl).toBe("http://127.0.0.1:9999/custom/callback")
  24. })
  25. test("uses explicit redirectUri when set without callbackPort", () => {
  26. const provider = makeProvider({ redirectUri: "http://127.0.0.1:8080/oauth/callback" })
  27. expect(provider.redirectUrl).toBe("http://127.0.0.1:8080/oauth/callback")
  28. })
  29. })
  30. describe("McpOAuthProvider.clientMetadata", () => {
  31. test("includes redirect_uris from redirectUrl", () => {
  32. const provider = makeProvider({ callbackPort: 6620 })
  33. expect(provider.clientMetadata.redirect_uris).toEqual([`http://127.0.0.1:6620${OAUTH_CALLBACK_PATH}`])
  34. })
  35. test("includes scope when set in config", () => {
  36. const provider = makeProvider({ scope: "openid offline_access" })
  37. expect(provider.clientMetadata.scope).toBe("openid offline_access")
  38. })
  39. test("omits scope when not set in config", () => {
  40. const provider = makeProvider({})
  41. expect(provider.clientMetadata.scope).toBeUndefined()
  42. })
  43. test("sets token_endpoint_auth_method to client_secret_post when clientSecret provided", () => {
  44. const provider = makeProvider({ clientSecret: "secret" })
  45. expect(provider.clientMetadata.token_endpoint_auth_method).toBe("client_secret_post")
  46. })
  47. test("sets token_endpoint_auth_method to none when no clientSecret", () => {
  48. const provider = makeProvider({})
  49. expect(provider.clientMetadata.token_endpoint_auth_method).toBe("none")
  50. })
  51. })
  52. describe("MCP OAuth scope selection", () => {
  53. test("adds offline_access when the authorization server and client support refresh tokens", () => {
  54. expect(
  55. determineScope({
  56. resourceMetadata: {
  57. resource: "https://mcp.example.com/mcp",
  58. scopes_supported: ["resource.read"],
  59. },
  60. authServerMetadata: {
  61. issuer: "https://auth.example.com",
  62. authorization_endpoint: "https://auth.example.com/authorize",
  63. token_endpoint: "https://auth.example.com/token",
  64. response_types_supported: ["code"],
  65. scopes_supported: ["resource.read", "offline_access"],
  66. },
  67. clientMetadata: makeProvider({}).clientMetadata,
  68. }),
  69. ).toBe("resource.read offline_access")
  70. })
  71. test("does not add unsupported authorization server scopes", () => {
  72. expect(
  73. determineScope({
  74. resourceMetadata: {
  75. resource: "https://mcp.example.com/mcp",
  76. scopes_supported: ["resource.read"],
  77. },
  78. authServerMetadata: {
  79. issuer: "https://auth.example.com",
  80. authorization_endpoint: "https://auth.example.com/authorize",
  81. token_endpoint: "https://auth.example.com/token",
  82. response_types_supported: ["code"],
  83. scopes_supported: ["resource.read"],
  84. },
  85. clientMetadata: makeProvider({}).clientMetadata,
  86. }),
  87. ).toBe("resource.read")
  88. })
  89. })