A headless Vercel Eve agent that reaches Buffer through a Zuplo MCP Gateway route instead of pointing at Buffer's hosted MCP server with a shared static key.
The gateway authenticates inbound clients with authorization-code + PKCE
only — no client_credentials / machine-to-machine grant. So a person signs
in once with npm run bootstrap to capture a refresh token; the agent runs
headless after that, refreshing silently. The gateway holds Buffer's upstream
key, fronts it with OAuth 2.1, curates tools, and emits a per-call audit —
none of which the agent manages. Pattern from the blog post "Use an MCP Gateway
With Vercel Eve Agents."
The agent carries no upstream secrets. Both its outbound paths — the model call and the tool calls — go through a Zuplo gateway that holds the real key.
| Path | Gateway | Agent carries | Real key held by | Configured in |
|---|---|---|---|---|
| Model calls | Zuplo AI Gateway | gateway key | the AI gateway | agent/agent.ts + env |
| Tool calls | Zuplo MCP Gateway | OAuth refresh token | the MCP gateway | connections/buffer.ts |
agent/agent.ts routes the model through the AI Gateway with
@ai-sdk/openai-compatible — the gateway exposes an OpenAI-compatible /v1
endpoint, holds the upstream provider key, and the agent only carries the
gateway's own key.
const zuplo = createOpenAICompatible({
name: "zuplo-ai-gateway",
baseURL: process.env.ZUPLO_AI_GATEWAY_URL!, // https://<your-gateway>/v1
apiKey: process.env.ZUPLO_AI_GATEWAY_KEY!,
});
export default defineAgent({
model: zuplo.chatModel(process.env.ZUPLO_AI_MODEL ?? "anthropic/claude-opus-4.8"),
modelContextWindowTokens: 200_000, // eve can't look up a custom model id's window
});Set ZUPLO_AI_GATEWAY_URL, ZUPLO_AI_GATEWAY_KEY, and ZUPLO_AI_MODEL in
.env. The model id is whatever your route expects (e.g.
anthropic/claude-opus-4.8).
agent/
agent.ts # model routed through a Zuplo AI Gateway (OpenAI-compatible)
instructions.md # system prompt
connections/
buffer.ts # the governed gateway route; getToken refreshes silently
lib/
oauth.ts # ~60-line refresh_token grant + RFC 8414 discovery, no deps
token-store.ts # persists the bootstrapped client_id + refresh token
schedules/
weekly-metrics.md # cron: summarize last-7-days posting activity across channels
scripts/
bootstrap.ts # one-time: sign in once, capture a refresh token
digest.ts # demo: fire the weekly schedule now and stream the summary
A connection's filename is its identifier, so connections/buffer.ts registers
as buffer. The model never sees the URL or credentials; it discovers tools
through the built-in connection__search and calls them by qualified name like
connection__buffer__list_channels.
The obvious "no human" pattern is the OAuth client_credentials grant. The
Zuplo MCP Gateway does not support it: it issues its own tokens via
authorization-code + PKCE and validates only gateway-issued tokens. The token
endpoint supports authorization_code and refresh_token grants — nothing
else. So the realistic headless pattern is:
- Once, with a human:
npm run bootstrapregisters a public client (DCR, RFC 7591), runs the PKCE flow in your browser, and stores the refresh token it gets back. - Every run, headless: the
bufferconnection'sgetTokencallsrefreshAccessToken(), which exchanges that refresh token for a fresh access token (RFC 6749 §6). No browser. Eve caches the token per step and refreshes ahead ofexpiresAt, so most steps make no token call at all.
Re-bootstrap only when the refresh token or the 90-day DCR client expires.
Prerequisites: Node 24.x, a Zuplo MCP Gateway route fronting Buffer (see the quickstart), and a Zuplo AI Gateway route for the model.
- Configure.
cp .env.example .env, then fill in:MCP_GATEWAY_URL— your gateway's Buffer route (https://<gateway>/mcp/buffer)ZUPLO_AI_GATEWAY_URL,ZUPLO_AI_GATEWAY_KEY,ZUPLO_AI_MODEL— the model route
- Install.
npm install - Bootstrap once.
npm run bootstrap— opens a sign-in URL, runs PKCE, stores theclient_id+ refresh token in.eve/oauth-store.json. - Run headless.
npm run dev— serves onhttp://127.0.0.1:3000.
Drive it over HTTP (how a schedule or service would):
curl -X POST http://127.0.0.1:3000/eve/v1/session \
-H 'content-type: application/json' \
-d '{"message":"List my Buffer channels."}'It refreshes a token silently and acts — no browser after the bootstrap.
agent/schedules/weekly-metrics.md is what makes the agent actually run on its
own. Its cron (0 9 * * 1 — Mondays 09:00 UTC) fires the agent in task mode
to pull the last 7 days of posts for every Buffer channel and combine them into
one cross-channel activity summary. No message, no human — just the cron.
Performance metrics (
get_aggregated_post_metrics) need Buffer'sinsights:readscope, which isn't granted here, so the digest summarizes posting activity fromlist_postsinstead.
eve dev never fires cron on cadence, so to demo it off-schedule, run the
schedule now and stream its summary to your terminal:
npm run dev # terminal 1
npm run digest # terminal 2 — fires weekly-metrics once, prints the summarynpm run digest reuses the schedule's own prompt (it triggers it by name
through Eve's dev-only dispatch route), so there's one source of truth for what
the job does. In production, eve start runs the cron itself; on Vercel each
schedule becomes a Vercel Cron Job.
| Concern | Static key in the agent | Route on the gateway |
|---|---|---|
| Identity | One shared API key | An OAuth client bound to the bootstrapped grant |
| Tools in scope | Everything Buffer ships | A curated subset; hidden tools return MethodNotFound |
| Upstream credential | Buffer key in the agent | Buffer key held by the gateway, never seen by the agent |
| Revocation | Rotate the key everywhere | Revoke the grant / rotate the client on the gateway |
| Audit | Whatever you build | A per-call analytics event per route |
Tool curation is a config change on the route, not a redeploy of the agent.
Want per-end-user identity instead of one service grant? Use Eve's
defineInteractiveAuthorizationso each user signs in through your IdP per session. That adds a user principal and a per-user token, at the cost of a browser round-trip on first use. This demo takes the headless path.
.eve/oauth-store.jsonholds theclient_id+ refresh token in plaintext for the demo. Swap it for a secrets manager / encrypted KV in production — the refresh token is a long-lived secret.- Build the MCP gateway route itself with the MCP Gateway quickstart.