Merge branch 'main' into fix/1

This commit is contained in:
오병진 2025-07-02 20:24:40 +09:00 • committed by GitHub
commit f31cefe2fa
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 246 additions and 57 deletions

View file

@ -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,

View file

@ -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;
}

View file

@ -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();
});

View file

@ -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 {