Connections

MCP Connections

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 eve exposes matching tools to the model through connection_search.

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 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 discovered tools are called as linear__<tool>.

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: it is the main signal connection_search uses when deciding which connection to query.

Use Vercel Connect for OAuth

For OAuth-backed MCP servers, use the shared Vercel Connect flow, 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.

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, Headers, and 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:

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 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:

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 defines the never(), once(), and always() helpers; Human-in-the-loop 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 shape authored tools use — to gate only the calls that matter. The policy receives { session, toolName, toolInput, approvedTools } 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:

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: { 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 covers the full set of statuses, how approvedTools and session.auth factor in, and how a gated call pauses and resumes durably.

Control result size

eve sends an MCP tool's returned content to the model as the tool result. MCP connections do not expose a per-result transform equivalent to an authored tool's 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 so the model does not see duplicate tools.

Troubleshooting

SymptomCheck
principal_requiredA 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 toolImprove the connection description, then check tools.allow / tools.block.
OAuth works locally but fails after deployAttach the Connect connector to the deployed Vercel project and verify the UID in connect("...").
The server rejects requestsConfirm the MCP URL, transport support, auth scheme, required headers, and application-provided arguments.