Skip to content

MCP: Support authInfo from Transport#426

Closed
kentcdodds wants to merge 6 commits into
cloudflare:mainfrom
kentcdodds:pr/add-message-extra-info
Closed

MCP: Support authInfo from Transport#426
kentcdodds wants to merge 6 commits into
cloudflare:mainfrom
kentcdodds:pr/add-message-extra-info

Conversation

@kentcdodds
Copy link
Copy Markdown

@kentcdodds kentcdodds commented Aug 28, 2025

Closes #422

Problem Description

Currently, the McpAgent class from 'agents/mcp' does not provide access to the authInfo from 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, the extra parameter in request handlers does not contain the authInfo property, even though the MCP SDK's RequestHandlerExtra type defines it as:

export type RequestHandlerExtra<
  SendRequestT extends Request,
  SendNotificationT extends Notification
> = {
  signal: AbortSignal;
  authInfo?: AuthInfo; // This is undefined in our handlers
  sessionId?: string;
  _meta?: RequestMeta;
  requestId: RequestId;
  requestInfo?: RequestInfo;
  sendNotification: (notification: SendNotificationT) => Promise<void>;
  sendRequest: <U extends ZodType<object>>(
    request: SendRequestT,
    resultSchema: U,
    options?: RequestOptions
  ) => Promise<z.infer<U>>;
};

Expected Behavior

Request handlers should be able to access authInfo from the extra parameter when the transport has validated an access token:

this.server.tool(
  "protectedAction",
  "A protected tool that requires authentication",
  {},
  async (params, extra) => {
    // extra.authInfo should contain validated token information
    const authInfo = extra.authInfo;
    if (authInfo) {
      console.log("Authenticated user:", authInfo.extra.userId);
      console.log("Token scopes:", authInfo.scopes);
    }
    // ... rest of handler logic
  }
);

Solution

This PR implements the following changes to support authInfo from the transport:

1. Enhanced Transport Interface

  • Updated McpSSETransport and McpStreamableHttpTransport to support MessageExtraInfo in their onmessage callbacks
  • Added support for passing requestInfo and authInfo through the transport layer
  • Transport onmessage callbacks now receive an optional extra parameter as the second argument

2. Request Context Tracking

  • Added requestInfo and authInfo properties to track the current request context
  • Implemented updateRequestInfo() method to capture and store request method, URL, and headers
  • Added updateAuthInfo() method to store authentication information
  • Request context is automatically captured during transport initialization

3. Authentication Resolution

  • Introduced resolveAuthInfo() method that subclasses can override to provide custom authentication validation
  • The resolver receives RequestInfo (including headers) and returns AuthInfo | undefined
  • This enables flexible authentication strategies (OAuth, JWT, API keys, etc.)
  • Added setCurrentAuthInfo() method for automatic auth validation during request processing

4. Integration Points

  • Updated SSE and Streamable HTTP transport initialization to capture request context via serializeRequestInfo()
  • Modified message forwarding to include requestInfo and authInfo in transport callbacks
  • Ensured both transport types receive the enhanced message context
  • Request info is updated on each new request to maintain current context

5. Type Safety and Utilities

  • Added proper TypeScript types for MessageExtraInfo, RequestInfo, and AuthInfo
  • Created ResolveAuthInfoArgs type for better type safety in auth resolvers
  • Enhanced transport interface definitions with optional extra parameter
  • Added utility functions for header conversion and request serialization

6. Testing

  • Added comprehensive tests for request info tracking in both SSE and Streamable HTTP transports
  • Added tests for authentication info handling with and without authorization headers
  • Verified that tool handlers receive the expected extra parameters
  • Added test tools (getRequestInfo, getAuthInfo) for validation

Usage Example

import { McpAgent, type ResolveAuthInfoArgs } from "@cloudflare/agents/mcp";

class MyMcpAgent extends McpAgent<Env, State, Props> {
  async resolveAuthInfo({ headers }: ResolveAuthInfoArgs) {
    const token = headers.authorization?.match(/^Bearer (.+)$/)?.[1];
    if (!token) return undefined;

    // Validate token and return auth info
    if (token === "valid-token") {
      return {
        token,
        clientId: "client-123",
        scopes: ["read", "write"],
        extra: { userId: "user-123" },
      };
    }
    return undefined;
  }

  async init() {
    // this.authInfo and this.requestInfo are available here

    // additionally, tool handlers now have access to authInfo and requestInfo
    this.server.tool(
      "protectedAction",
      "A protected tool that requires authentication",
      {},
      async (params, extra) => {
        if (!extra.authInfo) {
          throw new Error("Authentication required");
        }

        const userAgent = extra.requestInfo?.headers["user-agent"] || "Unknown";
        console.log(
          `User ${extra.authInfo.extra.userId} performing action from ${userAgent}`
        );

        // ... tool logic
        return {
          content: [
            {
              text: `Action completed for user ${extra.authInfo.extra.userId}`,
              type: "text"
            }
          ]
        };
      }
    );
  }
}

Testing

  • ✅ All existing tests pass
  • ✅ New tests verify request info tracking functionality in both transport types
  • ✅ New tests verify authentication info handling with and without auth headers
  • ✅ Both SSE and Streamable HTTP transports are covered
  • ✅ Edge cases (no auth header, invalid tokens) are handled gracefully
  • ✅ Test worker implementation demonstrates proper usage of resolveAuthInfo
  • ✅ Verified by publishing under @kentcdodds/tmp_agents@0.0.114-alpha.0 and using it in my own project

@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented Aug 28, 2025

🦋 Changeset detected

Latest 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
@kentcdodds kentcdodds force-pushed the pr/add-message-extra-info branch from 39d5f93 to 2ac318a Compare August 28, 2025 21:05
@kentcdodds
Copy link
Copy Markdown
Author

I changed the implementation quite a bit and verified it works in my project.

@kentcdodds
Copy link
Copy Markdown
Author

I've also verified this works in production as well.

@whoiskatrin
Copy link
Copy Markdown
Contributor

@deathbyknowledge can you please take a look? thank you

@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new Bot commented Aug 29, 2025

Open in StackBlitz

npm i https://pkg.pr.new/cloudflare/agents@426

commit: bf81165

@whoiskatrin
Copy link
Copy Markdown
Contributor

@kentcdodds do you want to add an .md in the docs or maybe mention this in the readme?

@kentcdodds
Copy link
Copy Markdown
Author

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.

@deathbyknowledge
Copy link
Copy Markdown
Contributor

deathbyknowledge commented Aug 30, 2025

Hey @kentcdodds, we currently have this PR #415 that refactors McpAgent to natively extend the Agent class and might simplify some of these issues. Mind clarifying why you need headers/auth in the callback? I might be able to address them there too

Is this.props insufficient to handle init() and your tool callbacks? What are you missing here?
I've read from a few examples that are setting feature flags through request headers to gate tools. This could be an example of how to implement that in conjunction with props, to make request-level info available in your props:

// 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 OAuthProvider we'd need to make sure we're only appending to the props in the apiHandler (once the provider has already set the props), otherwise they could get overridden.
Of course, you could also set any custom props for your session at the /authorization step.

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 MyMCP.serve("/").fetch(request, env, ctx), where ctx includes your custom props, as the example above.

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

@kentcdodds
Copy link
Copy Markdown
Author

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 extra argument to the transport onmessage callback to be spec compliant, so I think this PR still has value for that case.

@kentcdodds
Copy link
Copy Markdown
Author

Merged with main. I'd still love to get support for the extra argument 🙏

@kentcdodds
Copy link
Copy Markdown
Author

Anything I can do to get progress on merging this?

@deathbyknowledge
Copy link
Copy Markdown
Contributor

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 extra argument is not part of the spec but only part of the MCP TS SDK implementation.

Regardless, we're planning to re-think/improve the ergonomics around ctx.props which should solve the pain points you mention (afaik there are already valid workarounds), can't say when exactly that will happen though

@kentcdodds
Copy link
Copy Markdown
Author

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 new URL(request.url).origin into the tool handler. Right now I'm using this PR, but if this is not how that's going to work then I don't want to do that.

@deathbyknowledge
Copy link
Copy Markdown
Contributor

deathbyknowledge commented Sep 9, 2025

If you're using workers-oauth-provider to setup OAuth in your Worker, there are a few things to keep in mind. Take this example:

// 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 /authorize logic.

// 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 /authorize step is not implemented in the snippet. For the usual providers and how to rollout your own, have a look at the remove-mcp-* demos here.

Finally, you would use the props we just set in your McpAgent like so:

// 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 MyMCP.serve..... You get to skip the defaultHandler/apiHandlers shenanigans.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

MCP: Support authInfo

3 participants