Merge branch 'main' into fix/1
This commit is contained in:
commit
f31cefe2fa
4 changed files with 246 additions and 57 deletions
|
|
@ -403,6 +403,19 @@ describe("OAuth Authorization", () => {
|
|||
expect(mockFetch).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it("returns undefined when both CORS requests fail in fetchWithCorsRetry", async () => {
|
||||
// fetchWithCorsRetry tries with headers (fails with CORS), then retries without headers (also fails with CORS)
|
||||
// simulating a 404 w/o headers set. We want this to return undefined, not throw TypeError
|
||||
mockFetch.mockImplementation(() => {
|
||||
// Both the initial request with headers and retry without headers fail with CORS TypeError
|
||||
return Promise.reject(new TypeError("Failed to fetch"));
|
||||
});
|
||||
|
||||
// This should return undefined (the desired behavior after the fix)
|
||||
const metadata = await discoverOAuthMetadata("https://auth.example.com/path");
|
||||
expect(metadata).toBeUndefined();
|
||||
});
|
||||
|
||||
it("returns undefined when discovery endpoint returns 404", async () => {
|
||||
mockFetch.mockResolvedValueOnce({
|
||||
ok: false,
|
||||
|
|
|
|||
|
|
@ -292,25 +292,24 @@ export async function discoverOAuthProtectedResourceMetadata(
|
|||
return OAuthProtectedResourceMetadataSchema.parse(await response.json());
|
||||
}
|
||||
|
||||
/**
|
||||
* Looks up RFC 8414 OAuth 2.0 Authorization Server Metadata.
|
||||
*
|
||||
* If the server returns a 404 for the well-known endpoint, this function will
|
||||
* return `undefined`. Any other errors will be thrown as exceptions.
|
||||
*/
|
||||
/**
|
||||
* Helper function to handle fetch with CORS retry logic
|
||||
*/
|
||||
async function fetchWithCorsRetry(
|
||||
url: URL,
|
||||
headers: Record<string, string>,
|
||||
): Promise<Response> {
|
||||
headers?: Record<string, string>,
|
||||
): Promise<Response | undefined> {
|
||||
try {
|
||||
return await fetch(url, { headers });
|
||||
} catch (error) {
|
||||
// CORS errors come back as TypeError, retry without headers
|
||||
if (error instanceof TypeError) {
|
||||
return await fetch(url);
|
||||
if (headers) {
|
||||
// CORS errors come back as TypeError, retry without headers
|
||||
return fetchWithCorsRetry(url)
|
||||
} else {
|
||||
// We're getting CORS errors on retry too, return undefined
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
|
|
@ -334,7 +333,7 @@ function buildWellKnownPath(pathname: string): string {
|
|||
async function tryMetadataDiscovery(
|
||||
url: URL,
|
||||
protocolVersion: string,
|
||||
): Promise<Response> {
|
||||
): Promise<Response | undefined> {
|
||||
const headers = {
|
||||
"MCP-Protocol-Version": protocolVersion
|
||||
};
|
||||
|
|
@ -344,10 +343,16 @@ async function tryMetadataDiscovery(
|
|||
/**
|
||||
* Determines if fallback to root discovery should be attempted
|
||||
*/
|
||||
function shouldAttemptFallback(response: Response, pathname: string): boolean {
|
||||
return response.status === 404 && pathname !== '/';
|
||||
function shouldAttemptFallback(response: Response | undefined, pathname: string): boolean {
|
||||
return !response || response.status === 404 && pathname !== '/';
|
||||
}
|
||||
|
||||
/**
|
||||
* Looks up RFC 8414 OAuth 2.0 Authorization Server Metadata.
|
||||
*
|
||||
* If the server returns a 404 for the well-known endpoint, this function will
|
||||
* return `undefined`. Any other errors will be thrown as exceptions.
|
||||
*/
|
||||
export async function discoverOAuthMetadata(
|
||||
authorizationServerUrl: string | URL,
|
||||
opts?: { protocolVersion?: string },
|
||||
|
|
@ -362,18 +367,10 @@ export async function discoverOAuthMetadata(
|
|||
|
||||
// If path-aware discovery fails with 404, try fallback to root discovery
|
||||
if (shouldAttemptFallback(response, issuer.pathname)) {
|
||||
try {
|
||||
const rootUrl = new URL("/.well-known/oauth-authorization-server", issuer);
|
||||
response = await tryMetadataDiscovery(rootUrl, protocolVersion);
|
||||
|
||||
if (response.status === 404) {
|
||||
return undefined;
|
||||
}
|
||||
} catch {
|
||||
// If fallback fails, return undefined
|
||||
return undefined;
|
||||
}
|
||||
} else if (response.status === 404) {
|
||||
const rootUrl = new URL("/.well-known/oauth-authorization-server", issuer);
|
||||
response = await tryMetadataDiscovery(rootUrl, protocolVersion);
|
||||
}
|
||||
if (!response || response.status === 404) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -74,6 +74,7 @@ test("should read messages", async () => {
|
|||
await client.close();
|
||||
});
|
||||
|
||||
|
||||
test("should properly set default environment variables in spawned process", async () => {
|
||||
await envAsyncLocalStorage.run({ env: {} }, async () => {
|
||||
const client = new StdioClientTransport(serverParameters);
|
||||
|
|
@ -128,4 +129,13 @@ test("should override default environment variables with custom ones", async ()
|
|||
}
|
||||
}
|
||||
});
|
||||
|
||||
test("should return child process pid", async () => {
|
||||
const client = new StdioClientTransport(serverParameters);
|
||||
|
||||
await client.start();
|
||||
expect(client.pid).not.toBeNull();
|
||||
await client.close();
|
||||
expect(client.pid).toBeNull();
|
||||
|
||||
});
|
||||
|
|
|
|||
|
|
@ -189,6 +189,15 @@ export class StdioClientTransport implements Transport {
|
|||
return this._process?.stderr ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* The child process pid spawned by this transport.
|
||||
*
|
||||
* This is only available after the transport has been started.
|
||||
*/
|
||||
get pid(): number | null {
|
||||
return this._process?.pid ?? null;
|
||||
}
|
||||
|
||||
private processReadBuffer() {
|
||||
while (true) {
|
||||
try {
|
||||
|
|
|
|||
Loading…
Reference in a new issue