Skip to content
OTFotf
All posts

Wire TanStack AI agents to OAuth-protected MCP via Vercel Connect

D
DaveAuthor
7 min read
Wire TanStack AI agents to OAuth-protected MCP via Vercel Connect

If you already run TanStack AI agents that call MCP tools, the boring failure mode is not that the model is dumb. It is that you pasted a long-lived MCP token into env, shipped it with the deployment, and now every rotate is a redeploy plus a hope that no screenshot of $CONNECT_* ever leaked. Vercel’s changelog for Vercel Connect + TanStack AI (2026-09-24) is aimed at that boundary: install @vercel/connect/tanstack-ai, send MCP traffic through connectMCPTransport, and when the server returns a consent challenge, catch getConsentChallenge and redirect the user. Tokens are issued at runtime at an owned route. They do not live in env for you to rotate.

This post is the keep-path PSEO for that wiring. It is not the EAS/Claude connector path in Expo MCP connector for Claude. Different runtime, different consent surface, different place the secret is allowed to exist.

What Connect is doing at the agent boundary

TanStack AI already knows how to call tools. MCP already knows how to expose tools behind OAuth. The missing piece on a Vercel deployment is a first-party transport that refuses to treat “bearer in $CONNECT_TOKEN” as the integration.

connectMCPTransport is that transport. You point the agent at MCP the same way you would any other tool host, except the bytes do not leave your app with a static secret. When the MCP server needs a user (or org) to grant access, Connect surfaces a consent challenge. Your route handles it with getConsentChallenge and redirects. After consent, a runtime token is bound to that session at the route you own.

That is the product claim worth keeping: owned-route MCP auth. The agent never becomes a secret store. Env never becomes a token locker. Rotation is “consent again,” not “edit production env and pray.”

If you still think in gateway terms — one billed path for model calls, one for tools — pair this with AI Gateway delegation. Gateway is the model hop. Connect is the OAuth hop for MCP. Do not collapse them.

Install and the only import that matters

From the changelog: install @vercel/connect/tanstack-ai. That package is the TanStack-specific adapter. Do not cargo-cult a mobile EAS connector, a generic MCP SDK auth helper, or a “put the token in $CONNECT_MCP_TOKEN” snippet from an older thread.

Keep Connect configuration in env as connection config — whatever $CONNECT_* names your dashboard emits — not as the user’s MCP access token. Path-only on the app side: your route owns consent and the agent endpoint.

Honest skeleton from the changelog: TanStack AI (chat, createMCPClient) with MCP attached through connectMCPTransport — not a raw fetch with Authorization from .env. The same HTTP handler catches getConsentChallenge and redirects; it does not retry the model with a guessed token. Storing the resulting token in env to “make CI simpler” undoes the feature.

11 production screens. Login, database, payments — all wired.

The SaaS Dashboard Kit ships everything already connected. Nothing to set up. Live demo at saas.otf-kit.dev.

See the live demo

connectMCPTransport: keep the agent ignorant of OAuth

The agent’s job is tool names, arguments, and when to stop. OAuth is not a tool. If you shove authorize URLs into system prompts, you will get a model that pastes tokens into logs.

connectMCPTransport sits under the tool layer. The provider is called before every MCP request, so the token is always fresh. Failed auth is not a chat message; it is a control-flow exception your route understands. That is why getConsentChallenge exists as a catchable object rather than a string the model might echo.

Trace transport errors and redirects, not bearer prefixes. Treat consent timeouts as user-wait. Multiple MCP servers can share the Connect consent pattern — not one shared env token.

getConsentChallenge and the redirect you actually ship

Consent is a browser problem. When getConsentChallenge fires on the agent route the UI posts to, that route should:

  1. Stop the current agent turn. Do not half-apply tool results.
  2. Redirect the user to the consent URL Connect gave you (changelog uses a 303).
  3. Land back on a path you own after the user accepts or denies.
  4. Resume the agent only after a runtime token exists for that session.

“Owned route” is load-bearing. If consent returns to a third-party page you do not control, someone else’s cookie becomes your security model. Keep the callback on your deployment — path-only, like /connect/consent next to the agent route.

Denied consent is first-class. Show that MCP tools are unavailable. Do not fall back to a pasted env token “just this once.”

Catch the challenge before the model runs. The changelog is explicit: if a consent error is raised inside a tool call instead, it reaches the model as an error string rather than the user as a redirect.

Runtime tokens vs env tokens

Pasted env MCP token vs Connect consent at owned route

Pasted MCP token in envConnect consent at the route
Where the secret livesDeployment env, often copied into previewsIssued at runtime after getConsentChallenge redirect
RotateChange env, redeploy, invalidate every replicaRe-consent; old runtime token dies with the session
Blast radiusAnyone with env or a leaked previewBound to the user/session that consented
Agent codeTempted to log headersTransport handles OAuth; agent sees tools
Fits TanStack on VercelWorks until the first leakWhat @vercel/connect/tanstack-ai is for

Env is fine for which Connect app you are, $CONNECT_* identifiers, and public MCP URLs. Env is not fine for the OAuth access token that talks to a customer’s MCP. If your runbook still says “rotate MCP_TOKEN quarterly,” you adopted a rename, not Connect.

Sequence you can keep in your head

TanStack agent to connectMCPTransport to consent to runtime token to OAuth MCP

  1. UI hits your owned agent route.
  2. TanStack AI starts a turn and needs an MCP tool.
  3. connectMCPTransport talks to the MCP server without a long-lived env bearer.
  4. Server demands OAuth. Transport yields a consent challenge.
  5. Route catches getConsentChallenge, redirects the browser.
  6. User consents. Connect issues a runtime token at your callback path.
  7. Agent retries the tool call. MCP sees a valid OAuth token. Env never held it.

If step 5 is “stuff the token into $CONNECT_MCP_TOKEN so we skip redirects in staging,” staging will ship to production.

Hard split from the Claude MCP connector

We already covered Expo MCP connector for Claude: EAS builds, Claude, a connector aimed at that editor/runtime pair. This Vercel Connect + TanStack path is a different product surface — different package (@vercel/connect/tanstack-ai), host (Vercel route + TanStack), auth UX (getConsentChallenge on your web route), and secret home (runtime token at the Vercel boundary). Copy-pasting those snippets into a TanStack Vercel app gives you two half-wired OAuth stacks.

Failure modes that are on you

Redirect loops mean the callback did not persist enough session state — fix the session, not the transport. Ignoring getConsentChallenge so the model “tries another tool” leaks partial work; abort the turn. Log that a challenge happened, not URL query params that may carry one-time codes. Use connectMCPTransport in local and preview too, or those environments invent env tokens again. Keep AI Gateway credentials (model inference) separate from MCP OAuth (tools) — see AI Gateway model picks.

A minimal implementation checklist

Grounded in the changelog — not invented API:

import { chat, toServerSentEventsResponse } from '@tanstack/ai';
import { createMCPClient } from '@tanstack/ai-mcp';
import { vercelGatewayText } from '@tanstack/ai-vercel-gateway';
import {
  connectMCPTransport,
  getConsentChallenge,
} from '@vercel/connect/tanstack-ai';

export async function POST(request: Request) {
  const { messages } = await request.json();
  const userId = await getUserId(request);
  try {
    const linear = await createMCPClient({
      transport: connectMCPTransport(
        { type: 'http', url: process.env.CONNECT_MCP_URL! },
        'oauth/linear',
        { subject: { type: 'user', id: userId } },
      ),
    });
    const stream = chat({
      adapter: vercelGatewayText('anthropic/claude-opus-5'),
      messages,
      mcp: { clients: [linear] },
    });
    return toServerSentEventsResponse(stream);
  } catch (err) {
    const challenge = getConsentChallenge(err);
    if (challenge) return Response.redirect(challenge.url, 303);
    throw err;
  }
}

Checklist: @vercel/connect/tanstack-ai adapter; connectMCPTransport; catch getConsentChallenge on the agent route; owned-path consent callback; no MCP access token in env; deny disables tools (no static fallback); logs omit tokens. If the changelog is silent on a step, do not invent it — read the entry.

Owned-route auth is the pattern we want when an agent can spend money or read a private repo: the browser user is in the loop, the serverless function is the boundary, and the model is a guest. Install @vercel/connect/tanstack-ai, send tools through connectMCPTransport, catch getConsentChallenge and redirect, let tokens issue at runtime. Leave env for $CONNECT_* config. Keep the Claude connector in its own post. For a durable product surface under the agent churn, start from the OTF templates you own — Connect still owns the OAuth hop.

Sources

vercelagentsarchitecture
OTF SaaS Dashboard Kit

Ship the product, not the setup.

  • 11 production screens — auth, billing, team, analytics, settings
  • Real database, payments, and login — all wired on day 1
  • AI configs pre-tuned so your agent extends instead of regenerates
Need more than components?

Full-stack kits.
Pay once, own the code.

Auth, database, and payments already connected — so you ship product, not setup. Or take every kit in the Bundle.

Everything Bundle — $149See full pricing

Get the free AI configs pack

Pre-tuned AI configs for Cursor, Claude, and Lovable — drop them in and your AI tool instantly understands your project.

No spam. Unsubscribe any time.

Prefer the free SDK? Star it on GitHub →