diff --git a/.changeset/fluffy-poems-live.md b/.changeset/fluffy-poems-live.md
new file mode 100644
index 000000000000..fb9c9df41f97
--- /dev/null
+++ b/.changeset/fluffy-poems-live.md
@@ -0,0 +1,20 @@
+---
+'ai': patch
+'@ai-sdk/mcp': major
+---
+
+feat(ai): add OAuth for MCP clients + refactor to new package
+
+This change replaces
+
+```ts
+import { experimental_createMCPClient } from 'ai';
+import { Experimental_StdioMCPTransport } from 'ai/mcp-stdio';
+```
+
+with
+
+```ts
+import { experimental_createMCPClient } from '@ai-sdk/mcp';
+import { Experimental_StdioMCPTransport } from '@ai-sdk/mcp/mcp-stdio';
+```
diff --git a/content/cookbook/01-next/73-mcp-tools.mdx b/content/cookbook/01-next/73-mcp-tools.mdx
index f6ea163630f8..694d6b87f46d 100644
--- a/content/cookbook/01-next/73-mcp-tools.mdx
+++ b/content/cookbook/01-next/73-mcp-tools.mdx
@@ -12,24 +12,25 @@ The AI SDK supports Model Context Protocol (MCP) tools by offering a lightweight
Let's create a route handler for `/api/completion` that will generate text based on the input prompt and MCP tools that can be called at any time during a generation. The route will call the `streamText` function from the `ai` module, which will then generate text based on the input prompt and stream it to the client.
-To use the `StreamableHTTPClientTransport`, you will need to install the official Typescript SDK for Model Context Protocol:
+If you prefer to use the official transports (optional), install the official TypeScript SDK for Model Context Protocol:
```ts filename="app/api/completion/route.ts"
-import { experimental_createMCPClient, streamText } from 'ai';
-import { Experimental_StdioMCPTransport } from 'ai/mcp-stdio';
+import { experimental_createMCPClient, streamText } from '@ai-sdk/mcp';
+import { Experimental_StdioMCPTransport } from '@ai-sdk/mcp/mcp-stdio';
import { openai } from '@ai-sdk/openai';
-import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio';
-import { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse';
-import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp';
+// Optional: Official transports if you prefer them
+// import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio';
+// import { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse';
+// import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp';
export async function POST(req: Request) {
const { prompt }: { prompt: string } = await req.json();
try {
- // Initialize an MCP client to connect to a `stdio` MCP server:
- const transport = new StdioClientTransport({
+ // Initialize an MCP client to connect to a `stdio` MCP server (local only):
+ const transport = new Experimental_StdioMCPTransport({
command: 'node',
args: ['src/stdio/dist/server.js'],
});
@@ -38,22 +39,40 @@ export async function POST(req: Request) {
transport,
});
- // You can also connect to StreamableHTTP MCP servers
- const httpTransport = new StreamableHTTPClientTransport(
- new URL('http://localhost:3000/mcp'),
- );
+ // Connect to an HTTP MCP server directly via the client transport config
const httpClient = await experimental_createMCPClient({
- transport: httpTransport,
+ transport: {
+ type: 'http',
+ url: 'http://localhost:3000/mcp',
+
+ // optional: configure headers
+ // headers: { Authorization: 'Bearer my-api-key' },
+
+ // optional: provide an OAuth client provider for automatic authorization
+ // authProvider: myOAuthClientProvider,
+ },
});
- // Alternatively, you can connect to a Server-Sent Events (SSE) MCP server:
- const sseTransport = new SSEClientTransport(
- new URL('http://localhost:3000/sse'),
- );
+ // Connect to a Server-Sent Events (SSE) MCP server directly via the client transport config
const sseClient = await experimental_createMCPClient({
- transport: sseTransport,
+ transport: {
+ type: 'sse',
+ url: 'http://localhost:3000/sse',
+
+ // optional: configure headers
+ // headers: { Authorization: 'Bearer my-api-key' },
+
+ // optional: provide an OAuth client provider for automatic authorization
+ // authProvider: myOAuthClientProvider,
+ },
});
+ // Alternatively, you can create transports with the official SDKs instead of direct config:
+ // const httpTransport = new StreamableHTTPClientTransport(new URL('http://localhost:3000/mcp'));
+ // const httpClient = await experimental_createMCPClient({ transport: httpTransport });
+ // const sseTransport = new SSEClientTransport(new URL('http://localhost:3000/sse'));
+ // const sseClient = await experimental_createMCPClient({ transport: sseTransport });
+
const toolSetOne = await stdioClient.tools();
const toolSetTwo = await httpClient.tools();
const toolSetThree = await sseClient.tools();
diff --git a/content/cookbook/05-node/54-mcp-tools.mdx b/content/cookbook/05-node/54-mcp-tools.mdx
index 8b8bb0018aec..9f8663dbfa84 100644
--- a/content/cookbook/05-node/54-mcp-tools.mdx
+++ b/content/cookbook/05-node/54-mcp-tools.mdx
@@ -8,25 +8,30 @@ tags: ['node', 'tool use', 'agent', 'mcp']
The AI SDK supports Model Context Protocol (MCP) tools by offering a lightweight client that exposes a `tools` method for retrieving tools from a MCP server. After use, the client should always be closed to release resources.
-Use the official Model Context Protocol Typescript SDK to create the connection to the MCP server.
+If you prefer to use the official transports (optional), install the official Model Context Protocol TypeScript SDK.
```ts
-import { experimental_createMCPClient, generateText, stepCountIs } from 'ai';
-import { Experimental_StdioMCPTransport } from 'ai/mcp-stdio';
+import {
+ experimental_createMCPClient,
+ generateText,
+ stepCountIs,
+} from '@ai-sdk/mcp';
+import { Experimental_StdioMCPTransport } from '@ai-sdk/mcp/mcp-stdio';
import { openai } from '@ai-sdk/openai';
-import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio';
-import { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse';
-import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp';
+// Optional: Official transports if you prefer them
+// import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio';
+// import { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse';
+// import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp';
let clientOne;
let clientTwo;
let clientThree;
try {
- // Initialize an MCP client to connect to a `stdio` MCP server:
- const transport = new StdioClientTransport({
+ // Initialize an MCP client to connect to a `stdio` MCP server (local only):
+ const transport = new Experimental_StdioMCPTransport({
command: 'node',
args: ['src/stdio/dist/server.js'],
});
@@ -35,22 +40,40 @@ try {
transport,
});
- // You can also connect to StreamableHTTP MCP servers
- const httpTransport = new StreamableHTTPClientTransport(
- new URL('http://localhost:3000/mcp'),
- );
+ // Connect to an HTTP MCP server directly via the client transport config
const clientTwo = await experimental_createMCPClient({
- transport: httpTransport,
+ transport: {
+ type: 'http',
+ url: 'http://localhost:3000/mcp',
+
+ // optional: configure headers
+ // headers: { Authorization: 'Bearer my-api-key' },
+
+ // optional: provide an OAuth client provider for automatic authorization
+ // authProvider: myOAuthClientProvider,
+ },
});
- // Alternatively, you can connect to a Server-Sent Events (SSE) MCP server:
- const sseTransport = new SSEClientTransport(
- new URL('http://localhost:3000/sse'),
- );
+ // Connect to a Server-Sent Events (SSE) MCP server directly via the client transport config
const clientThree = await experimental_createMCPClient({
- transport: sseTransport,
+ transport: {
+ type: 'sse',
+ url: 'http://localhost:3000/sse',
+
+ // optional: configure headers
+ // headers: { Authorization: 'Bearer my-api-key' },
+
+ // optional: provide an OAuth client provider for automatic authorization
+ // authProvider: myOAuthClientProvider,
+ },
});
+ // Alternatively, you can create transports with the official SDKs instead of direct config:
+ // const httpTransport = new StreamableHTTPClientTransport(new URL('http://localhost:3000/mcp'));
+ // clientTwo = await experimental_createMCPClient({ transport: httpTransport });
+ // const sseTransport = new SSEClientTransport(new URL('http://localhost:3000/sse'));
+ // clientThree = await experimental_createMCPClient({ transport: sseTransport });
+
const toolSetOne = await clientOne.tools();
const toolSetTwo = await clientTwo.tools();
const toolSetThree = await clientThree.tools();
diff --git a/content/docs/03-ai-sdk-core/16-mcp-tools.mdx b/content/docs/03-ai-sdk-core/16-mcp-tools.mdx
index 05d40b9a7910..e3095ab41672 100644
--- a/content/docs/03-ai-sdk-core/16-mcp-tools.mdx
+++ b/content/docs/03-ai-sdk-core/16-mcp-tools.mdx
@@ -18,16 +18,35 @@ We recommend using HTTP transport (like `StreamableHTTPClientTransport`) for pro
Create an MCP client using one of the following transport options:
-- **HTTP transport (Recommended)**: Use transports from MCP's official TypeScript SDK like `StreamableHTTPClientTransport` for production deployments
+- **HTTP transport (Recommended)**: Either configure HTTP directly via the client using `transport: { type: 'http', ... }`, or use MCP's official TypeScript SDK `StreamableHTTPClientTransport`
- SSE (Server-Sent Events): An alternative HTTP-based transport
- `stdio`: For local development only. Uses standard input/output streams for local MCP servers
### HTTP Transport (Recommended)
-For production deployments, we recommend using HTTP transports like `StreamableHTTPClientTransport` from MCP's official TypeScript SDK:
+For production deployments, we recommend using the HTTP transport. You can configure it directly on the client:
```typescript
-import { experimental_createMCPClient as createMCPClient } from 'ai';
+import { experimental_createMCPClient as createMCPClient } from '@ai-sdk/mcp';
+
+const mcpClient = await createMCPClient({
+ transport: {
+ type: 'http',
+ url: 'https://your-server.com/mcp',
+
+ // optional: configure HTTP headers
+ headers: { Authorization: 'Bearer my-api-key' },
+
+ // optional: provide an OAuth client provider for automatic authorization
+ authProvider: myOAuthClientProvider,
+ },
+});
+```
+
+Alternatively, you can use `StreamableHTTPClientTransport` from MCP's official TypeScript SDK:
+
+```typescript
+import { experimental_createMCPClient as createMCPClient } from '@ai-sdk/mcp';
import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
const url = new URL('https://your-server.com/mcp');
@@ -40,20 +59,21 @@ const mcpClient = await createMCPClient({
### SSE Transport
-SSE provides an alternative HTTP-based transport option. Configure it with a `type` and `url` property:
+SSE provides an alternative HTTP-based transport option. Configure it with a `type` and `url` property. You can also provide an `authProvider` for OAuth:
```typescript
-import { experimental_createMCPClient as createMCPClient } from 'ai';
+import { experimental_createMCPClient as createMCPClient } from '@ai-sdk/mcp';
const mcpClient = await createMCPClient({
transport: {
type: 'sse',
url: 'https://my-server.com/sse',
- // optional: configure HTTP headers, e.g. for authentication
- headers: {
- Authorization: 'Bearer my-api-key',
- },
+ // optional: configure HTTP headers
+ headers: { Authorization: 'Bearer my-api-key' },
+
+ // optional: provide an OAuth client provider for automatic authorization
+ authProvider: myOAuthClientProvider,
},
});
```
@@ -67,10 +87,10 @@ const mcpClient = await createMCPClient({
The Stdio transport can be imported from either the MCP SDK or the AI SDK:
```typescript
-import { experimental_createMCPClient as createMCPClient } from 'ai';
+import { experimental_createMCPClient as createMCPClient } from '@ai-sdk/mcp';
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
// Or use the AI SDK's stdio transport:
-// import { Experimental_StdioMCPTransport as StdioClientTransport } from 'ai/mcp-stdio';
+// import { Experimental_StdioMCPTransport as StdioClientTransport } from '@ai-sdk/mcp/mcp-stdio';
const mcpClient = await createMCPClient({
transport: new StdioClientTransport({
@@ -87,8 +107,12 @@ You can also bring your own transport by implementing the `MCPTransport` interfa
The client returned by the `experimental_createMCPClient` function is a
lightweight client intended for use in tool conversion. It currently does not
- support all features of the full MCP client, such as: authorization, session
+ support all features of the full MCP client, such as: session
management, resumable streams, and receiving notifications.
+
+Authorization via OAuth is supported when using the AI SDK MCP HTTP or SSE
+transports by providing an `authProvider`.
+
### Closing the MCP Client
diff --git a/content/docs/07-reference/01-ai-sdk-core/23-create-mcp-client.mdx b/content/docs/07-reference/01-ai-sdk-core/23-create-mcp-client.mdx
index 9a2a19ea8d09..d7e8929403a5 100644
--- a/content/docs/07-reference/01-ai-sdk-core/23-create-mcp-client.mdx
+++ b/content/docs/07-reference/01-ai-sdk-core/23-create-mcp-client.mdx
@@ -14,7 +14,7 @@ This feature is experimental and may change or be removed in the future.
## Import
@@ -79,11 +79,11 @@ This feature is experimental and may change or be removed in the future.
],
},
{
- type: 'McpSSEServerConfig',
+ type: 'MCPTransportConfig',
parameters: [
{
name: 'type',
- type: "'sse'",
+ type: "'sse' | 'http",
description: 'Use Server-Sent Events for communication',
},
{
@@ -98,6 +98,13 @@ This feature is experimental and may change or be removed in the future.
description:
'Additional HTTP headers to be sent with requests.',
},
+ {
+ name: 'authProvider',
+ type: 'OAuthClientProvider',
+ isOptional: true,
+ description:
+ 'Optional OAuth provider for authorization to access protected remote MCP servers.',
+ },
],
},
],
@@ -160,8 +167,8 @@ Returns a Promise that resolves to an `MCPClient` with the following methods:
## Example
```typescript
-import { experimental_createMCPClient, generateText } from 'ai';
-import { Experimental_StdioMCPTransport } from 'ai/mcp-stdio';
+import { experimental_createMCPClient, generateText } from '@ai-sdk/mcp';
+import { Experimental_StdioMCPTransport } from '@ai-sdk/mcp/mcp-stdio';
import { openai } from '@ai-sdk/openai';
let client;
diff --git a/examples/mcp/package.json b/examples/mcp/package.json
index 14b2806cf5f7..fcb576f6d7a9 100644
--- a/examples/mcp/package.json
+++ b/examples/mcp/package.json
@@ -5,6 +5,8 @@
"scripts": {
"sse:server": "tsx src/sse/server.ts",
"sse:client": "tsx src/sse/client.ts",
+ "sse-auth:server": "tsx src/mcp-with-auth/server.ts",
+ "sse-auth:client": "tsx src/mcp-with-auth/client.ts",
"stdio:build": "tsc src/stdio/server.ts --outDir src/stdio/dist --target es2023 --module nodenext",
"stdio:client": "tsx src/stdio/client.ts",
"http:server": "tsx src/http/server.ts",
@@ -22,6 +24,7 @@
"zod": "3.25.76"
},
"devDependencies": {
+ "@ai-sdk/mcp": "workspace:*",
"@types/express": "5.0.0",
"@types/node": "20.17.24",
"tsx": "4.19.2",
diff --git a/examples/mcp/src/http/client.ts b/examples/mcp/src/http/client.ts
index 048f576c5e93..c285089c060c 100644
--- a/examples/mcp/src/http/client.ts
+++ b/examples/mcp/src/http/client.ts
@@ -1,12 +1,11 @@
import { openai } from '@ai-sdk/openai';
import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
+import { generateText, stepCountIs } from 'ai';
+import 'dotenv/config';
import {
experimental_createMCPClient as createMCPClient,
experimental_MCPClient as MCPClient,
- generateText,
- stepCountIs,
-} from 'ai';
-import 'dotenv/config';
+} from '@ai-sdk/mcp';
async function main() {
const transport = new StreamableHTTPClientTransport(
diff --git a/examples/mcp/src/mcp-with-auth/client.ts b/examples/mcp/src/mcp-with-auth/client.ts
new file mode 100644
index 000000000000..ae3f2168cd82
--- /dev/null
+++ b/examples/mcp/src/mcp-with-auth/client.ts
@@ -0,0 +1,224 @@
+import { openai } from '@ai-sdk/openai';
+import { generateText, stepCountIs } from 'ai';
+
+/**
+ * @deprecated Use the `@ai-sdk/mcp` package instead.
+ *
+import { experimental_createMCPClient, auth } from 'ai';
+import type {
+ OAuthClientProvider,
+ OAuthClientInformation,
+ OAuthClientMetadata,
+ OAuthTokens,
+} from 'ai';
+*/
+
+import { experimental_createMCPClient, auth } from '@ai-sdk/mcp';
+import 'dotenv/config';
+import type {
+ OAuthClientProvider,
+ OAuthClientInformation,
+ OAuthClientMetadata,
+ OAuthTokens,
+} from '@ai-sdk/mcp';
+import { createServer } from 'node:http';
+import { exec } from 'node:child_process';
+
+class InMemoryOAuthClientProvider implements OAuthClientProvider {
+ private _tokens?: OAuthTokens;
+ private _codeVerifier?: string;
+ private _clientInformation?: OAuthClientInformation;
+ private _redirectUrl: string | URL =
+ `http://localhost:${process.env.MCP_CALLBACK_PORT ?? 8090}/callback`;
+
+ async tokens(): Promise {
+ return this._tokens;
+ }
+ async saveTokens(tokens: OAuthTokens): Promise {
+ this._tokens = tokens;
+ }
+ async redirectToAuthorization(authorizationUrl: URL): Promise {
+ const cmd =
+ process.platform === 'win32'
+ ? `start ${authorizationUrl.toString()}`
+ : process.platform === 'darwin'
+ ? `open "${authorizationUrl.toString()}"`
+ : `xdg-open "${authorizationUrl.toString()}"`;
+ exec(cmd, error => {
+ if (error) {
+ console.error(
+ 'Open this URL to continue:',
+ authorizationUrl.toString(),
+ );
+ }
+ });
+ }
+ async saveCodeVerifier(codeVerifier: string): Promise {
+ this._codeVerifier = codeVerifier;
+ }
+ async codeVerifier(): Promise {
+ if (!this._codeVerifier) throw new Error('No code verifier saved');
+ return this._codeVerifier;
+ }
+ get redirectUrl(): string | URL {
+ return this._redirectUrl;
+ }
+ get clientMetadata(): OAuthClientMetadata {
+ return {
+ client_name: 'AI SDK MCP OAuth Example',
+ redirect_uris: [String(this._redirectUrl)],
+ grant_types: ['authorization_code', 'refresh_token'],
+ response_types: ['code'],
+ token_endpoint_auth_method: 'client_secret_post',
+ };
+ }
+ async clientInformation(): Promise {
+ return this._clientInformation;
+ }
+ async saveClientInformation(info: OAuthClientInformation): Promise {
+ this._clientInformation = info;
+ }
+ addClientAuthentication = async (
+ headers: Headers,
+ params: URLSearchParams,
+ _url: string | URL,
+ ): Promise => {
+ const info = this._clientInformation;
+ if (!info) {
+ return;
+ }
+
+ const method = (info as any).token_endpoint_auth_method as
+ | 'client_secret_post'
+ | 'client_secret_basic'
+ | 'none'
+ | undefined;
+
+ const hasSecret = Boolean((info as any).client_secret);
+ const clientId = info.client_id;
+ const clientSecret = (info as any).client_secret as string | undefined;
+
+ // Prefer the method assigned at registration; fall back sensibly
+ const chosen = method ?? (hasSecret ? 'client_secret_post' : 'none');
+
+ if (chosen === 'client_secret_basic') {
+ if (!clientSecret) {
+ params.set('client_id', clientId);
+ return;
+ }
+ const credentials = Buffer.from(`${clientId}:${clientSecret}`).toString(
+ 'base64',
+ );
+ headers.set('Authorization', `Basic ${credentials}`);
+ return;
+ }
+
+ if (chosen === 'client_secret_post') {
+ params.set('client_id', clientId);
+ if (clientSecret) params.set('client_secret', clientSecret);
+ return;
+ }
+
+ // none (public client)
+ params.set('client_id', clientId);
+ };
+ async invalidateCredentials(scope: 'all' | 'client' | 'tokens' | 'verifier') {
+ if (scope === 'all' || scope === 'tokens') this._tokens = undefined;
+ if (scope === 'all' || scope === 'client')
+ this._clientInformation = undefined;
+ if (scope === 'all' || scope === 'verifier') this._codeVerifier = undefined;
+ }
+}
+
+async function authorizeWithPkceOnce(
+ authProvider: OAuthClientProvider,
+ serverUrl: string,
+ waitForCode: () => Promise,
+): Promise {
+ const result = await auth(authProvider, { serverUrl: new URL(serverUrl) });
+ if (result !== 'AUTHORIZED') {
+ const authorizationCode = await waitForCode();
+ await auth(authProvider, {
+ serverUrl: new URL(serverUrl),
+ authorizationCode,
+ });
+ }
+}
+
+function waitForAuthorizationCode(port: number): Promise {
+ return new Promise((resolve, reject) => {
+ const server = createServer((req, res) => {
+ if (!req.url) {
+ res.writeHead(400).end('Bad request');
+ return;
+ }
+ const url = new URL(req.url, `http://localhost:${port}`);
+ if (url.pathname !== '/callback') {
+ res.writeHead(404).end('Not found');
+ return;
+ }
+ const code = url.searchParams.get('code');
+ const err = url.searchParams.get('error');
+ if (code) {
+ res.writeHead(200, { 'Content-Type': 'text/html' });
+ res.end(
+ '