Skip to content
Docs

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.

  1. Link your directory and pull a development OpenID Connect (OIDC) token:

    Terminal
    vercel link
    vercel env pull

    The second command writes VERCEL_OIDC_TOKEN to .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 pull again if local requests start returning authentication errors. Vercel rotates the token automatically after deployment.

  2. Install Vercel Connect, AI SDK, and the AI SDK MCP client:

    pnpm add @vercel/connect ai @ai-sdk/mcp

    You don't need to store a Linear access token. connectAuthProvider requests a current token from Vercel Connect whenever the MCP client needs one.

  3. Create a route that connects to Linear's MCP server and passes its tools to streamText:

    app/api/chat/route.ts
    import { 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.js
    import { 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 connectAuthProvider for a token before each request. If the user hasn't granted access, Vercel Connect creates a consent challenge. The provider throws ConsentRequiredError, and the route redirects the browser to error.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.

    Derive the user subject ID from your authenticated session. Do not accept an arbitrary user ID from the request body.

  4. Start your app, then send a prompt to the route:

    Terminal
    pnpm dev

    In another terminal, make the request:

    Terminal
    curl -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 303 redirect when user_demo_123 has no Linear grant. Open the Location URL 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:

SubjectUse it forConsent behavior
{ type: 'user', id: userId }Actions on behalf of one signed-in userThat user grants OAuth access before the first tool call
{ type: 'app' }Bot, service, or tenant-wide actionsUses 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:

OptionBehavior
redirectUrlSends the user back to this URL after consent. If you omit it, Connect uses the connector's registered redirect.
onConsentRequiredReceives 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.
deviceCodeSet 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:

CheckQuestionOwner
OAuth consentCan this app access the user's Linear account?Vercel Connect through connectAuthProvider
Tool approvalCan 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.

Last updated August 28, 2026

Was this helpful?

supported.