Run on Vercel without secrets
Deploy the self-serve starter MCP server or agent to Vercel, where it authenticates to Keycard with Vercel's OIDC token instead of a client secret.
This guide walks through the Deploy step of the Keycard Console onboarding guide on Vercel: deploying the starter MCP server or agent you enrolled, then giving Keycard the values it needs to trust the deployment. The MCP server answers your agent’s requests; the agent runs once a day on Vercel Cron. It’s written for people who don’t use Vercel every day, and it covers the places where that step usually goes wrong.
Vercel mints a short-lived OIDC token for every function invocation. Keycard trusts your Vercel team’s issuer and pins a Workload Identity credential on your Application to the project’s production environment. The deployment presents that token on each token exchange, so it holds no client secret. Authenticate deployed workloads explains the model; this page is the Vercel walkthrough.
Before you start
Section titled “Before you start”-
You finished the onboarding guide’s earlier steps, and the starter runs locally:
You cloned the starter (
git clone https://github.com/keycardai/getting-started-mcp local-tools) and rankeycard new mcpin it, which wrote the.envfile this guide pushes from.You cloned the starter (
git clone https://github.com/keycardai/getting-started-agent agent), rankeycard new agentin it, and addedANTHROPIC_API_KEYto the.envfile it wrote. -
A Vercel account. The Hobby plan works.
-
Homebrew or npm, to install the Vercel CLI.
Run every command below from the starter’s folder (local-tools or agent).
Deploy the starter
Section titled “Deploy the starter”-
Install the Vercel CLI and sign in
Terminal window brew install vercel-clivercel loginWithout Homebrew,
npm i -g vercelinstalls the same CLI.vercel loginfinishes in your browser. -
Link the folder to a Vercel project
Terminal window vercel linkThe CLI asks which scope (team) to use and whether to link an existing project or create one. Note both answers: the Deploy form asks for the team and the project name. The link is saved in
.vercel/project.json, which holds IDs rather than names, so it isn’t where to read them back from. -
Push your settings to the project
Terminal window make env-pushThis copies one value from
.envto the project’s production environment:KEYCARD_ZONE_URL, the starter’s name for your Zone’s Issuer URL. The deployed server reads it at startup and returns a 500 without it. The agent starter copies three values:KEYCARD_ZONE_URL,KEYCARD_RESOURCE_URLandANTHROPIC_API_KEY. Either way,KEYCARD_CLIENT_IDandKEYCARD_CLIENT_SECRETstay in your local.env, because the deployment authenticates with Vercel’s workload identity. A successful run printsset KEYCARD_ZONE_URL -> production.If it prints
This directory is not linked to a Vercel project, runvercel linkfirst. -
Make a local commit as yourself
Vercel only creates a production deployment from a commit whose author has access to your Vercel team, and every commit in the cloned starter is authored by Keycard. Make an empty commit with the email on your Vercel account:
Terminal window git commit --allow-empty -m "Deploy to Vercel" --author="Abed Nadir <abed.nadir@acme.com>"Use your own name and Vercel email. Don’t push the commit:
vercel deployuploads your local folder, so it never needs to reach GitHub. -
Deploy to production
Terminal window vercel deploy --prodThe
--prodflag matters. Without it, Vercel makes a preview deployment, which the Workload Identity credential doesn’t trust (see Preview deployments don’t authenticate).
Find the production URL
Section titled “Find the production URL”When the deploy finishes, the CLI prints two URLs:
- Production: the URL of this one deployment, with a generated suffix, for example
https://local-tools-a1b2c3d4e-acme-corp.vercel.app. It changes on every deploy. - Aliased: the project’s stable production domain, for example
https://local-tools.vercel.app. This is the one to give Keycard.
The production domain is usually https://<project-name>.vercel.app, but not always. .vercel.app names are global, so when another Vercel user already holds your project’s name, Vercel adds a suffix: a project named local-tools can land on https://local-tools-ten.vercel.app. Don’t type the URL from the project name; copy the Aliased line, or the domain listed on the project’s Domains page in the Vercel dashboard. A custom domain replaces it too, if you add one. The agent only needs it to run a cycle on demand.
Tell Keycard where it deployed
Section titled “Tell Keycard where it deployed”Back in the onboarding guide, the form depends on the starter:
The Tell Keycard where it deployed form asks for three values:
| Field | What to enter | Where to find it |
|---|---|---|
| Vercel team | The team slug, exactly as Vercel spells it | The part of your team’s URL after vercel.com/, for example acme-corp in vercel.com/acme-corp, or the ID column of vercel teams ls. Not the team’s display name. |
| Project name | The project name | As in vercel.com/acme-corp/local-tools, or the name vercel project ls lists. It can differ from the deployed hostname. |
| Deployed URL | The production domain | The Aliased URL from the deploy. With or without /mcp; Keycard adds it. |
From these, Keycard creates:
- A Provider for your team’s issuer,
https://oidc.vercel.com/<team-slug> - A Workload Identity credential on your Application, with the subject
owner:<team-slug>:project:<project-name>:environment:production - A Resource for the deployed endpoint,
<deployed-url>/mcp
Vercel builds the token’s subject from the team slug and the project name, not from the hostname. That’s why the form asks for all three: a wrong team or project breaks authentication, and a wrong URL registers an endpoint that nothing serves.
The Tell Keycard where it deployed form asks for two values:
| Field | What to enter | Where to find it |
|---|---|---|
| Vercel team | The team slug, exactly as Vercel spells it | The part of your team’s URL after vercel.com/, for example acme-corp in vercel.com/acme-corp, or the ID column of vercel teams ls. Not the team’s display name. |
| Project name | The project name | As in vercel.com/acme-corp/agent, or the name vercel project ls lists. |
From these, Keycard creates a Provider for your team’s issuer, https://oidc.vercel.com/<team-slug>, and a Workload Identity credential on your Application with the subject owner:<team-slug>:project:<project-name>:environment:production. There’s no Resource: nothing calls the agent, it calls PagerDuty.
Check the deployment
Section titled “Check the deployment”Point your agent at the deployed endpoint, then ask it to use one of the server’s tools:
claude mcp add --transport http --scope local local-tools-prod <deployed-url>/mcpAdding the server leaves no trace in Keycard. The first tool call does: it appears on the Application’s Activity tab, authenticated with the Vercel identity.
The starter’s vercel.json has Vercel Cron call /api/run once a day, the most a Hobby plan allows. Each call is one cycle, like make dev. To run one now:
curl -i <deployed-url>/api/runThe response only says whether the cycle ran. The PagerDuty summary goes to the function logs (vercel logs), so the URL never shows your incidents. The run appears on the Application’s Activity tab, authenticated with the Vercel identity.
Pitfalls
Section titled “Pitfalls”The team slug or project name doesn’t match
Section titled “The team slug or project name doesn’t match”Keycard compares the token’s issuer and subject character for character. The slug in vercel.com/<team-slug> is often different from the team name Vercel displays (Acme Corp versus acme-corp). A project renamed after the deploy also stops matching, because its tokens carry the new name. Submit the form again with the values Vercel shows now.
Keycard expects Vercel’s Team issuer mode, which puts the team slug in the issuer. If the project’s OIDC issuer mode is set to Global, its tokens carry https://oidc.vercel.com as the issuer and match no Provider. Switch it back to Team and redeploy.
The deployed URL was guessed
Section titled “The deployed URL was guessed”Entering https://<project-name>.vercel.app without checking registers a Resource that nothing answers on whenever Vercel suffixed the hostname. Your agent then can’t connect to it, even though the credential works. Copy the Aliased URL from the deploy output instead.
Preview deployments don’t authenticate
Section titled “Preview deployments don’t authenticate”The credential’s subject ends in environment:production. Tokens from a preview deployment, which is what vercel deploy makes without --prod, end in environment:preview and don’t match it, so their token exchanges fail with invalid_client. Deploy with vercel deploy --prod. make env-push also writes KEYCARD_ZONE_URL to production only, so a preview deployment is missing it as well.
Vercel Authentication blocks the agent
Section titled “Vercel Authentication blocks the agent”Vercel’s Deployment Protection can put a Vercel sign-in page in front of a deployment. When it covers the URL you gave Keycard, your agent gets Vercel’s login page instead of the MCP server and can’t connect. Open the deployed URL in a private browser window: if you see a Vercel sign-in, go to the project’s Settings → Deployment Protection and turn off Vercel Authentication, or make sure you registered the production domain rather than the per-deployment URL. Claude Code users can also install the Vercel plugin (/plugin install vercel), which can reach a deployment that still has Vercel Authentication on.
Vercel refuses to deploy the commit
Section titled “Vercel refuses to deploy the commit”If vercel deploy --prod rejects the deploy because the Git author doesn’t have access to the team, the latest commit is still the starter’s. Make the empty commit from step 4 with the email on your Vercel account and deploy again.
The deployed server returns a 500
Section titled “The deployed server returns a 500”KEYCARD_ZONE_URL isn’t set on the deployment. (The agent instead answers with the settings it’s missing.) Run make env-push, then vercel deploy --prod again: Vercel applies environment variables at deploy time, so a deployment made before the push never sees them. make env-list shows what the project has.
Token exchange fails with invalid_client
Section titled “Token exchange fails with invalid_client”No Workload Identity credential matched the token. The error names the issuer and subject exactly as Keycard read them, so compare them with what you entered in the form; one of the pitfalls above is almost always the cause.
What’s next
Section titled “What’s next”- Authenticate deployed workloads: the same model on Render, Fly.io, and any platform with an OIDC issuer
- Audit Log & Sessions: watch every credential the deployment is issued
- Providers: how Keycard trusts a platform’s tokens