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

# Overview



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 discovers tools through the built-in `connection_search` and calls them by their qualified name, `<connection>__<tool>` (e.g. `linear__list_issues`).

## 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.

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.

## 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, and `always()` asks every time. 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.

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.

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 lookupCachedToken(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.
    completeAuthorization: async ({ resume, callback }) => {
      const token = await exchangeCode(resume!.verifier, callback.params.code!);
      return { token };
    },
  }),
});
```

`getToken` runs before every tool call. `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.


---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

For agent-facing discovery, including API and MCP surfaces, see [/agents.md](/agents.md)