Allow WWW-Authenticate header to be accessed by client
Summary of Changes
This pull request enhances the server's proxy functionality by enabling the correct forwarding of WWW-Authenticate headers from the backend to the client. This ensures that clients receive appropriate authentication challenges. The changes involve capturing the header during transport creation, exposing it through the transport object, and applying it to the proxy's outgoing responses. A significant refactoring was also undertaken to improve the robustness and maintainability of the fetch interception mechanism and error handling, directly addressing critical feedback regarding potential race conditions and type safety.
Highlights
- Authentication Header Forwarding: Implemented logic to intercept and forward the
WWW-Authenticateheader from backend (MCP server) responses to the client, ensuring proper authentication challenges are propagated. - Localized Fetch Interception: Refactored the
createTransportfunction to use a localizedinterceptingFetchmechanism, which is passed directly to theSSEClientTransportandStreamableHTTPClientTransportconstructors. This change addresses critical race condition concerns by avoiding global modification ofglobalThis.fetch. - Enhanced Transport Creation Return: The
createTransportfunction now returns an object containing both theTransportinstance and any capturedWWW-Authenticateheader, allowing for its propagation to the client. - Centralized Header Handling: Introduced new helper functions,
maybeSetAuthHeaderandsetAuthHeaderFromError, to centralize and reuse the logic for setting theWWW-Authenticateheader on proxy responses, particularly in error scenarios, improving code maintainability and reducing duplication.
Changelog
- server/src/index.ts
- Added
maybeSetAuthHeaderhelper function to conditionally set theWWW-Authenticateheader on an Express response. - Added
setAuthHeaderFromErrorhelper function to safely extract and set theWWW-Authenticateheader from an error object, including type checks for robustness. - Modified the
createTransportfunction signature to return an object containing both theTransportinstance and an optionalauthHeader. - Implemented a local
interceptingFetchwithincreateTransportto capture theWWW-Authenticateheader from 401 responses, passing this custom fetch toSSEClientTransportandStreamableHTTPClientTransportto avoid global side effects. - Updated the
app.postroute for StreamableHTTP connections to utilize the newcreateTransportreturn value and themaybeSetAuthHeaderandsetAuthHeaderFromErrorhelpers. - Updated the
app.getroute for STDIO connections to utilize the newcreateTransportreturn value and themaybeSetAuthHeaderandsetAuthHeaderFromErrorhelpers. - Updated the
app.getroute for SSE connections to utilize the newcreateTransportreturn value and themaybeSetAuthHeaderandsetAuthHeaderFromErrorhelpers.
- Added
Motivation and Context
When a 401 Unauthorized error is returned from a server, the WWW-Authenticate header should be included in the response, advertising the HTTP authentication methods (or challenges) that might be used to gain access.
By adding this header name to the Access-Control-Expose-Headers header in the server response, it should tell the browser to expose that header to scripts, so that the client can extract the authentication method or challenge and react appropriately.
However, even with the above change, we can see that with a server that is actually returning a WWW-Authenticate header with a 401, the header isn't making back to the client via the proxy.
There are two transports involved; Client <-> Proxy and Proxy <-> Server. We also need to bridge the headers across the transports in the proxy, not just the error message.
How Has This Been Tested?
User @MOmarMiraj has been helping with a server he has that returns WWW-Authenticate header on 401.
Breaking Changes
Nope.
Types of changes
- [x] Bug fix (non-breaking change which fixes an issue)
- [ ] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing functionality to change)
- [ ] Documentation update
Checklist
- [x] I have read the MCP Documentation
- [x] My code follows the repository's style guidelines
- [x] New and existing tests pass locally
- [x] I have added appropriate error handling
- [ ] I have added or updated documentation as needed
Additional context
- This fixes #168
In case it's useful at all, I've got a demo MCP server that returns 401/www-authenticate here! I have tested this with a client that handles auth properly according to spec and it's working. Would be great if the inspector worked this way 👀
In case it's useful at all, I've got a demo MCP server that returns 401/www-authenticate here! I have tested this with a client that handles auth properly according to spec and it's working. Would be great if the inspector worked this way 👀
Only problem is I don't have a Clerk application. If you don't mind, though, keep an eye on this PR and when it gets merged, raise a flag here if it doesn't work with your server.
Hey Folks,
Just wanted to know what is currently blocking this MR and if needed how I can help. This would be awesome to get merged in considering the latest spec.
Hey Folks,
Just wanted to know what is currently blocking this MR and if needed how I can help. This would be awesome to get merged in considering the latest spec.
Waiting for SDK support. @pcarleton can probably elucidate the gap.
Hmm do you mean https://github.com/modelcontextprotocol/typescript-sdk/pull/503 (this is server side) and this is client side https://github.com/modelcontextprotocol/typescript-sdk/pull/416 unless im missing something.
@pcarleton mentioned this one when I asked in Discord. But I'm not certain that keeps 401 from reaching the client, it's just related to scopes. So I'll look into this one again today.
ALSO: A big problem Is I don't have an in-project server that returns a 401 with this header for testing.
@pcarleton mentioned this one when I asked in Discord. But I'm not certain that keeps 401 from reaching the client, it's just related to scopes. So I'll look into this one again today.
ALSO: A big problem Is I don't have an in-project server that returns a 401 with this header for testing.
I am in the process of building one.. if you can get the MR up I can try testing!
ALSO: A big problem Is I don't have an in-project server that returns a 401 with this header for testing.
I am in the process of building one.. if you can get the MR up I can try testing!
Thanks @MOmarMiraj but I don't want to merge this without testing, there might be more to do. However, in order to test this PR, you could
- Fork this repo
- Clone your fork locally, and assuming you didn't rename it, do:
git clone https://github.com/MOmarMiraj/inspector.git
- From your local project folder add my repo as a remote:
git add remote cliffhall https://github.com/cliffhall/mcp-inspector.git
- See the branches on my repo
fetch cliffhallgit checkout -b pass-www-authenticate-headers cliffhall/pass-www-authenticate-headersto check out my PR branch
- Install deps
npm install
- Build the project
npm run build
- Launch the inspector with my changes
npm run start
- Point it at your server and see what happens in the network tab of the browser's devtools window. Does the header make it through?
I see in the request that it is exposed and when I try to start the server.. it immediately runs the OAuth flow and tries to fetch the .well-known/oauth-protected-resource etc.. So I believe it is working but I can't find the actual 401 unauthorized in the console.
When I change the status code to 200 but send the WWW-Authenticate header, it doesn't start the OAuth flow.. so from my testing looks like it goes through!
I see in the request that it is exposed and when I try to start the server.. it immediately runs the OAuth flow and tries to fetch the
.well-known/oauth-protected-resourceetc.. So I believe it is working but I can't find the actual 401 unauthorized in the console.When I change the status code to 200 but send the
WWW-Authenticateheader, it doesn't start the OAuth flow.. so from my testing looks like it goes through!
@MOmarMiraj This report is sort of confusing. You never see the 401 at the client?
Ahh sorry for the confusing report, I do see the 401 and then it continues with the OAuth Flow.
See attached picture as you can see at the top with
Error POSTing to endpoint (HTTP 401)
and then the OAuth flow starts.
Ahh sorry for the confusing report, I do see the 401 and then it continues with the OAuth Flow.
See attached picture as you can see at the top with
Error POSTing to endpoint (HTTP 401)
@MOmarMiraj Can you show the output both when you do and when you don't have these changes in place for comparison?
Open the network tab of devtools and select any request to view its response headers directly, without relying on error console output. this is what we really want to verify.
E.g, ...
This is the image with no WWW-Authenticate
and this is the image with WWW-Authenticate
This is the image with no
WWW-Authenticate... and this is the image withWWW-Authenticate...
Ok, right. But if you notice, the actual header you're looking at is Access-Control-Expose-Headers and its value is WWW-Authenticate. This is good and what I would expect; it says to expose any actual WWW-Authenticate header to the client for processing.
What I need to see now, is the headers of a request that returned a 401 response. In that case, we want to see WWW-Authenticate on the left side of the picture - an actual header. And the same call when this code is not in place should not show that header. Try again and examine whichever call was outputting the 401 error message in the console in your comment above.
Hm.. for some reason I can't find the 401 in the browser client inspector and I just see a 200 for the actual /mcp endpoint call. I tried doing curl requests and I can see it better:
and this is when I pass an actual token
Hm.. for some reason I can't find the 401 in the browser client inspector and I just see a 200 for the actual
/mcpendpoint call. I tried doing curl requests and I can see it better
Ok this is super useful. We can see that the server is actually returning a WWW-Authenticate header with a 401, but that isn't making back to the client via the proxy.
There are two transports involved; Client <-> Proxy and Proxy <-> Server. This report tells me we need to do more to bridge the headers across the transports in the proxy.
@MOmarMiraj I've had a crack at a fix. Can you test it again?
@cliffhall Just tried it and still similar issue, don't see the 401 on the network side of things.
Also what I noticed, the MCP inspector doesn't extract the URL from the WWW-Authenticate header. No matter the value of the WWW-Authenticate header, the protected resource URL is always the URL of my MCP server + the .well-known/oauth-protected-resource which doesn't abide by the MCP spec. I haven't looked into if this is more of SDK issue or inspector issue.
@cliffhall Just tried it and still similar issue, don't see the 401 on the network side of things.
It would be great to see the response header list there. The screenshot says there are 10 headers, but the header list is closed
Also what I noticed, the MCP inspector doesn't extract the URL from the WWW-Authenticate header.
First we have to get the header. We're working on that part.
@cliffhall Just tried it and still similar issue, don't see the 401 on the network side of things.
It would be great to see the full response header list there. I trust you but it would still be nice to see that is missing and not just below the fold. I'm going to have to build this into an in-project server for testing soon.
the protected resource URL is always the URL of my MCP server + the
.well-known/oauth-protected-resourcewhich doesn't abide by the MCP spec.Your screenshot doesn't support this statement; it doesn't talk about the URL.
The second paragraph speaking about the server metadata URL. If we look inside the RFC pointed out in the MCP spec, you can see it talks about the full URL.
Response headers. Can you show them? You posted the request headers.
~~Your screenshot doesn't support this statement; it doesn't talk about the URL.~~ Also, my bad on that part I amended my comment
oops lol sorry wrong screen shot.. Here you go is the response headers
Ok, thanks @MOmarMiraj for your help. I'm going to have to figure out a good way to test this locally.
I believe the issue lies in the SDK not accepting a custom fetch function. I believe you are correctly overriding it but inside the StreamableHTTPClient class in the MCP TS SDK, they don't call the custom fetch implementation and always defaults to the native TS one.
Heres an issue about it as well https://github.com/modelcontextprotocol/typescript-sdk/issues/476
I believe the issue lies in the SDK not accepting a custom fetch function. I believe you are correctly overriding it but inside the
StreamableHTTPClientclass in the MCP TS SDK, they don't call the custom fetch implementation and always defaults to the native TS one.Heres an issue about it as well modelcontextprotocol/typescript-sdk#476
Thanks for this insight @MOmarMiraj!
Waiting on https://github.com/modelcontextprotocol/typescript-sdk/pull/721
@MOmarMiraj could you try this again? You'll need to run npm install again, and should be using sdk version 1.15.0, which has the support for custom fetch that this PR uses to capture and bridge the www-authenticate header to the client.
I updated to the latest SDK and pulled ur branch and still not getting that WWW-Authenticate header across.. I still think the fetch isn't working correctly. I try to debug from inside that function and I do not receive my debug statement...
From the proxy, we get a 401 unauthorized but for some reason the actual POST /mcp endpoint call gives a 200?