---
title: "MCP Connections"
description: "Connect an eve agent to a remote MCP server, authorize it with Vercel Connect or static credentials, and control which tools the model can discover."
---

MCP connections point eve at a remote MCP server you do not author. The server publishes its tools and schemas, and the model finds them with `eve__search` and calls them with `eve__tool`.

Use MCP when the service already has an MCP server, when the server owns tool schemas dynamically, or when one connection should expose a family of related remote tools. Use an [OpenAPI connection](./openapi) instead when the service publishes an HTTP API contract and you want eve to generate one tool per operation.

## Define an MCP connection

Create one file under `agent/connections/`. The filename becomes the runtime connection name, so `agent/connections/linear.ts` registers as `linear`, and events report its tools as `linear__<tool>`.

```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("mcp.linear.app/linear"),
});
```

The `url` must speak Streamable HTTP or SSE. Write the `description` for the model, not for yourself: eve lists it next to the connection name, and it is the main signal the model uses to decide which connection to search.

## Configure protocol discovery

eve discovers the server's MCP protocol by default. If a server requires the older `initialize` handshake, disable discovery for that connection:

```ts
export default defineMcpClientConnection({
  url: "https://mcp.example.com/mcp",
  description: "Search support cases",
  protocolVersionDiscovery: false,
});
```

With `protocolVersionDiscovery: false`, the client skips `server/discover` and starts with `initialize`, proposing `2025-11-25` and negotiating a supported handshake-based version. This is a per-connection setting; omitting it or setting it to `true` keeps discovery enabled. Remove the override when the server supports discovery. The setting also applies to connections returned by dynamic resolvers.

## Use Vercel Connect for OAuth

For OAuth-backed MCP servers, use the shared [Vercel Connect flow](../connections#interactive-oauth-via-vercel-connect), then pass the returned connector UID to `connect()`. Keep the MCP runtime URL and Connect service identifier distinct. For example, Linear uses `https://mcp.linear.app/mcp` as the MCP endpoint and `mcp.linear.app` when creating the connector.

`connect("...")` is user-scoped by default and requires an authenticated user on the active eve session. Use `connect({ connector, principalType: "app" })` when the server should act as the agent instead. The connections overview covers connector setup, session requirements, app and user scope, callback behavior, and troubleshooting.

## When eve asks for sign-in

eve asks the user to sign in only when the MCP server asks for it. When a connection has `auth` but no token yet, because its `getToken` throws `ConnectionAuthorizationRequiredError`, eve connects without an `Authorization` header and lists the server's tools. Any other error from `getToken` still fails the connection.

- **The server rejects every request without a token.** Most hosted OAuth servers, such as Linear, GitHub, and Notion, answer `initialize` or `tools/list` with HTTP `401`. `eve__search` lists the connection as one result named after it, such as `linear`. Calling that name with `eve__tool({ name: "linear" })`, or any of its tools with `eve__tool`, asks the user to sign in.
- **The server lists tools without a token.** `eve__search` returns its tools with no prompt, and `eve__tool` runs tools the server answers anonymously. Sign-in starts only when a tool call comes back with HTTP `401`, or with an error result that carries a `WWW-Authenticate` challenge in `_meta["mcp/www_authenticate"]`.

After the user signs in, the turn resumes and the model makes the call again. eve reconnects with the token and reloads the tool list, which may now include tools the server hides from anonymous callers. A connection that already has a token sends it on every request, as before.

A server that leaves protected tools out of its anonymous `tools/list` never triggers sign-in, because the model cannot find those tools. Servers that support anonymous use should list every tool and refuse the protected calls.

## Static tokens and headers

MCP connections accept the shared connection `auth` and `headers` options. Use `auth.getToken` for a bearer token, `headers` for another scheme, and resolver functions when credentials or routing depend on the caller. See [Static-token auth](../connections#static-token-auth), [Headers](../connections#headers), and [Per-caller auth and headers](../connections#per-caller-auth-and-headers) for the canonical examples and token-lifecycle behavior.

## Application-provided tool arguments

Some MCP servers require arguments that belong to the application rather than the model. For example, UCP servers expect the agent profile in `arguments.meta` on every tool call. Configure those values with `toolCall.providedArguments`:

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

const profileUrl = "https://agent.example.com/.well-known/ucp";

export default defineMcpClientConnection({
  url: "https://store.example.com/api/ucp/mcp",
  description: "Storefront catalog, carts, checkouts, and orders.",
  toolCall: {
    providedArguments: {
      meta: ({ session }) => ({
        "ucp-agent": {
          profile: `${profileUrl}?session=${encodeURIComponent(session.id)}`,
        },
      }),
    },
  },
});
```

Values may be JSON values, promises, or callbacks. Callbacks receive the active session context, the bare remote `toolName`, and a replay-stable `callId` that is unique to the tool call. Use `callId` when the remote server needs an idempotency key.

eve treats configured keys as application-owned: it removes them from every remote tool's model-facing input schema and adds their resolved values immediately before execution. They apply to every tool call on the connection and replace any conflicting model value. Approval policies continue to receive only the model-authored input.

## No auth

Omit `auth` and `headers` only for an intentionally public or loopback MCP server. See [No auth](../connections#no-auth) for the shared connection behavior and security boundary.

## Tool filters

MCP servers can expose broad read and write surfaces. Narrow what the model can discover with exactly one of `tools.allow` or `tools.block`:

```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: read issue and project data.",
  auth: connect("mcp.linear.app/linear"),
  tools: { allow: ["search_issues", "get_issue"] },
});
```

Prefer `allow` for the smallest safe surface, especially when the server exposes write tools. Use `block` when the server has a broad stable surface and only a few tools should be hidden.

## Approval gates

Use the shared `approval` option to gate every tool served by an MCP connection. The [connections overview](../connections#per-connection-approval) defines the `never()`, `once()`, `always()`, and `auto()` helpers; [Human-in-the-loop](../human-in-the-loop#approvals) defines the pause-and-resume contract.

### Gate specific tools by name or input

A remote MCP server usually mixes read tools with destructive or publishing ones, so a blanket `always()` would prompt on every harmless call. Pass a custom policy instead — the same [`Approval`](/docs/human-in-the-loop#approvals) shape authored tools use — to gate only the calls that matter. The policy receives `{ session, toolName, toolInput, approvedTools, callId, abortSignal }` and returns an approval status, synchronously or as a promise.

This connection always gates deletes, gates a publish only when the call actually schedules a post, and lets everything else through:

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

// Bare tool names whose effects are irreversible — always gate these.
const DELETE_TOOLS = ["delete_draft", "delete_thread"];
// Tools that can publish — gate only when the call schedules a post.
const PUBLISH_TOOLS = ["create_draft", "edit_draft"];

// Read `requestBody.publish_at` without trusting the input's shape.
const publishesNow = (input: unknown): boolean => {
  const body = (input as { requestBody?: { publish_at?: unknown } })?.requestBody;
  return typeof body?.publish_at === "string" && body.publish_at.length > 0;
};

export default defineMcpClientConnection({
  url: "https://mcp.example.com/mcp",
  description: "Social publishing: draft, schedule, and manage posts.",
  auth: {
    credentialOwner: "app",
    getToken: async () => ({ token: process.env.SOCIAL_API_KEY! }),
  },
  approval: ({ toolName, toolInput }) => {
    if (DELETE_TOOLS.some((t) => toolName.includes(t))) return "user-approval";
    if (PUBLISH_TOOLS.some((t) => toolName.includes(t))) {
      return publishesNow(toolInput) ? "user-approval" : "not-applicable";
    }
    return "not-applicable";
  },
});
```

Two details are specific to connection tools:

- **`toolName` arrives qualified**, not as the bare remote name. An MCP tool surfaces to the policy as `<connection>__<tool>` (e.g. `social__delete_draft`), so match the bare tool name with `.includes()` or `.endsWith()` rather than `===`.
- **`toolInput` is the raw input the model produced**, typed as `Record<string, unknown> | undefined`. With an authored tool you define the `inputSchema`, so its approval policy gets input typed and checked against your schema; a connection tool's schema is published by the remote MCP server, not you, so the shape is one you neither own nor can rely on. It is also `undefined` whenever the model's input isn't an object. Read nested fields defensively — as `publishesNow` does — instead of trusting the shape.

Return `"user-approval"` (or `true`) to pause for a person and `"not-applicable"` (or `false`) to run without a prompt; return `"approved"` or `"denied"` to decide automatically without involving anyone.

[Human-in-the-loop](/docs/human-in-the-loop#approvals) covers the full set of statuses, how `approvedTools` and `session.auth` factor in, and how a gated call pauses and resumes durably.

## Forward the caller to an eve agent

When the server is another eve agent's [`mcpChannel`](../channels/mcp), set `forwardPrincipal: true` so its published tools run as the user of the current turn instead of as this deployment:

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

export default defineMcpClientConnection({
  url: "https://specialist.example.com/eve/v1/mcp",
  description: "The specialist agent's tools.",
  headers: { authorization: `Bearer ${process.env.SPECIALIST_TOKEN}` },
  forwardPrincipal: true,
});
```

eve sends the turn's user, and its initiator when that differs, in the `eve-forwarded-principal` header on every request. A turn with no caller, or an anonymous one, sends no header. The server must trust this deployment's route-authenticated identity in `trustedForwarders`; otherwise it refuses the request with `403`. See [Forwarded principals](../channels/mcp#forwarded-principals). A request carrying the header fails rather than follow a redirect, so the header never reaches another host. A principal over 16 KiB encoded fails the call before it is sent; trim its attributes.

Authenticate the connection as this deployment, with a stable service credential, like the token above, not with user-scoped `connect("...")` or a per-caller resolver. eve resolves `auth` and `headers` once when it opens the client, but one client can serve turns from different users in a shared session, so the header is what names the user on each request. The server trusts the forwarder by that service identity, and grants the user's own access to downstream services itself.

## Control result size

An MCP tool call returns the tool's `structuredContent` when the server sends it. Otherwise it returns the result's text, parsed as JSON when the tool declares no output schema. Image, audio, and resource content reaches the model as file parts, and a result with `isError: true` fails the call with its text. MCP connections do not expose a per-result transform equivalent to an authored tool's [`toModelOutput`](/docs/tools#shape-what-the-model-sees-with-tomodeloutput).

When a remote tool returns more data than the model needs, narrow the result at the MCP server. Prefer a purpose-built search or summary tool, or return a stable handle that another call can use to fetch a smaller slice. If you do not control the server and an equivalent upstream API is available, replace only that operation with an authored tool that stores the full payload outside model context and projects the needed fields through `toModelOutput`. Block the original MCP operation through [`tools.block`](#tool-filters) so the model does not see duplicate tools.

## Troubleshooting

| Symptom                                    | Check                                                                                                                                     |
| ------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------- |
| `principal_required`                       | A user-scoped `connect("...")` ran without an authenticated user. Return `principalType: "user"` from route auth, or use app-scoped auth. |
| The model does not find the remote tool    | Improve the connection `description`, then check `tools.allow` / `tools.block`.                                                           |
| OAuth works locally but fails after deploy | Attach the Connect connector to the deployed Vercel project and verify the UID in `connect("...")`.                                       |
| The server rejects requests                | Confirm the MCP URL, transport support, auth scheme, required headers, and application-provided arguments.                                |

## What to read next

- [Connections](../connections): shared auth, headers, approval, and per-caller patterns.
- [OpenAPI connections](./openapi): generate tools from OpenAPI operations.
- [Authentication](../guides/auth-and-route-protection): establish the caller identity required by user-scoped auth.
- [Security model](../concepts/security-model): how connection credentials stay out of the model's reach.
