diff --git a/.changeset/fast-steaks-shine.md b/.changeset/fast-steaks-shine.md new file mode 100644 index 00000000000..3a396f14e77 --- /dev/null +++ b/.changeset/fast-steaks-shine.md @@ -0,0 +1,5 @@ +--- +'@mastra/mcp': patch +--- + +Expose authProvider option for HTTP-based MCP servers to enable OAuth authentication with automatic token refresh. The authProvider is automatically passed to both Streamable HTTP and SSE transports. diff --git a/packages/mcp/README.md b/packages/mcp/README.md index b91d214cb92..c121ff41cb2 100644 --- a/packages/mcp/README.md +++ b/packages/mcp/README.md @@ -378,7 +378,41 @@ mcp.prompts.onListChanged({ Prompt notifications are delivered via SSE or compatible transports. Register handlers before expecting notifications. -## SSE Authentication and Headers (Legacy Fallback) +## Authentication + +### OAuth Token Refresh with AuthProvider + +For HTTP-based MCP servers that require OAuth authentication with automatic token refresh, you can use the `authProvider` option: + +```typescript +const httpClient = new MCPClient({ + servers: { + myOAuthClient: { + url: new URL('https://your-mcp-server.com/mcp'), + authProvider: { + tokens: async () => { + // Your token refresh logic here + const refreshedToken = await refreshAccessToken(); + return { + token: refreshedToken, + type: 'Bearer', + }; + }, + // Additional OAuth provider methods as needed + redirectUrl: 'https://your-app.com/oauth/callback', + clientMetadata: { + /* ... */ + }, + // ... other OAuth provider properties + }, + }, + }, +}); +``` + +The `authProvider` is automatically passed to both Streamable HTTP and SSE transports. + +### SSE Authentication and Headers (Legacy Fallback) When the client falls back to using the legacy SSE (Server-Sent Events) transport and you need to include authentication or custom headers, you need to configure headers in a specific way. The standard `requestInit` headers won't work alone because SSE connections using the browser's `EventSource` API don't support custom headers directly. @@ -456,6 +490,7 @@ Here are the available options within `MastraMCPServerDefinition`: - **`url`**: (Optional, URL) For HTTP servers (Streamable HTTP or SSE): The URL of the server. - **`requestInit`**: (Optional, RequestInit) For HTTP servers: Request configuration for the fetch API. Used for the initial Streamable HTTP connection attempt and subsequent POST requests. Also used for the initial SSE connection attempt. - **`eventSourceInit`**: (Optional, EventSourceInit) **Only** for the legacy SSE fallback: Custom fetch configuration for SSE connections. Required when using custom headers with SSE. +- **`authProvider`**: (Optional, OAuthClientProvider) For HTTP servers: OAuth authentication provider for automatic token refresh. Automatically passed to both Streamable HTTP and SSE transports. - **`logger`**: (Optional, LogHandler) Optional additional handler for logging. - **`timeout`**: (Optional, number) Server-specific timeout in milliseconds, overriding the global client/configuration timeout. - **`capabilities`**: (Optional, ClientCapabilities) Server-specific capabilities configuration. @@ -469,6 +504,8 @@ Here are the available options within `MastraMCPServerDefinition`: - Multiple transport layers with automatic detection: - Stdio-based for local servers (`command`) - HTTP-based for remote servers (`url`): Tries Streamable HTTP first, falls back to legacy SSE. +- OAuth authentication with automatic token refresh (`authProvider`) +- Manual authentication headers for static tokens (`requestInit`, `eventSourceInit`) - Per-server logging capability using all standard MCP log levels - Automatic error handling and logging - Tool execution with context diff --git a/packages/mcp/src/client/client.test.ts b/packages/mcp/src/client/client.test.ts index bf7540f641a..b84ee690dd2 100644 --- a/packages/mcp/src/client/client.test.ts +++ b/packages/mcp/src/client/client.test.ts @@ -517,3 +517,69 @@ describe('MastraMCPClient - Elicitation Tests', () => { expect(elicitationResultText).toContain('Elicitation response content does not match requested schema'); }); }); + +describe('MastraMCPClient - AuthProvider Tests', () => { + let testServer: { + httpServer: HttpServer; + mcpServer: McpServer; + serverTransport: StreamableHTTPServerTransport; + baseUrl: URL; + }; + let client: InternalMastraMCPClient; + + beforeEach(async () => { + testServer = await setupTestServer(false); + }); + + afterEach(async () => { + await client?.disconnect().catch(() => {}); + await testServer?.mcpServer.close().catch(() => {}); + await testServer?.serverTransport.close().catch(() => {}); + testServer?.httpServer.close(); + }); + + it('should accept authProvider field in HTTP server configuration', async () => { + const mockAuthProvider = { test: 'authProvider' } as any; + + client = new InternalMastraMCPClient({ + name: 'auth-config-test', + server: { + url: testServer.baseUrl, + authProvider: mockAuthProvider, + }, + }); + + const serverConfig = (client as any).serverConfig; + expect(serverConfig.authProvider).toBe(mockAuthProvider); + expect(client).toBeDefined(); + expect(typeof client).toBe('object'); + }); + + it('should handle undefined authProvider gracefully', async () => { + client = new InternalMastraMCPClient({ + name: 'auth-undefined-test', + server: { + url: testServer.baseUrl, + authProvider: undefined, + }, + }); + + await client.connect(); + const tools = await client.tools(); + expect(tools).toHaveProperty('greet'); + }); + + it('should work without authProvider for HTTP transport (backward compatibility)', async () => { + client = new InternalMastraMCPClient({ + name: 'no-auth-http-client', + server: { + url: testServer.baseUrl, + }, + }); + + await client.connect(); + const tools = await client.tools(); + expect(tools).toHaveProperty('greet'); + }); + +}); diff --git a/packages/mcp/src/client/client.ts b/packages/mcp/src/client/client.ts index 6273c59e01a..08fc4855f88 100644 --- a/packages/mcp/src/client/client.ts +++ b/packages/mcp/src/client/client.ts @@ -74,6 +74,7 @@ type StdioServerDefinition = BaseServerOptions & { url?: never; // Exclude 'url' for Stdio requestInit?: never; // Exclude HTTP options for Stdio eventSourceInit?: never; // Exclude HTTP options for Stdio + authProvider?: never; // Exclude HTTP options for Stdio reconnectionOptions?: never; // Exclude Streamable HTTP specific options sessionId?: never; // Exclude Streamable HTTP specific options }; @@ -89,6 +90,7 @@ type HttpServerDefinition = BaseServerOptions & { // Include relevant options from SDK HTTP transport types requestInit?: StreamableHTTPClientTransportOptions['requestInit']; eventSourceInit?: SSEClientTransportOptions['eventSourceInit']; + authProvider?: StreamableHTTPClientTransportOptions['authProvider']; reconnectionOptions?: StreamableHTTPClientTransportOptions['reconnectionOptions']; sessionId?: StreamableHTTPClientTransportOptions['sessionId']; }; @@ -237,7 +239,7 @@ export class InternalMastraMCPClient extends MastraBase { } private async connectHttp(url: URL) { - const { requestInit, eventSourceInit } = this.serverConfig; + const { requestInit, eventSourceInit, authProvider } = this.serverConfig; this.log('debug', `Attempting to connect to URL: ${url}`); @@ -251,6 +253,7 @@ export class InternalMastraMCPClient extends MastraBase { const streamableTransport = new StreamableHTTPClientTransport(url, { requestInit, reconnectionOptions: this.serverConfig.reconnectionOptions, + authProvider: authProvider, }); await this.client.connect(streamableTransport, { timeout: @@ -269,7 +272,7 @@ export class InternalMastraMCPClient extends MastraBase { this.log('debug', 'Falling back to deprecated HTTP+SSE transport...'); try { // Fallback to SSE transport - const sseTransport = new SSEClientTransport(url, { requestInit, eventSourceInit }); + const sseTransport = new SSEClientTransport(url, { requestInit, eventSourceInit, authProvider }); await this.client.connect(sseTransport, { timeout: this.serverConfig.timeout ?? this.timeout }); this.transport = sseTransport; this.log('debug', 'Successfully connected using deprecated HTTP+SSE transport.');