MCP: Support authInfo from Transport#426
Conversation
🦋 Changeset detectedLatest commit: 69e57bd The changes in this PR will be included in the next version bump. Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
- Add request context tracking to capture headers, method, and URL information - Implement authentication info support with customizable auth validation via resolveAuthInfo method - Enhance transport message handling with request and auth context in MessageExtraInfo - Update McpSSETransport and McpStreamableHttpTransport to support extra parameter - Add comprehensive tests for request info and auth info functionality - Add ResolveAuthInfoArgs type for better type safety - Internal change: Transport onmessage callbacks now receive optional extra parameter
39d5f93 to
2ac318a
Compare
|
I changed the implementation quite a bit and verified it works in my project. |
|
I've also verified this works in production as well. |
|
@deathbyknowledge can you please take a look? thank you |
commit: |
|
@kentcdodds do you want to add an .md in the docs or maybe mention this in the readme? |
|
Sure thing. I added a few docs to this PR. The agent wanted to add a bunch more, but I couldn't 100% verify the accuracy of some of what it said and I don't want this PR to get tied up by those changes. |
|
Hey @kentcdodds, we currently have this PR #415 that refactors Is // In your worker fetch handler
fetch(request: Request, env: Env, ctx: ExecutionContext) {
// Handle custom props
const props: Props = {
echoAvailable: !!request.headers.get("x-my-feature-flag")
};
// If we're not using OAuthProvider we can set our custom props directly
// otherwise we'd need to make sure we don't override the already existing props
ctx.props = props;
return MyMCP.serve("/").fetch(request, env, ctx);
}
// In your McpAgent
async init() {
const { echoAvailable } = this.props;
// Simple echo tool that's gated behind a feature flag
if (echoAvailable) {
this.server.tool("echo", { msg: z.string() }, async ({ msg }) => ({
content: [{ type: "text", text: msg }]
}));
}
}If the McpAgent is behind an If I'm not mistaken, if you're not using OAuthProvider, you can completely customize your prop handling and provide them to your MCP directly with I've updated the E2E in the PR I mentioned so they include some of these examples (authless, OAuth). Let me know if I missed anything |
|
Modifying props is actually how I handle this now as noted in #422, but I was told this is not intended to be modified so I decided to add this feature. However, if what I'm doing is fine then that's great! However, we do still need a way to pass the |
|
Merged with main. I'd still love to get support for the |
|
Anything I can do to get progress on merging this? |
Hey @kentcdodds, we're currently in the process of merging #415 which conflicts with these changes. Also, I believe the Regardless, we're planning to re-think/improve the ergonomics around |
|
I see. Good to know. I'm going to be recording videos showing people how to use this package and I would really love to know how things will work so I can do that in my videos (even if it doesn't work that way currently, I can make it appear that way in the videos). Can you give me an example of how I could get the auth info into my tool handler and also how I could get the |
|
If you're using // Consider this Worker with the following entrypoint
export default new OAuthProvider({
// These API handlers ONLY run after authorization.
// This means ctx.props has already been populated by OAuthProvider
apiHandlers: {
// Any other API handlers you'd want to add would go here...
"/mcp": {
fetch(request: Request, env: unknown, ctx: ExecutionContext) {
// This handler is running in your Worker's context, not the DO.
// You are safe to inject any props here as you see fit, since the
// OAuthProvider has already set its own, meaning yours won't get overwritten.
ctx.props = {
...ctx.props,
origin: new URL(request.url).origin,
// You could also read the headers to set custom props
flag: !!request.headers.get("x-my-feature-flag"),
};
// The `serve` handler will always ensure that the latest props are available
// in your MCP through `this.props`
return MyMCP.serve("/mcp").fetch(request, env, ctx);
}
}
},
defaultHandler: myHandler, // This one runs before authorization!
authorizeEndpoint: "/authorize", // In fact, `/authorize` lives in the defaultHandler too!
tokenEndpoint: "/token",
clientRegistrationEndpoint: "/register",
});This would be for "request-level" props in case you had any. If you needed session-wide props, you can set them in your // Following the previous snippet, this could be its defaultHandler
const myHandler = {
// This handles all requests for your Worker that are:
// - non-OAuth related. Any authless routing your app might have
// - OR the `/authorization` step for OAuth, which you get to write here
async fetch(request: Request, env: Env) {
const url = new URL(request.url);
const provider = env.OAUTH_PROVIDER;
// Health check for the Worker (which is authless)
if (url.pathname === "/health") {
return new Response("OK");
}
// Anything else you might want to expose without auth...
// Your `/authorize` handler for OAuth
if (new URL(request.url).pathname === "/authorize") {
// Your authorization step logic
// ...
const { email, username } = myAuthorizedPayloadOrSomething;
// Let's pretend you have access to `email` and `username` after
// this step and you want to make them available in the props
// Now this props will be what OAuthProvider sets in `ctx.props` and
// makes available to every authorized request from this user. You should
// be able to get the same data here as from `authInfo` but the specific
// implementation depends on how you rollout your auth.
const props = { email, username };
const { redirectTo } = await provider.completeAuthorization({
// your other auth args...
props, // we give OAuthProvider the props to use from now on
});
return Response.redirect(redirectTo, 302);
}
}
};Mind that the Finally, you would use the props we just set in your // The type for the props we set previously
type MyProps = {
flag: boolean; // Let's use this flag to gate a tool
origin: string;
username: string;
email: string;
}
// The type param McpAgent<..., ..., Props> will land with the PR I mentioned previously
export class MyMCP extends McpAgent<Env, State, MyProps> {
server = new McpServer({ name: "my-mcp", version: "0.1.0" });
async init() {
// We gate the `whoami` tool if the request included the feature flag header.
// This means that the `ctx.props.flag` was set at init time of the MCP
if (this.props?.flag) {
this.server.tool(
"whoami",
"Return the email and username of the user",
{},
async () => ({
// This tool will read email and username from `this.props` every time it's called
content: [{ type: "text", text: `User: ${this.props?.username} (${this.props?.email})` }]
})
);
}
this.server.tool(
"getOrigin",
"Return the request origin",
{},
async () => ({
content: [{ type: "text", text: this.props?.origin }]
})
);
}
}It's a bit long but I think that covers every step of the request (except OAuth internals)! Let me know if that works :) PS: If you were NOT using OAuth, you can follow the same pattern and provide your own props to |
Closes #422
Problem Description
Currently, the
McpAgentclass from'agents/mcp'does not provide access to theauthInfofrom the underlying MCP transport layer. This prevents request handlers from accessing authentication information that should be automatically populated by the transport.Current Behavior
When setting request handlers in
McpAgent, theextraparameter in request handlers does not contain theauthInfoproperty, even though the MCP SDK'sRequestHandlerExtratype defines it as:Expected Behavior
Request handlers should be able to access
authInfofrom theextraparameter when the transport has validated an access token:Solution
This PR implements the following changes to support
authInfofrom the transport:1. Enhanced Transport Interface
McpSSETransportandMcpStreamableHttpTransportto supportMessageExtraInfoin theironmessagecallbacksrequestInfoandauthInfothrough the transport layeronmessagecallbacks now receive an optionalextraparameter as the second argument2. Request Context Tracking
requestInfoandauthInfoproperties to track the current request contextupdateRequestInfo()method to capture and store request method, URL, and headersupdateAuthInfo()method to store authentication information3. Authentication Resolution
resolveAuthInfo()method that subclasses can override to provide custom authentication validationRequestInfo(including headers) and returnsAuthInfo | undefinedsetCurrentAuthInfo()method for automatic auth validation during request processing4. Integration Points
serializeRequestInfo()requestInfoandauthInfoin transport callbacks5. Type Safety and Utilities
MessageExtraInfo,RequestInfo, andAuthInfoResolveAuthInfoArgstype for better type safety in auth resolversextraparameter6. Testing
extraparametersgetRequestInfo,getAuthInfo) for validationUsage Example
Testing
resolveAuthInfo@kentcdodds/tmp_agents@0.0.114-alpha.0and using it in my own project