AI SDK and MCP with Vercel Connect
Vercel Connect is available on all plans and is subject to the Vercel Connect terms
This tutorial connects an AI SDK route to Linear's Model Context Protocol (MCP) server. Vercel Connect supplies short-lived OAuth tokens, while AI SDK discovers and calls the server's tools.
- A Next.js App Router project
- A Vercel project linked to your local directory
- A Linear connector with the UID
oauth/linear, linked to the project's Development environment - A stable ID for each signed-in user
Follow the Vercel Connect quickstart if you haven't created and linked the connector.
Link your directory and pull a development OpenID Connect (OIDC) token:
Terminalvercel link vercel env pullThe second command writes
VERCEL_OIDC_TOKENto.env.local. Next.js loads this file during local development. Vercel Connect and AI Gateway read the token automatically.The token expires after about 12 hours. Run
vercel env pullagain if local requests start returning authentication errors. Vercel rotates the token automatically after deployment.Install Vercel Connect, AI SDK, and the AI SDK MCP client:
pnpm add @vercel/connect ai @ai-sdk/mcpYou don't need to store a Linear access token.
connectAuthProviderrequests a current token from Vercel Connect whenever the MCP client needs one.Create a route that connects to Linear's MCP server and passes its tools to
streamText:app/api/chat/route.tsimport { createMCPClient } from '@ai-sdk/mcp'; import { connectAuthProvider, ConsentRequiredError, } from '@vercel/connect/ai-sdk'; import { stepCountIs, streamText } from 'ai'; export const maxDuration = 60; export async function POST(request: Request) { const { prompt } = await request.json(); if (typeof prompt !== 'string') { return Response.json({ error: 'prompt is required' }, { status: 400 }); } // Replace this demo value with the ID from your authenticated session. const userId = 'user_demo_123'; let mcpClient: Awaited<ReturnType<typeof createMCPClient>> | undefined; try { mcpClient = await createMCPClient({ transport: { type: 'http', url: 'https://mcp.linear.app/mcp', authProvider: connectAuthProvider( 'oauth/linear', { subject: { type: 'user', id: userId }, scopes: ['read'], }, { redirectUrl: new URL('/', request.url).toString(), }, ), }, }); const tools = await mcpClient.tools(); const result = await streamText({ model: 'openai/gpt-5.6-sol', tools, prompt, stopWhen: stepCountIs(5), onFinish: async () => { await mcpClient?.close(); }, }); return result.toTextStreamResponse(); } catch (error) { await mcpClient?.close(); if (error instanceof ConsentRequiredError) { return Response.redirect(error.url, 303); } throw error; } }app/api/chat/route.jsimport { createMCPClient } from '@ai-sdk/mcp'; import { connectAuthProvider, ConsentRequiredError, } from '@vercel/connect/ai-sdk'; import { stepCountIs, streamText } from 'ai'; export const maxDuration = 60; export async function POST(request) { const { prompt } = await request.json(); if (typeof prompt !== 'string') { return Response.json({ error: 'prompt is required' }, { status: 400 }); } // Replace this demo value with the ID from your authenticated session. const userId = 'user_demo_123'; let mcpClient; try { mcpClient = await createMCPClient({ transport: { type: 'http', url: 'https://mcp.linear.app/mcp', authProvider: connectAuthProvider( 'oauth/linear', { subject: { type: 'user', id: userId }, scopes: ['read'], }, { redirectUrl: new URL('/', request.url).toString(), }, ), }, }); const tools = await mcpClient.tools(); const result = await streamText({ model: 'openai/gpt-5.6-sol', tools, prompt, stopWhen: stepCountIs(5), onFinish: async () => { await mcpClient?.close(); }, }); return result.toTextStreamResponse(); } catch (error) { await mcpClient?.close(); if (error instanceof ConsentRequiredError) { return Response.redirect(error.url, 303); } throw error; } }The MCP client asks
connectAuthProviderfor a token before each request. If the user hasn't granted access, Vercel Connect creates a consent challenge. The provider throwsConsentRequiredError, and the route redirects the browser toerror.url.Catch this error at the route boundary. Don't hold the original request open while the user grants access. After consent, start a new chat turn so the provider can request a token.
Start your app, then send a prompt to the route:
Terminalpnpm devIn another terminal, make the request:
Terminalcurl -i -N \ -X POST http://localhost:3000/api/chat \ -H "Content-Type: application/json" \ -d '{"prompt":"List my assigned Linear issues."}'The first request returns a
303redirect whenuser_demo_123has no Linear grant. Open theLocationURL in a browser and approve access. Send the request again to stream the model's response.
The subject controls whose provider identity the MCP tools use:
| Subject | Use it for | Consent behavior |
|---|---|---|
{ type: 'user', id: userId } | Actions on behalf of one signed-in user | That user grants OAuth access before the first tool call |
{ type: 'app' } | Bot, service, or tenant-wide actions | Uses the connector's app grant without per-user consent |
For app-level access, change the provider call:
const authProvider = connectAuthProvider('oauth/linear', {
subject: { type: 'app' },
scopes: ['read'],
});The connector must support an app-level grant, such as OAuth client credentials. See Tokens for subject and scope details.
connectAuthProvider supports three consent options:
| Option | Behavior |
|---|---|
redirectUrl | Sends the user back to this URL after consent. If you omit it, Connect uses the connector's registered redirect. |
onConsentRequired | Receives the consent challenge so you can show your own UI or persist an out-of-band challenge. Providing this callback suppresses the default ConsentRequiredError, so throw your own boundary error if the callback can't finish the response. |
deviceCode | Set to true to request device authorization. When the connector supports it, the challenge includes optional deviceCode and expiresAt values. |
Both ConsentRequiredError and the onConsentRequired challenge include url, connector, subject, request, and verifier. Use url for browser consent. For a headless client, set deviceCode: true and display the returned deviceCode until expiresAt.
Use the AI SDK entry point in an AI SDK app:
import { connectAuthProvider } from '@vercel/connect/ai-sdk';Use the MCP entry point when you plug the same OAuth provider into another MCP-spec client:
import { connectAuthProvider } from '@vercel/connect/mcp';Both paths export the same adapter and consent types. Choose one path for your client. The root @vercel/connect entry point doesn't export connectAuthProvider.
OAuth consent and tool approval answer different questions:
| Check | Question | Owner |
|---|---|---|
| OAuth consent | Can this app access the user's Linear account? | Vercel Connect through connectAuthProvider |
| Tool approval | Can the agent run this specific tool call with these arguments? | AI SDK through toolApproval |
Connect handles account access. It doesn't approve each MCP action. For sensitive tools that create, update, delete, send, or spend, add AI SDK tool approval separately.
Use the exact name returned by your MCP server when you configure AI SDK tool approval. Your chat UI must collect the decision and include the approval response in the next turn.
- Authentication: Learn how deployments and local code authenticate with Vercel Connect.
- Tokens: Choose subjects, scopes, and installations.
- TypeScript SDK Reference: Review Connect token and authorization APIs.
Was this helpful?