MCP: generated endpoints doesn't follow .well-known spec #1889

Closed
opened 2026-03-13 09:10:58 -05:00 by GiteaMirror · 1 comment
Owner

Originally created by @paoloricciuti on GitHub (Sep 9, 2025).

Is this suited for github?

  • Yes, this is suited for github

To Reproduce

The mcp plugin generates endpoints to serve the authorization server and the protected resource metadata (the latter is also used in the withMcpAuth helper to return the right WWW-authenticate header). However the docs says the user also need to create their own route for those two endpoints.

While testing I realised why that is actually needed: the mcp client respects the WWW-authenticate URL so when the withMcpAuth returns /api/auth/.well-known/oauth-protected-resource it actually fetches that endpoint for the protected resource metadata. But then despite the authorization server being {base_url}/api/auth it doesn't try to fetch {base_url}/api/auth/.well-known/oauth-authorization-server and goes straight for {base_url}/.well-known/oauth-authorization-server (notice the missing api/auth at the beginning.

Initially I thought this was a bug in the official sdk but doing some research I found out that the .well-known route has an actual specification and that per spec the .well-known part of the path MUST be at the start of the pathname.

This is problematic because the endpoints generated from a plugin can only be nested within the base path (which makes sense) but it means .well-known endpoints aren't spec compliant.

I can work on a PR to fix this but I'm not sure about what's the best strategy for this...should we maybe update the handlers and special case .well-known endpoints to not start from base_path? Or maybe we should add a configuration option to better-call to allow for "out of base path endpoints"?

Not really sure what the best bet is. What do you think?

Current vs. Expected behavior

Endpoints created by the plugins should respect the .well-known spec and allow the mcp plugin to be config-less without having the user create a separate and duplicate route.

What version of Better Auth are you using?

1.3.9

System info

{
  "system": {
    "platform": "darwin",
    "arch": "arm64",
    "version": "Darwin Kernel Version 24.6.0: Mon Jul 14 11:30:30 PDT 2025; root:xnu-11417.140.69~1/RELEASE_ARM64_T6020",
    "release": "24.6.0",
    "cpuCount": 10,
    "cpuModel": "Apple M2 Pro",
    "totalMemory": "32.00 GB",
    "freeMemory": "0.14 GB"
  },
  "node": {
    "version": "v24.3.0",
    "env": "development"
  },
  "packageManager": {
    "name": "npm",
    "version": "11.4.2"
  },
  "frameworks": [
    {
      "name": "svelte",
      "version": "^5.0.0"
    },
    {
      "name": "@sveltejs/kit",
      "version": "^2.22.0"
    }
  ],
  "databases": [
    {
      "name": "@libsql/client",
      "version": "^0.14.0"
    },
    {
      "name": "drizzle",
      "version": "^0.40.0"
    }
  ],
  "betterAuth": {
    "version": "0.0.1",
    "config": {
      "baseURL": "http://localhost:5173",
      "plugins": [
        {
          "name": "my-plugin",
          "config": {
            "id": "my-plugin",
            "endpoints": {}
          }
        },
        {
          "name": "mcp",
          "config": {
            "id": "mcp",
            "hooks": {
              "after": [
                {}
              ]
            },
            "endpoints": {},
            "schema": {
              "oauthApplication": {
                "modelName": "oauthApplication",
                "fields": {
                  "name": {
                    "type": "string"
                  },
                  "icon": {
                    "type": "string",
                    "required": false
                  },
                  "metadata": {
                    "type": "string",
                    "required": false
                  },
                  "clientId": {
                    "type": "string",
                    "unique": true
                  },
                  "clientSecret": {
                    "type": "string",
                    "required": false
                  },
                  "redirectURLs": {
                    "type": "string"
                  },
                  "type": {
                    "type": "string"
                  },
                  "disabled": {
                    "type": "boolean",
                    "required": false,
                    "defaultValue": false
                  },
                  "userId": {
                    "type": "string",
                    "required": false,
                    "references": {
                      "model": "user",
                      "field": "id",
                      "onDelete": "cascade"
                    }
                  },
                  "createdAt": {
                    "type": "date"
                  },
                  "updatedAt": {
                    "type": "date"
                  }
                }
              },
              "oauthAccessToken": {
                "modelName": "oauthAccessToken",
                "fields": {
                  "accessToken": {
                    "type": "string",
                    "unique": true
                  },
                  "refreshToken": {
                    "type": "string",
                    "unique": true
                  },
                  "accessTokenExpiresAt": {
                    "type": "date"
                  },
                  "refreshTokenExpiresAt": {
                    "type": "date"
                  },
                  "clientId": {
                    "type": "string",
                    "references": {
                      "model": "oauthApplication",
                      "field": "clientId",
                      "onDelete": "cascade"
                    }
                  },
                  "userId": {
                    "type": "string",
                    "required": false,
                    "references": {
                      "model": "user",
                      "field": "id",
                      "onDelete": "cascade"
                    }
                  },
                  "scopes": {
                    "type": "string"
                  },
                  "createdAt": {
                    "type": "date"
                  },
                  "updatedAt": {
                    "type": "date"
                  }
                }
              },
              "oauthConsent": {
                "modelName": "oauthConsent",
                "fields": {
                  "clientId": {
                    "type": "string",
                    "references": {
                      "model": "oauthApplication",
                      "field": "clientId",
                      "onDelete": "cascade"
                    }
                  },
                  "userId": {
                    "type": "string",
                    "references": {
                      "model": "user",
                      "field": "id",
                      "onDelete": "cascade"
                    }
                  },
                  "scopes": {
                    "type": "string"
                  },
                  "createdAt": {
                    "type": "date"
                  },
                  "updatedAt": {
                    "type": "date"
                  },
                  "consentGiven": {
                    "type": "boolean"
                  }
                }
              }
            }
          }
        },
        {
          "name": "sveltekit-cookies",
          "config": {
            "id": "sveltekit-cookies",
            "hooks": {
              "after": [
                {}
              ]
            }
          }
        }
      ],
      "emailAndPassword": {
        "enabled": true
      }
    }
  }
}

Which area(s) are affected? (Select all that apply)

Backend

Auth config (if applicable)


Additional context

No response

Originally created by @paoloricciuti on GitHub (Sep 9, 2025). ### Is this suited for github? - [x] Yes, this is suited for github ### To Reproduce The `mcp` plugin generates endpoints to serve the authorization server and the protected resource metadata (the latter is also used in the `withMcpAuth` helper to return the right `WWW-authenticate` header). However the docs says the user also need to create their own route for those two endpoints. While testing I realised why that is actually needed: the mcp client respects the `WWW-authenticate` URL so when the `withMcpAuth` returns `/api/auth/.well-known/oauth-protected-resource` it actually fetches that endpoint for the protected resource metadata. But then despite the authorization server being `{base_url}/api/auth` it doesn't try to fetch `{base_url}/api/auth/.well-known/oauth-authorization-server` and goes straight for `{base_url}/.well-known/oauth-authorization-server` (notice the missing `api/auth` at the beginning. Initially I thought this was a bug in the official sdk but doing some research I found out that the `.well-known` route has an [actual specification](https://datatracker.ietf.org/doc/html/rfc5785) and that per spec the `.well-known` part of the path MUST be at the start of the pathname. This is problematic because the endpoints generated from a plugin can only be nested within the base path (which makes sense) but it means `.well-known` endpoints aren't spec compliant. I can work on a PR to fix this but I'm not sure about what's the best strategy for this...should we maybe update the handlers and special case `.well-known` endpoints to not start from `base_path`? Or maybe we should add a configuration option to `better-call` to allow for "out of base path endpoints"? Not really sure what the best bet is. What do you think? ### Current vs. Expected behavior Endpoints created by the plugins should respect the .well-known spec and allow the mcp plugin to be config-less without having the user create a separate and duplicate route. ### What version of Better Auth are you using? 1.3.9 ### System info ```bash { "system": { "platform": "darwin", "arch": "arm64", "version": "Darwin Kernel Version 24.6.0: Mon Jul 14 11:30:30 PDT 2025; root:xnu-11417.140.69~1/RELEASE_ARM64_T6020", "release": "24.6.0", "cpuCount": 10, "cpuModel": "Apple M2 Pro", "totalMemory": "32.00 GB", "freeMemory": "0.14 GB" }, "node": { "version": "v24.3.0", "env": "development" }, "packageManager": { "name": "npm", "version": "11.4.2" }, "frameworks": [ { "name": "svelte", "version": "^5.0.0" }, { "name": "@sveltejs/kit", "version": "^2.22.0" } ], "databases": [ { "name": "@libsql/client", "version": "^0.14.0" }, { "name": "drizzle", "version": "^0.40.0" } ], "betterAuth": { "version": "0.0.1", "config": { "baseURL": "http://localhost:5173", "plugins": [ { "name": "my-plugin", "config": { "id": "my-plugin", "endpoints": {} } }, { "name": "mcp", "config": { "id": "mcp", "hooks": { "after": [ {} ] }, "endpoints": {}, "schema": { "oauthApplication": { "modelName": "oauthApplication", "fields": { "name": { "type": "string" }, "icon": { "type": "string", "required": false }, "metadata": { "type": "string", "required": false }, "clientId": { "type": "string", "unique": true }, "clientSecret": { "type": "string", "required": false }, "redirectURLs": { "type": "string" }, "type": { "type": "string" }, "disabled": { "type": "boolean", "required": false, "defaultValue": false }, "userId": { "type": "string", "required": false, "references": { "model": "user", "field": "id", "onDelete": "cascade" } }, "createdAt": { "type": "date" }, "updatedAt": { "type": "date" } } }, "oauthAccessToken": { "modelName": "oauthAccessToken", "fields": { "accessToken": { "type": "string", "unique": true }, "refreshToken": { "type": "string", "unique": true }, "accessTokenExpiresAt": { "type": "date" }, "refreshTokenExpiresAt": { "type": "date" }, "clientId": { "type": "string", "references": { "model": "oauthApplication", "field": "clientId", "onDelete": "cascade" } }, "userId": { "type": "string", "required": false, "references": { "model": "user", "field": "id", "onDelete": "cascade" } }, "scopes": { "type": "string" }, "createdAt": { "type": "date" }, "updatedAt": { "type": "date" } } }, "oauthConsent": { "modelName": "oauthConsent", "fields": { "clientId": { "type": "string", "references": { "model": "oauthApplication", "field": "clientId", "onDelete": "cascade" } }, "userId": { "type": "string", "references": { "model": "user", "field": "id", "onDelete": "cascade" } }, "scopes": { "type": "string" }, "createdAt": { "type": "date" }, "updatedAt": { "type": "date" }, "consentGiven": { "type": "boolean" } } } } } }, { "name": "sveltekit-cookies", "config": { "id": "sveltekit-cookies", "hooks": { "after": [ {} ] } } } ], "emailAndPassword": { "enabled": true } } } } ``` ### Which area(s) are affected? (Select all that apply) Backend ### Auth config (if applicable) ```typescript ``` ### Additional context _No response_
GiteaMirror added the plugin label 2026-03-13 09:10:58 -05:00
Author
Owner

@dosubot[bot] commented on GitHub (Sep 9, 2025):

You're right—Better Auth currently enforces a base path for all plugin endpoints, so even though the MCP plugin tries to register .well-known endpoints at the root, they're actually served under the base path (like /api/auth/.well-known/...), which breaks RFC 5785 compliance and causes issues with clients expecting /.well-known at the domain root. There’s no built-in way to override this or register plugin endpoints at the root right now, and rewrites don’t work because the router still expects the base path, but redirects can be a workaround for static metadata. This is a known limitation and has been discussed in the community, with some interest in making the .well-known endpoint URL overridable in plugin config, but no official solution yet. Supporting true root-level .well-known endpoints would require either special-casing them in the router or adding a config option to allow "out of base path" endpoints, both of which would need architectural changes to Better Auth’s core routing logic. For now, if you need dynamic metadata (like JWKS), the only options are manual routes or a redirect workaround. More details and related discussion are in this issue and this thread.

To reply, just mention @dosu.


How did I do? Good | Irrelevant | Incorrect | Verbose | Hallucination | Report 🐛 | Other  Chat with Dosu Join Discord Share on X

@dosubot[bot] commented on GitHub (Sep 9, 2025): <!-- Answer --> You're right—Better Auth currently enforces a base path for all plugin endpoints, so even though the MCP plugin tries to register .well-known endpoints at the root, they're actually served under the base path (like /api/auth/.well-known/...), which breaks RFC 5785 compliance and causes issues with clients expecting /.well-known at the domain root. There’s no built-in way to override this or register plugin endpoints at the root right now, and rewrites don’t work because the router still expects the base path, but redirects can be a workaround for static metadata. This is a known limitation and has been discussed in the community, with some interest in making the .well-known endpoint URL overridable in plugin config, but no official solution yet. Supporting true root-level .well-known endpoints would require either special-casing them in the router or adding a config option to allow "out of base path" endpoints, both of which would need architectural changes to Better Auth’s core routing logic. For now, if you need dynamic metadata (like JWKS), the only options are manual routes or a redirect workaround. More details and related discussion are in [this issue](https://github.com/better-auth/better-auth/issues/1276) and [this thread](https://github.com/better-auth/better-auth/issues/2703). <!-- Dosu Comment Footer --> *To reply, just mention [@dosu](https://go.dosu.dev/dosubot).* --- <sup>How did I do? [Good](https://app.dosu.dev/response-feedback/201189fe-2fef-486b-ad5e-7bace0548532?feedback_type=great_response) | [Irrelevant](https://app.dosu.dev/response-feedback/201189fe-2fef-486b-ad5e-7bace0548532?feedback_type=irrelevant_answer) | [Incorrect](https://app.dosu.dev/response-feedback/201189fe-2fef-486b-ad5e-7bace0548532?feedback_type=incorrect_sources) | [Verbose](https://app.dosu.dev/response-feedback/201189fe-2fef-486b-ad5e-7bace0548532?feedback_type=too_verbose) | [Hallucination](https://app.dosu.dev/response-feedback/201189fe-2fef-486b-ad5e-7bace0548532?feedback_type=hallucination) | [Report 🐛](https://app.dosu.dev/response-feedback/201189fe-2fef-486b-ad5e-7bace0548532?feedback_type=bug_report) | [Other](https://app.dosu.dev/response-feedback/201189fe-2fef-486b-ad5e-7bace0548532?feedback_type=other)</sup>&nbsp;&nbsp;[![Chat with Dosu](https://dosu.dev/dosu-chat-badge.svg)](https://app.dosu.dev/cdda13d9-dd27-4d31-b09a-5d8bec92de21/ask?utm_source=github)&nbsp;[![Join Discord](https://img.shields.io/badge/join-5865F2?logo=discord&logoColor=white&label=)](https://go.dosu.dev/discord-bot)&nbsp;[![Share on X](https://img.shields.io/badge/X-share-black)](https://twitter.com/intent/tweet?text=%40dosu_ai%20helped%20me%20solve%20this%20issue!&url=https%3A//github.com/better-auth/better-auth/issues/4540)
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: github-starred/better-auth#1889