Skip to content

Commit 58f8380

Browse files
committed
fix: retry OAuth discovery with backoff
1 parent 39775f2 commit 58f8380

File tree

1 file changed

+78
-41
lines changed

1 file changed

+78
-41
lines changed

e2e_tests/typescript/src/automated_oauth.ts

Lines changed: 78 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ export async function createAutomatedOAuthTransport(
1818
clientId: string,
1919
clientSecret: string
2020
): Promise<StreamableHTTPClientTransport> {
21-
const scope = await discoverScope(serverUrl);
21+
const scope = await retryWithBackoff(() => discoverScope(serverUrl), 5, 2000);
2222

2323
const clientMetadata: OAuthClientMetadata = {
2424
client_name: "MCP Client",
@@ -30,58 +30,95 @@ export async function createAutomatedOAuthTransport(
3030
};
3131

3232
const oauthProvider = new AutomatedOAuthClientProvider(clientMetadata, clientId, clientSecret);
33-
await performClientCredentialsFlow(serverUrl, oauthProvider);
33+
await retryWithBackoff(() => performClientCredentialsFlow(serverUrl, oauthProvider), 5, 2000);
3434

3535
return new StreamableHTTPClientTransport(new URL(serverUrl), {
3636
authProvider: oauthProvider,
3737
});
3838
}
3939

40+
async function retryWithBackoff<T>(
41+
fn: () => Promise<T>,
42+
maxRetries: number,
43+
initialDelay: number
44+
): Promise<T> {
45+
let lastError: Error | undefined;
46+
for (let i = 0; i < maxRetries; i++) {
47+
try {
48+
return await fn();
49+
} catch (error) {
50+
lastError = error as Error;
51+
if (i < maxRetries - 1) {
52+
const delay = initialDelay * Math.pow(2, i);
53+
await new Promise(resolve => setTimeout(resolve, delay));
54+
}
55+
}
56+
}
57+
throw lastError;
58+
}
59+
4060
async function discoverScope(serverUrl: string): Promise<string> {
41-
const response = await fetch(serverUrl, {
42-
method: "POST",
43-
headers: { "Content-Type": "application/json" },
44-
body: JSON.stringify({ jsonrpc: "2.0", method: "ping", id: 1 }),
45-
});
46-
const resourceMetadataUrl = extractResourceMetadataUrl(response);
47-
const resourceMetadata = await discoverOAuthProtectedResourceMetadata(serverUrl, { resourceMetadataUrl });
48-
return resourceMetadata.scopes_supported?.join(" ") || "";
61+
const controller = new AbortController();
62+
const timeout = setTimeout(() => controller.abort(), 10000);
63+
64+
try {
65+
const response = await fetch(serverUrl, {
66+
method: "POST",
67+
headers: { "Content-Type": "application/json" },
68+
body: JSON.stringify({ jsonrpc: "2.0", method: "ping", id: 1 }),
69+
signal: controller.signal,
70+
});
71+
const resourceMetadataUrl = extractResourceMetadataUrl(response);
72+
const resourceMetadata = await discoverOAuthProtectedResourceMetadata(serverUrl, { resourceMetadataUrl });
73+
return resourceMetadata.scopes_supported?.join(" ") || "";
74+
} finally {
75+
clearTimeout(timeout);
76+
}
4977
}
5078

5179
async function performClientCredentialsFlow(serverUrl: string, oauthProvider: AutomatedOAuthClientProvider): Promise<void> {
5280
if (oauthProvider.tokens()?.access_token) return;
5381

54-
const response = await fetch(serverUrl, {
55-
method: "POST",
56-
headers: { "Content-Type": "application/json" },
57-
body: JSON.stringify({ jsonrpc: "2.0", method: "ping", id: 1 }),
58-
});
59-
const resourceMetadataUrl = extractResourceMetadataUrl(response);
60-
const resourceMetadata = await discoverOAuthProtectedResourceMetadata(serverUrl, { resourceMetadataUrl });
61-
const authServerUrl = resourceMetadata.authorization_servers?.[0];
62-
if (!authServerUrl) throw new Error("No authorization server found");
63-
64-
const metadata = await discoverAuthorizationServerMetadata(authServerUrl);
65-
if (!metadata?.token_endpoint) throw new Error("No token endpoint found");
66-
67-
const clientInfo = oauthProvider.clientInformation();
68-
if (!clientInfo) throw new Error("No client information available");
69-
70-
const params = new URLSearchParams({
71-
grant_type: "client_credentials",
72-
client_id: clientInfo.client_id,
73-
client_secret: clientInfo.client_secret!,
74-
scope: oauthProvider.clientMetadata.scope || "",
75-
});
76-
77-
const tokenResponse = await fetch(metadata.token_endpoint, {
78-
method: "POST",
79-
headers: { "Content-Type": "application/x-www-form-urlencoded" },
80-
body: params,
81-
});
82-
83-
if (!tokenResponse.ok) throw new Error(`Token request failed: ${tokenResponse.status}`);
84-
oauthProvider.saveTokens(await tokenResponse.json());
82+
const controller = new AbortController();
83+
const timeout = setTimeout(() => controller.abort(), 10000);
84+
85+
try {
86+
const response = await fetch(serverUrl, {
87+
method: "POST",
88+
headers: { "Content-Type": "application/json" },
89+
body: JSON.stringify({ jsonrpc: "2.0", method: "ping", id: 1 }),
90+
signal: controller.signal,
91+
});
92+
const resourceMetadataUrl = extractResourceMetadataUrl(response);
93+
const resourceMetadata = await discoverOAuthProtectedResourceMetadata(serverUrl, { resourceMetadataUrl });
94+
const authServerUrl = resourceMetadata.authorization_servers?.[0];
95+
if (!authServerUrl) throw new Error("No authorization server found");
96+
97+
const metadata = await discoverAuthorizationServerMetadata(authServerUrl);
98+
if (!metadata?.token_endpoint) throw new Error("No token endpoint found");
99+
100+
const clientInfo = oauthProvider.clientInformation();
101+
if (!clientInfo) throw new Error("No client information available");
102+
103+
const params = new URLSearchParams({
104+
grant_type: "client_credentials",
105+
client_id: clientInfo.client_id,
106+
client_secret: clientInfo.client_secret!,
107+
scope: oauthProvider.clientMetadata.scope || "",
108+
});
109+
110+
const tokenResponse = await fetch(metadata.token_endpoint, {
111+
method: "POST",
112+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
113+
body: params,
114+
signal: controller.signal,
115+
});
116+
117+
if (!tokenResponse.ok) throw new Error(`Token request failed: ${tokenResponse.status}`);
118+
oauthProvider.saveTokens(await tokenResponse.json());
119+
} finally {
120+
clearTimeout(timeout);
121+
}
85122
}
86123

87124
class AutomatedOAuthClientProvider implements OAuthClientProvider {

0 commit comments

Comments
 (0)