---
title: "Overview"
description: "Expose external MCP and OpenAPI servers to the model, with connection tokens the model never sees."
url: /connections
---

A connection wires an agent into an external server you don't author, either an MCP server (Linear, GitHub, a warehouse) or any HTTP API with an OpenAPI document. eve handles the parts you'd otherwise hand-roll, discovering the remote tools, surfacing them to the model, and brokering auth. To instead let eve communicate with users through an external service, use a [channel](/docs/channels/overview).

Connections live under `agent/connections/`. A static connection's runtime name comes from the filename, so `agent/connections/linear.ts` registers as `"linear"`. A dynamic connection file can instead resolve a caller-specific connection set at session or turn boundaries. The model never sees a connection's URL or credentials. It finds tools with the built-in [`eve__search`](/docs/concepts/built-in-tools#eve__search-eve__tool-and-eve__skill), where a query such as `linear__` searches one connection's tools, and calls them with `eve__tool`. Stream events, approval policies, and hooks name each call by its qualified name, `<connection>__<tool>` (e.g. `linear__list_issues`).

A connection owns its name and every name that starts with `<name>__`, so `linear__list_issues` always means the `linear` connection's `list_issues` tool. The compiler rejects a tool, subagent, or connection whose name is a static connection's name or starts with its `<name>__` prefix. eve applies the same rule to dynamic connections, tools, and subagents when they resolve. Both errors name the two sides of the conflict; rename either one.

## MCP connections

Use an MCP connection when the external service already exposes an MCP server. The server publishes its tools and schemas, and eve makes the matched tools callable by the model. Each tool's JSON Schema reaches the model as published, minus any arguments you supply with `toolCall.providedArguments`, and eve checks the model's arguments against it before it calls the server.

Read [MCP connections](/docs/connections/mcp) for `defineMcpClientConnection`, transport requirements, and MCP tool filters.

## OpenAPI connections

Use an OpenAPI connection when the service exposes an OpenAPI 3.x document. eve turns operations in the document into connection tools, one per operation.

Read [OpenAPI connections](/docs/connections/openapi) for `defineOpenAPIConnection`, `baseUrl`, and operation filters.

## Dynamic connections

Use `defineDynamic` when the connection set depends on the authenticated user,
tenant, or another runtime lookup. Import it with the connection factories from
`eve/connections`, then return one connection definition, a map of definitions,
or `null` from `session.started` or `turn.started`.

```ts title="agent/connections/accounts.ts"
import { defineDynamic, defineMcpClientConnection } from "eve/connections";
import { listAccounts } from "../lib/accounts";

export default defineDynamic({
  events: {
    "session.started": async (_event, ctx) => {
      const accounts = await listAccounts(ctx.session.auth.current);
      return Object.fromEntries(
        accounts.map((account) => [
          account.slug,
          defineMcpClientConnection({
            url: account.mcpUrl,
            description: account.description,
            instanceKey: account.id,
            auth: account.auth,
          }),
        ]),
      );
    },
  },
});
```

For a single returned definition, the filename supplies the connection name.
For a map, each bare map key becomes a connection name. Every entry must use
`defineMcpClientConnection` or `defineOpenAPIConnection`. Authenticated entries
must also set `instanceKey` to a stable, non-secret account or tenant identifier.
See [Dynamic capabilities](/docs/guides/dynamic-capabilities#dynamic-connections)
for event scope, naming, conflicts, and durable recovery behavior.

Tools from a `turn.started` connection can require approval, including an
`approval.response` policy. When an approval resumes between turns, eve rebuilds
the connection callbacks before checking the response policy, even in a fresh
process. This restoration uses the pending request's turn coordinates and the
current authenticated session context; it does not emit another public
`turn.started` event. The response policy still receives the authenticated
responder separately. When one delivery answers requests from different turns,
eve restores each candidate's originating connection scope before its policy
check, then prepares the complete batch selected for tool replay. Keep connection
resolvers idempotent because restoration can run them again.

Restoration uses current resolver results, not a historical connection snapshot.
If the tool is absent from the persisted tool metadata, or its required response
policy is unavailable, eve refuses the response and leaves the request pending.
eve does not reconstruct missing tool metadata from earlier turns. Before replay, eve checks that the
approved tool is still available and that its connection identity has not
changed since the call requested approval. A missing or changed connection
fails the replay rather than sending the call to a replacement destination.
Request a new tool call and approval after correcting the connection. Identity
includes the endpoint and `instanceKey`; use a distinct `instanceKey` for each
account or tenant even when they share an endpoint.

## Static-token auth

`getToken` returns a `TokenResult` (`{ token, expiresAt? }`), and eve sends it as `Authorization: Bearer <token>` on every request. Because it runs on each connection attempt, you can mint a fresh token from wherever you keep secrets, including an env var, a secrets manager, an internal vault, or your own OAuth exchange:

```ts title="agent/connections/linear.ts"
import { defineMcpClientConnection } from "eve/connections";

export default defineMcpClientConnection({
  url: "https://mcp.linear.app/mcp",
  description: "Linear workspace.",
  auth: {
    credentialOwner: "app",
    getToken: async () => ({ token: process.env.LINEAR_API_TOKEN! }),
  },
});
```

If the token has a known TTL, set `expiresAt` (milliseconds since epoch) and eve refreshes ahead of time rather than waiting for a `401`.

When `getToken` is the only auth, `credentialOwner` defaults to `"app"`: one shared credential keyed across all sessions. Set `credentialOwner: "user"` when each end-user carries their own token.

eve resolves and caches connection tokens per step; they never land in conversation history or reach the model.

## Choose app vs. user auth

A connection credential can belong to the agent or to the person using it. This choice is separate from route auth, but user-scoped connection auth depends on route auth: eve can only resolve a user token when the active session has `ctx.session.auth.current?.principalType === "user"`.

| Credential owner | Use when                                                                       | Auth shape                                                                                                                                                   |
| ---------------- | ------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| App              | The agent should use one shared service, bot, installation, or app credential. | `auth: { getToken }` defaults to `credentialOwner: "app"`, or use `connect({ connector: "linear/myagent", principalType: "app" })` with Vercel Connect.      |
| User             | Each end-user should authorize and use their own third-party account.          | `connect("linear/myagent")`, `connect({ connector: "linear/myagent", principalType: "user" })`, or `auth: { credentialOwner: "user", getToken }`.            |
| User from a job  | Background work should use the same user's OAuth grant that started the work.  | Start or resume the session through a channel whose route auth resolved that user, or pass an explicit user auth context when dispatching through a channel. |

`credentialOwner: "user"` does not mean "ask any human later." It means "key this credential to the authenticated user already attached to the eve session." If the run was started by a schedule, a same-project runtime token, `localDev()`, or another internal runtime path without an end-user principal, a user-scoped connection fails with `reason: "principal_required"` instead of starting OAuth. In that case, either authenticate the inbound channel as a user or configure the connection as app-scoped.

`credentialOwner` is eve's field for raw getToken-only connection auth. Existing non-interactive raw `principalType` definitions still work, but are deprecated. Interactive auth and `connect()` from `@vercel/connect/eve` continue to use `principalType: "user"`; that field also remains correct for route-auth session identities.

## No auth

Drop `auth` entirely for servers that need no token, such as a localhost server during development or a public one:

```ts title="agent/connections/local.ts"
import { defineMcpClientConnection } from "eve/connections";

export default defineMcpClientConnection({
  url: "http://localhost:3001/mcp",
  description: "Local dev server.",
});
```

Use no-auth connections only for services that are intentionally public, local-only, or otherwise protected outside eve. Do not use no-auth connections for sensitive third-party services.

## Headers

Use `headers` when the server wants a non-Bearer scheme (an API-key header) or extra configuration. Headers stack on top of `auth` and work for both MCP and OpenAPI connections:

```ts title="agent/connections/example.ts"
import { defineMcpClientConnection } from "eve/connections";

export default defineMcpClientConnection({
  url: "https://example.com/mcp",
  description: "Example service.",
  headers: { "X-Api-Key": process.env.EXAMPLE_API_KEY! },
});
```

## Per-caller auth and headers

When credentials or routing depend on the caller, make `auth` or `headers` a function. eve calls it inside the active turn and passes the same session context exposed to tools and hooks:

```ts title="agent/connections/warehouse.ts"
import { defineOpenAPIConnection } from "eve/connections";

export default defineOpenAPIConnection({
  spec: "https://warehouse.example.com/openapi.json",
  description: "The caller's tenant-scoped warehouse.",
  auth: (ctx) => ({
    credentialOwner: "user",
    getToken: async () => ({
      token: await tenantToken(ctx.session.auth.current),
    }),
  }),
  headers: (ctx) => ({
    "X-Tenant-Id": tenantId(ctx.session.auth.current),
  }),
});
```

An `auth` resolver returns the same provider object accepted by static `auth`, including a `connect(...)` provider for interactive OAuth. The resolver itself can be async. Header callbacks can return the whole map, as above, or resolve individual values:

```ts
headers: {
  "X-Tenant-Id": (ctx) => tenantId(ctx.session.auth.current),
}
```

Use `credentialOwner: "user"` for per-user tokens so eve rejects unauthenticated callers and keys its step-local token cache by user. The resolver supplements route auth; it does not authenticate the inbound request. Static auth objects and header maps remain available when every caller shares the same configuration.

## Per-connection approval

To put every tool a connection serves behind a human, use the helpers from `eve/tools/approval`:

```ts title="agent/connections/linear.ts"
import { defineMcpClientConnection } from "eve/connections";
import { once } from "eve/tools/approval";

export default defineMcpClientConnection({
  url: "https://mcp.linear.app/mcp",
  description: "Linear workspace.",
  auth: { getToken: async () => ({ token: process.env.LINEAR_API_TOKEN! }) },
  approval: once(),
});
```

`never()` lets every call through, `once()` asks for approval the first time in a session, `always()` asks every time, and `auto()` uses a decision model to allow routine calls and ask about dangerous or unclear effects. The pause and resume is the same human-in-the-loop flow covered in [Tools](/docs/tools).

For connection tools that can create, modify, delete, transmit, purchase, message, or access sensitive data, use approval, allow-lists, or other safeguards appropriate to the action.

## Interactive OAuth via Vercel Connect

When the server uses OAuth and you want each end-user to sign in through their own browser, turn on interactive authorization with [Vercel Connect](https://vercel.com/docs/connect). The `connect()` helper from `@vercel/connect/eve` handles consent, encrypted token storage, and refresh, then hooks all of that into eve's authorization flow.

Create and attach the connector from the Vercel project or agent app that will use it:

```bash
npm install @vercel/connect
vercel link
vercel connect create <service> --name <name>
vercel connect attach <connector-uid> --yes
vercel env pull
```

Use the connector UID returned by the CLI in `connect("<connector-uid>")`. The service identifier accepted by `vercel connect create` depends on the provider; it is not necessarily the same as an MCP runtime URL or OpenAPI base URL.

```ts title="agent/connections/linear.ts"
import { connect } from "@vercel/connect/eve";
import { defineMcpClientConnection } from "eve/connections";

export default defineMcpClientConnection({
  url: "https://mcp.linear.app/mcp",
  description: "Linear workspace: issues, projects, cycles, and comments.",
  auth: connect("linear/myagent"),
});
```

Use the connector UID returned by `vercel connect create`; the display name passed to `--name` is not the UID. `connect("linear/myagent")` is shorthand for a user-scoped interactive OAuth connection: eve resolves a token for the active user before each tool call, emits `authorization.required` when that user has not authorized yet, and resumes the parked turn after the callback completes.

Discovering tools never posts a sign-in prompt for a service the answer does not need: `eve__search` never asks for sign-in. Before the user signs in, eve connects to an MCP server without a token, so a server that lists its tools anonymously is searched like any other connection, and sign-in starts only when one of its tools asks for it (see [When eve asks for sign-in](/docs/connections/mcp#when-eve-asks-for-sign-in)). Most OAuth MCP servers reject even tool listing without the user's token, so `search` lists such a connection as one result named after the connection, such as `linear`. The model asks for sign-in explicitly, one connection at a time, by calling that name with `eve__tool({ name: "linear" })`; once the user signs in, `eve__search({ query: "linear__" })` returns the connection's tools. Calling a tool with `eve__tool` on such a connection also asks for sign-in.

A sign-in holds the turn open until the user connects, the turn is cancelled, or the user sends another message. That message steers the turn: eve cancels the sign-in, reports `authorization.completed` with `outcome: "declined"`, and the model reads the message next. Messages from other people in a shared thread wait until the turn ends, and the completed sign-in resumes the turn as the person who started it.

When a local subagent needs interactive authorization, eve surfaces the authorization lifecycle on the root session's channel, including through nested subagent chains. The challenge still points to the child session's callback, so completing it resumes the child directly while the parent continues waiting for its result. The parent's turn stays open: it emits `turn.waiting` after the proxied `authorization.required` and resumes under the same `turnId` once the subagent returns.

That means the channel that creates or continues the session must authenticate a real user. For a web app, configure `agent/channels/eve.ts` so your app session maps to `principalType: "user"`; for platform channels, use the built-in channel auth that maps the sender to a user principal. If no authenticated user is attached to the session, the first user-scoped connection call fails with `reason: "principal_required"`.

If the remote service should act as the agent itself instead of the end-user, make the Connect connection app-scoped:

```ts title="agent/connections/linear.ts"
import { connect } from "@vercel/connect/eve";
import { defineMcpClientConnection } from "eve/connections";

export default defineMcpClientConnection({
  url: "https://mcp.linear.app/mcp",
  description: "Linear workspace: issues, projects, cycles, and comments.",
  auth: connect({ connector: "linear/myagent", principalType: "app" }),
});
```

App-scoped Connect auth is non-interactive. eve asks Vercel Connect for an app token and does not emit a browser consent challenge; if the connector is not installed or cannot issue an app token, the tool call fails terminally so an operator can fix the connector setup. See [Authentication](/docs/guides/auth-and-route-protection) when a user-scoped connection needs your HTTP channel to establish the caller's identity.

### Troubleshooting Vercel Connect auth

| Symptom                                      | What it means                                                                                                                     | Fix                                                                                                                                                             |
| -------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `reason: "principal_required"`               | A user-scoped connection ran without an authenticated user on the active session.                                                 | Return `principalType: "user"` from the channel's route auth, or use `connect({ connector, principalType: "app" })` if the Connect credential should be shared. |
| `authorization.required` appears but no UI   | eve parked the turn for OAuth, but the channel or frontend is not rendering the challenge.                                        | Render the challenge from the stream event and continue the same session after the callback.                                                                    |
| OAuth works locally but fails after deploy   | The project may not be linked to the Connect client, or the deployed runtime may not have the expected Vercel OIDC/project scope. | Run Connect setup from the consuming project directory, link the project, deploy again, and verify the connector UID in `connect("...")`.                       |
| A scheduled or internal run needs user OAuth | Schedules and runtime callers do not automatically carry an end-user principal.                                                   | Dispatch through a user-authenticated channel when work is user-owned, or use app-scoped auth for agent-owned background work.                                  |

## Self-hosted interactive OAuth

To run your own OAuth, use `defineInteractiveAuthorization` from `eve/connections`, which takes a three-method form and needs no Vercel Connect. eve mints a callback URL, parks (durably suspends) the turn on a framework-owned webhook, and resumes once the token comes back. Interactive auth is always `principalType: "user"`; `credentialOwner` applies only to getToken-only auth, and the factory pins the interactive value for you.

```ts title="agent/connections/linear.ts"
import {
  ConnectionAuthorizationRequiredError,
  defineInteractiveAuthorization,
  defineMcpClientConnection,
} from "eve/connections";

export default defineMcpClientConnection({
  url: "https://mcp.linear.app/mcp",
  description: "Linear workspace.",
  auth: defineInteractiveAuthorization<{ verifier: string }>({
    // Probed before every tool call. Return a token to run the tool;
    // throw `Required` to start the consent flow.
    getToken: async ({ principal }) => {
      const token = await loadStoredToken(principal);
      if (!token) throw new ConnectionAuthorizationRequiredError("linear");
      return { token };
    },
    // Runs in a durable step. Return the user-facing `challenge` and
    // an optional `resume` value the runtime journals across the park.
    startAuthorization: async ({ callbackUrl }) => {
      const verifier = makePkceVerifier();
      return {
        challenge: { url: buildAuthorizeUrl(callbackUrl, verifier) },
        resume: { verifier },
      };
    },
    // Runs when the provider redirects to the callback URL. `resume` is
    // typed as `{ verifier: string } | undefined`; `callback.params`
    // holds the IdP's returned query/body params.
    // Persist the token here so `getToken` finds it on later tool calls.
    completeAuthorization: async ({ principal, resume, callback }) => {
      const token = await exchangeCode(resume!.verifier, callback.params.code!);
      await saveStoredToken(principal, token);
      return { token };
    },
  }),
});
```

`getToken` runs before every tool call. eve does not store tokens for you: the token returned from `completeAuthorization` is reused only within the current workflow step, so it never lands in durable session state. Save it in `completeAuthorization` (or let your OAuth provider keep it) and read it back in `getToken`. If `getToken` cannot find the token on a later call, eve asks the user to sign in again. `startAuthorization` and `completeAuthorization` are both-or-neither: provide one without the other and you get a definition error. The `challenge` rides along verbatim on the `authorization.required` event. Its fields:

| Field          | Purpose                                                                                   |
| -------------- | ----------------------------------------------------------------------------------------- |
| `url`          | The authorize URL for redirect or device flows.                                           |
| `userCode`     | The device code, for device flows.                                                        |
| `instructions` | The call to action when there's no URL.                                                   |
| `displayName`  | Human-readable provider name channels show on the sign-in affordance (e.g. "Salesforce"). |

Drop `resume` when the provider keeps flow state server-side, so nothing has to cross the step boundary.

`displayName` is presentation-only. The connection's resolved name still keys
the callback URL, while eve scopes token caching and authorization completion
to the opaque resolved instance identity. You can also set `displayName` on the
`auth` definition itself (e.g.
`auth: { ...connect("salesforce/myagent"), displayName: "Salesforce" }`); that
definition-level value wins over one the strategy stamps on the challenge, and
channels fall back to title-casing the connection name when neither is set.

### Signaling authorization state

Two error classes drive the consent flow. Throw them from `getToken` or `completeAuthorization`; both are exported from `eve/connections`.

- `ConnectionAuthorizationRequiredError(connectionName)`: the user must authorize. Throw it from `getToken` to emit `authorization.required` and kick off the flow.
- `ConnectionAuthorizationFailedError(connectionName, { reason?, retryable? })`: authorization failed. `reason` is a stable machine-readable code (e.g. `"access_denied"`) that shows up on the `authorization.completed` event and the failed tool result. `retryable` defaults to `true`; set it to `false` for terminal cases like user denial so the runtime stops re-prompting.

```ts
import { ConnectionAuthorizationFailedError } from "eve/connections";

throw new ConnectionAuthorizationFailedError("linear", {
  reason: "access_denied",
  retryable: false,
});
```

To narrow a caught error, use `isConnectionAuthorizationRequiredError(err)` and `isConnectionAuthorizationFailedError(err)`. They match on `err.name`, which is why they survive the class-identity split `instanceof` can hit after bundling.

### Handling a revoked token mid-call

`getToken` only runs _before_ a tool call, so a grant revoked while a tool is mid-flight first surfaces as a downstream `401` inside your `execute`. A plain throw there is only a tool error, so the model sees a failure and the cached bearer sticks around. Instead, map a provider `401` to `ctx.requireAuth(provider)`. eve then evicts the rejected token from its per-step cache and re-runs the consent flow with a fresh one, exactly as it does for a connection whose server rejects the bearer.

```ts title="agent/tools/list_issues.ts"
import { connect } from "@vercel/connect/eve";
import { defineTool } from "eve/tools";
import { z } from "zod";

const linearAuth = connect("linear/myagent");

export default defineTool({
  description: "List open Linear issues.",
  inputSchema: z.object({}),
  async execute(_input, ctx) {
    const { token } = await ctx.getToken(linearAuth);
    const res = await fetch("https://api.linear.app/graphql", {
      headers: { authorization: `Bearer ${token}` },
    });
    // The grant was revoked since getToken ran: re-challenge instead of
    // returning a dead-token error to the model.
    if (res.status === 401) ctx.requireAuth(linearAuth);
    return await res.json();
  },
});
```

### Authorization and approval together

A tool can require both sign-in (`auth`) and a human approval. The model's approval gate runs before the tool's `execute`, so the order the user sees is **approve, then sign in**. eve records the approval on session state the moment it's granted, and that record survives the sign-in park, so when the turn resumes after authorization the tool is not put through approval again. You get one approval and one sign-in, never a double prompt.

## What to read next

- [MCP connections](/docs/connections/mcp): connect to remote MCP servers.
- [OpenAPI connections](/docs/connections/openapi): generate tools from OpenAPI operations.
- [Integrations](/integrations): browse every connection eve ships using the Connections filter.
- [Tools](/docs/tools): authored tools live alongside connection-provided tools; the same approval helpers apply.
- [Security model](/docs/concepts/security-model): how connection credentials stay out of the model's reach.
