Build a social media agent with the Vercel AI SDK: TypeScript tutorial

This tutorial builds a working Vercel AI SDK agent that posts to social media from a Next.js Route Handler. We use the SDK's experimental MCP client to load the Posta MCP server's tools, stream the agent's response back to the browser, and wire a separate Route Handler to receive Posta's HMAC-signed webhooks. Total time: 20 minutes.

What you'll build

A /api/post Route Handler that takes a prompt and returns the agent's streaming response — the agent picks which Posta tools to call, schedules the post, and reports back with the platform post URL once Posta's webhook confirms it published. The same setup runs in Node, Edge (for the REST path), Server Actions, or any other Vercel AI SDK surface.

Prerequisites

  • A Next.js 15+ app (or any Node runtime that supports the AI SDK).
  • A Posta account with at least one connected social account. 14-day free trial.
  • A Posta API token (Settings → API).
  • An Anthropic API key (or any other supported provider).

Step 1 — Install dependencies

# Examples below target AI SDK v5; if you're on v4, swap
#   stopWhen: stepCountIs(n)  →  maxSteps: n
npm i ai @ai-sdk/anthropic zod

Step 2 — Load the Posta MCP server

experimental_createMCPClient talks to the Posta MCP server over stdio. The tools it returns drop straight into generateText or streamText:

// app/api/post/route.ts
import { anthropic } from '@ai-sdk/anthropic'
import {
  experimental_createMCPClient as createMCPClient,
  generateText,
  stepCountIs,
} from 'ai'
import { Experimental_StdioMCPTransport as StdioTransport } from 'ai/mcp-stdio'

export const runtime = 'nodejs'

export async function POST(req: Request) {
  const { prompt } = await req.json()

  const mcp = await createMCPClient({
    transport: new StdioTransport({
      command: 'npx',
      args: ['-y', 'posta-mcp'],
      env: { POSTA_API_TOKEN: process.env.POSTA_API_TOKEN! },
    }),
  })

  try {
    const tools = await mcp.tools()
    const result = await generateText({
      model: anthropic('claude-sonnet-4-6'),
      tools,
      prompt,
      stopWhen: stepCountIs(8), // AI SDK v5; on v4, use maxSteps: 8
    })
    return Response.json({ text: result.text, steps: result.steps.length })
  } finally {
    await mcp.close()
  }
}

Step 3 — Stream tool calls to the browser

For an interactive UI, swap generateText for streamText and return the stream as a Response. The tool calls still run server-side; the conversation streams to the client:

import { streamText, stepCountIs } from 'ai'

// inside the Route Handler:
const tools = await mcp.tools()
const result = streamText({
  model: anthropic('claude-sonnet-4-6'),
  tools,
  prompt,
  stopWhen: stepCountIs(8), // AI SDK v5
  // Always close the MCP client when the stream completes.
  onFinish: () => mcp.close(),
})
return result.toTextStreamResponse()

Step 4 — Run it

export POSTA_API_TOKEN=posta_...
export ANTHROPIC_API_KEY=sk-ant-...

# Start the dev server
npm run dev

# In another terminal:
curl -X POST http://localhost:3000/api/post \
  -H "Content-Type: application/json" \
  -d '{ "prompt": "Draft a LinkedIn post about our v2 release and schedule it for tomorrow 9am CET. Save as draft." }'

Closing the loop with a webhook Route Handler

A second Route Handler receives Posta's outbound webhooks. When Posta fires post.published, this handler can kick off a follow-up AI SDK run — draft a reply, fan out to another network, post a Slack note:

// app/api/posta-webhook/route.ts
import { createHmac, timingSafeEqual } from 'node:crypto'

export const runtime = 'nodejs'

export async function POST(req: Request) {
  const sig = req.headers.get('x-posta-signature')
  if (!sig) return new Response('missing signature', { status: 401 })

  const raw = await req.text()
  const expected = createHmac('sha256', process.env.POSTA_WEBHOOK_SECRET!)
    .update(raw).digest('hex')
  const sigBuf = Buffer.from(sig)
  const expBuf = Buffer.from(expected)
  if (sigBuf.length !== expBuf.length || !timingSafeEqual(sigBuf, expBuf)) {
    return new Response('bad signature', { status: 401 })
  }

  const event = JSON.parse(raw)
  if (event.event === 'post.published') {
    // Enqueue a follow-up AI SDK run, e.g. draft a reply
    await enqueueReplyDraft(event.platform, event.platformPostUrl)
  }
  return new Response('ok')
}

The receiver hardening pattern (early-return on missing header, length-check before timingSafeEqual) is the same one walked through in webhook-driven social media agent loops.

When to use the REST path instead

For a single deterministic step in a chain, define a Zod-typed tool() wrapping the Posta REST API directly — no MCP transport, no tool discovery. The shape (two-step POST /v1/posts + POST /v1/posts/:id/schedule) is documented on the Vercel AI SDK integration page.

Pitfalls

  • The Stdio MCP transport needs Node. Edge runtime won't spawn the npx process. If you need Edge, use the REST-API-as-typed-tool path instead.
  • Always close() the MCP client. Wrap in try / finally (or use onFinish for streaming) — leaked stdio processes will pile up.
  • Cap the step count. On AI SDK v5 set stopWhen: stepCountIs(n); on v4 use maxSteps: n. Without one, a single tool call won't continue to a follow-up — and unbounded is a runaway risk on transient errors.
  • Start with "save as draft" prompts. Until you trust the agent's output, don't let it publish directly.

Where to go from here

Read the Vercel AI SDK integration reference for the full Zod-tool example. Compare with the Python framework setups in the LangChain tutorial and the OpenAI Agents SDK tutorial, or the TypeScript-native Mastra setup in the Mastra tutorial. 14-day free trial.

Ready to simplify your social media workflow?

Join creators and teams who save hours every week with Posta.