Eve is Vercel’s filesystem-first framework for building durable agents, currently in beta, and connecting one to an MCP server is a single file. That makes it a clean place to show a pattern worth copying: instead of handing the agent a static API key for the upstream, point it at an OAuth-protected route on the Zuplo MCP Gateway and let the gateway hold the key.
I’ll use Buffer as the example. Its MCP server authenticates with one static key and nothing else, so the gateway is what gives it OAuth, tool curation, and a per-call audit. The catch: most agents run with no human at runtime, and the gateway dictates how a headless one can authenticate, not you. A grant that needs a browser is a non-starter for a cron job, so what the gateway’s OAuth allows decides the whole design. Check it before you write any agent code.
- You run Eve agents headless, on a schedule or behind a service
- The MCP server you need only ships a static API key
- You want OAuth, tool curation, and a per-call audit without a key in the agent
Anatomy of an Eve agent
Eve scaffolds with one command that drops in the eve, ai, and zod
dependencies and starts a dev server:
An agent is a directory of files rather than a config object:
The model lives in agent/agent.ts:
A connection’s filename becomes its identifier, so agent/connections/buffer.ts
registers as buffer. The model discovers its tools automatically and never
sees the connection’s URL or credentials.
Connect Eve directly to Buffer
Of course you can point the agent straight at Buffer, no gateway involved. The shortest path is a static token: the agent talks to Buffer’s hosted MCP server with an API key from Buffer’s settings:
That’s fine while it’s just you experimenting locally. Share it and you’re holding the static key’s usual problems:
- The key lives in the agent. Your Buffer credential sits in the agent runtime’s environment, copied wherever the agent runs.
- No curation. Every tool Buffer ships is in scope,
create_postanddelete_postincluded. - All-or-nothing revocation. Cut off access and you rotate the key everywhere at once.
Front Buffer with the gateway
The gateway fronts one or more upstream MCP servers behind a single
OAuth-protected route. The shape is
https://<your-gateway>/mcp/<upstream-name>, so a Buffer route reads like
https://mcp.yourdomain.xyz/mcp/buffer. It speaks OAuth 2.1, which Buffer
doesn’t offer yet, and holds Buffer’s API key upstream so the agent no longer
carries it.
The gateway’s auth model decides how your agent authenticates, so it’s the first
thing I checked. Its token endpoint, in Zuplo’s words, “accepts
authorization_code and refresh_token grants” and nothing else: no
client_credentials, no service account, nothing a headless process can present
on its own. Tokens come from authorization-code with PKCE, and self-registered
clients expire after 90 days, which matters later.
That leaves a headless agent one way in: a person signs in through the browser once to mint a refresh token, then the agent trades that refresh token for fresh access tokens on its own, with no browser after the first time.
Bootstrap once, run headless
The headless pattern is bootstrap once, refresh forever:
- Once, with a human (
npm run bootstrap). Register a public client via dynamic client registration (DCR) and run authorization-code + PKCE in the browser. The code exchange returns an access token and a refresh token; you save the refresh token. - Every run, headless. The connection’s
getTokentrades that saved refresh token for a fresh access token. No browser.
I keep the refresh in a helper, so the runtime connection stays tiny:
There’s no user principal to wire up: principalType stays at its default
"app", one grant shared across every session.
Eve ships a managed OAuth path of its own, Vercel Connect, and it’s worth saying
why this doesn’t use it. connect() manages consent, token storage, and refresh
for you, and for a per-user agent against a provider it already knows, it’s the
right tool. This isn’t that. The upstream is a route on your own gateway, not
one of Connect’s registered providers, and a scheduled service wants a single
app-level grant rather than a per-user sign-in. So the refresh lives in
getToken, the one auth hook that’s a plain async call instead of an
interactive flow.
The refresh half is the only other moving part, and the grant is the whole point of it:
Eve injects whatever getToken returns as Authorization: Bearer and refreshes
ahead of expiresAt on its own, so most steps reuse a cached token. It doesn’t
persist anything for you on this path though (only Vercel Connect does), so you
keep the refresh token yourself. That leaves four small files behind the
one-line connection:
| File | Runs | What it does |
|---|---|---|
scripts/bootstrap.ts | once, with a human | runs the one-time browser sign-in and stores the first grant |
agent/lib/token-store.ts | every run | reads and writes { clientId, refreshToken } |
agent/lib/oauth.ts | every run | trades the refresh token for a fresh access token |
agent/connections/buffer.ts | every run | the Eve connection; its getToken calls the refresh helper |
Only bootstrap.ts involves a person, and only the first time. The honest cost
is re-bootstrapping when the refresh token or the 90-day registered client
expires.
One thing to get right: that refresh token is a long-lived secret. The sample
keeps it in a plaintext .eve/oauth-store.json so it’s easy to inspect, but in
production token-store.ts should read from a secrets manager or encrypted KV,
not a file on disk.
Eve agent behind the MCP Gateway
The complete project, including the one-time bootstrap script and token store this post abstracts over, the refresh helper, the Buffer connection, and the weekly-metrics schedule.
Run the agent on a schedule
Headless auth removes the human from the credential, not from the trigger: something still has to start the agent. An Eve session can start plenty of ways, from an HTTP call or a Slack or GitHub mention to an inbound webhook or a schedule. The one that runs with nobody prompting it is the schedule, and schedules run in task mode, which the docs are blunt about:
A task-mode session runs to completion or fails, and cannot park to wait for a person or an OAuth sign-in.
The interactive connection authorizes by parking the turn for a browser sign-in,
and task mode forbids parking, so an interactive, per-user connection can’t run
on a schedule at all. The headless connection refreshes its token with a plain
getToken, an ordinary async call, so it runs fine under cron. The
refresh-token bootstrap isn’t just a way to go headless; it’s the only way to
put this agent on a schedule. Pick interactive and a human triggers every run.
Pick headless and you’ve unlocked cron. Same decision, two consequences.
The schedule that runs it is one file: the cron frontmatter sets the cadence,
the body is the prompt.
cron is a standard 5-field expression, so 0 9 * * 1 is Mondays at 9am. On
Vercel each schedule becomes a Vercel Cron Job evaluated in UTC. The model reads
the seven-day window from the prompt, so I keep it explicit there rather than
leaving it implied.
Worth knowing before you rely on it:
- Task mode discards the delivered output. Fine when the run streams to you
or the job is a side effect, but a digest someone should read needs a
destination: the handler form (
run) canreceive(...)into Slack or Discord, or you tell the agent to POST the summary to an endpoint. - Only a real cron runner fires it on cadence.
eve devnever fires schedules on their cron cadence;eve startor Vercel Cron does.
The headless trade-off
The gateway also supports the interactive flow, where a person signs in each session and the agent acts for that named user. Headless trades that identity for unattended runs:
| Gateway job | Interactive (per user) | Headless (bootstrapped grant) |
|---|---|---|
| Buffer key held upstream | yes | yes |
Tool curation (MethodNotFound) | yes | yes |
| Per-call audit | yes, per named person | yes, attributed to the one grant |
| Revocation | revoke the user in your IdP | revoke the grant or rotate the client |
| Which person did this | yes | no, it’s the service, by design |
| Human at runtime | every new session, first touch | none, one upfront bootstrap |
Tool curation is a configuration change on the route, not a redeploy of the
agent. For an analytics agent I drop create_post and delete_post from the
Buffer route: it can still read get_aggregated_post_metrics but can never
publish, because a tool the gateway doesn’t expose returns MethodNotFound.
MCP Gateway Quickstart
Build a virtual MCP server in the browser: pick an upstream, wire up OAuth against your IdP, curate the tools, and hand an agent the route URL.
Run it for real
We front Buffer through our own gateway exactly this way, so the pattern here is the one we run in-house. It is the same move we use to wrap a token-only MCP server in OAuth and the boundary Anthropic argued for without naming a vendor: contain the agent at a deterministic boundary rather than trusting the credential in its environment.
On the agent side it stays small: one connection file, a refresh helper you write once, and a one-file schedule. After a single sign-in you never repeat, the same agent pulls a weekly Buffer summary across every channel on a Monday-morning cron with no human in the loop, reaching Buffer through a route that holds the key, exposes only the tools you allow, and logs every call.
The MCP Gateway is in public beta on every plan, the free tier included. Spin up a free Zuplo project and point your first Eve agent at a governed route.
