fix(mcp): retain DCR metadata fallback

This commit is contained in:
can1357
2026-07-14 21:00:58 +02:00
parent 80f9329e31
commit ebe79d6f53
2 changed files with 57 additions and 2 deletions
+51 -1
View File
@@ -563,7 +563,7 @@ export class MCPOAuthFlow extends OAuthCallbackFlow {
* accept the later authorize request for the same scope set.
*/
async #tryRegisterClient(redirectUri: string): Promise<void> {
const registrationEndpoint = this.config.registrationUrl;
const registrationEndpoint = this.config.registrationUrl ?? (await this.#resolveRegistrationEndpoint());
if (!registrationEndpoint) return;
try {
@@ -620,6 +620,56 @@ export class MCPOAuthFlow extends OAuthCallbackFlow {
}
}
async #resolveRegistrationEndpoint(): Promise<string | null> {
const authorizationUrl = new URL(this.config.authorizationUrl);
// origin-root well-known; most servers serve metadata here.
const rootUrl = new URL("/.well-known/oauth-authorization-server", authorizationUrl.origin).toString();
const endpoint = await this.#tryWellKnownForRegistration(rootUrl);
if (endpoint) return endpoint;
// path-prefixed well-known for gateways (e.g. https://gateway.example.com/my-service/).
const normalizedPath = authorizationUrl.pathname.replace(/\/$/, "");
const lastSlash = normalizedPath.lastIndexOf("/");
// Bare-origin authorization URL — nothing further to try.
if (lastSlash < 0) return null;
// Single-segment paths are the gateway prefix itself; multi-segment paths
// drop the trailing segment (typically a service endpoint).
const prefixPath = lastSlash === 0 ? normalizedPath : normalizedPath.slice(0, lastSlash);
const prefixedUrl = new URL(
".well-known/oauth-authorization-server",
`${authorizationUrl.origin}${prefixPath}/`,
).toString();
const prefixedEndpoint = await this.#tryWellKnownForRegistration(prefixedUrl);
if (prefixedEndpoint) return prefixedEndpoint;
// RFC 8414 §3.1 path-ful issuer form: /.well-known/oauth-authorization-server/<path>.
const pathfulUrl = new URL(
`/.well-known/oauth-authorization-server${normalizedPath}`,
authorizationUrl.origin,
).toString();
return await this.#tryWellKnownForRegistration(pathfulUrl);
}
async #tryWellKnownForRegistration(wellKnownUrl: string): Promise<string | null> {
try {
const response = await this.#fetch(wellKnownUrl, {
method: "GET",
headers: { Accept: "application/json" },
signal: this.ctrl.signal,
});
if (!response.ok) return null;
const metadata = (await response.json()) as { registration_endpoint?: string };
if (metadata.registration_endpoint && metadata.registration_endpoint.trim() !== "") {
return metadata.registration_endpoint;
}
} catch {
// Ignore fetch/parse failures.
}
return null;
}
async #assertClientIdNotRequired(authorizationUrl: string): Promise<void> {
try {
const response = await this.#fetch(authorizationUrl, {
@@ -28,6 +28,12 @@ function mockProviderTokenEndpoint(onBody: (body: string) => void): FetchImpl {
function mockFigmaRegistration(onRegistration: (payload: Record<string, unknown>) => void): FetchImpl {
return async (input, init) => {
const url = String(input);
if (url === "https://www.figma.com/.well-known/oauth-authorization-server") {
return new Response(JSON.stringify({ registration_endpoint: "https://www.figma.com/oauth/register" }), {
status: 200,
headers: { "Content-Type": "application/json" },
});
}
if (url === "https://www.figma.com/oauth/register") {
onRegistration(JSON.parse(String(init?.body)) as Record<string, unknown>);
return new Response(
@@ -62,7 +68,6 @@ describe("mcp oauth flow", () => {
{
authorizationUrl: "https://www.figma.com/oauth/mcp",
tokenUrl: "https://api.figma.com/v1/oauth/token",
registrationUrl: "https://www.figma.com/oauth/register",
fetch: mockFigmaRegistration(payload => {
registrationPayload = payload;
}),