Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/fast-steaks-shine.md
Original file line number Diff line number Diff line change
@@ -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.
39 changes: 38 additions & 1 deletion packages/mcp/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down Expand Up @@ -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.
Expand All @@ -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
Expand Down
66 changes: 66 additions & 0 deletions packages/mcp/src/client/client.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');
});

});
7 changes: 5 additions & 2 deletions packages/mcp/src/client/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
};
Expand All @@ -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'];
};
Expand Down Expand Up @@ -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}`);

Expand All @@ -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:
Expand All @@ -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.');
Expand Down