---
title: MCP
description: Publish an eve agent as a durable MCP invocation service with route authentication and OAuth discovery.
---

# MCP



The MCP channel lets clients such as Claude Code delegate durable work to an eve agent through four tools: `agent_start`, `agent_get`, `agent_update`, and `agent_cancel`.

Use an [MCP connection](../connections/mcp) instead when your eve agent needs to call someone else's MCP server.

## Configure the channel

Create `agent/channels/mcp.ts`. Authentication is required explicitly, even during development.

```ts title="agent/channels/mcp.ts"
import { localDev, vercelOidc } from "eve/channels/auth";
import { mcpChannel } from "eve/channels/mcp";

export default mcpChannel({
  auth: [vercelOidc(), localDev()],
});
```

This accepts Vercel-issued bearer tokens in a deployment and admits a synthetic local principal under `eve dev` or `vercel dev`. `localDev()` checks the running environment, not the request hostname: accessing an `eve start` production process through localhost does not activate it.

### Routes

The default Streamable HTTP endpoint is `/eve/v1/mcp`. Set `route` when the application should publish it somewhere else:

```ts title="agent/channels/mcp.ts"
import { none } from "eve/channels/auth";
import { mcpChannel } from "eve/channels/mcp";

export default mcpChannel({
  auth: none(),
  route: "/mcp",
});
```

`none()` makes the endpoint public; use it only when anonymous access is intentional. The channel registers `GET`, `POST`, and `DELETE` at the selected route.

## Interactive OAuth

For an MCP client that should open a sign-in flow, configure eve as an OAuth protected resource. Wrap the access-token verifier in `oauthResource()`:

```ts title="agent/channels/mcp.ts"
import { oauthResource, oidc } from "eve/channels/auth";
import { mcpChannel } from "eve/channels/mcp";

const issuer = "https://auth.example.com";
const resource = "https://agent.example.com/eve/v1/mcp";

export default mcpChannel({
  auth: oauthResource(
    oidc({
      audiences: [resource],
      issuer,
    }),
    {
      issuer,
      resource,
      scopes: ["agent:invoke"],
    },
  ),
});
```

The authorization server identified by `issuer` must issue tokens accepted by the wrapped verifier. `scopes` advertises what clients should request; the verifier remains responsible for signature, expiration, audience/resource, and scope enforcement.

`oauthResource()` does not issue tokens or run an authorization server. It decorates an ordinary inbound `AuthFn` with OAuth protected-resource metadata so `mcpChannel()` can publish discovery and add `resource_metadata` to Bearer challenges. Client registration, consent, token issuance, and authorization-server metadata remain the identity provider's responsibility.

### Verify the bearer token yourself

Use a custom `AuthFn` when token verification needs application-specific logic:

```ts title="agent/channels/mcp.ts"
import { extractBearerToken, oauthResource, verifyOidc } from "eve/channels/auth";
import { mcpChannel } from "eve/channels/mcp";

const issuer = "https://auth.example.com";
const resource = "https://agent.example.com/eve/v1/mcp";

async function verifyToken(request: Request) {
  const token = extractBearerToken(request.headers.get("authorization"));
  const result = await verifyOidc(token, {
    audiences: [resource],
    issuer,
  });

  return result.ok ? result.sessionAuth : null;
}

export default mcpChannel({
  auth: oauthResource(verifyToken, {
    issuer,
    resource,
    scopes: ["agent:invoke"],
  }),
});
```

`verifyOidc()` uses the issuer's discovery document to validate the token signature and claims. Returning `sessionAuth` accepts the request and binds invocation ownership to that verified principal. Returning `null` lets the auth walk continue; when no strategy accepts the request, eve returns `401`.

You can wrap any strategy in `oauthResource()`, including `vercelOidc()`, but the advertised authorization server must issue tokens that strategy accepts. `vercelOidc()` by itself is preconfigured Vercel workload identity and does not advertise an interactive login.

### Protected-resource metadata

By default, eve derives the metadata path from the complete MCP resource identifier according to RFC 9728:

| MCP resource                           | Protected-resource metadata                                                 |
| -------------------------------------- | --------------------------------------------------------------------------- |
| `https://agent.example.com/eve/v1/mcp` | `https://agent.example.com/.well-known/oauth-protected-resource/eve/v1/mcp` |
| `https://agent.example.com/mcp`        | `https://agent.example.com/.well-known/oauth-protected-resource/mcp`        |

Set `resource` when the public resource identifier cannot be derived from the incoming request. Set `metadataPath` only when discovery must live at a non-derived location:

```ts
oauthResource(verifyToken, {
  issuer,
  metadataPath: "/.well-known/custom-resource",
  resource: "https://agent.example.com/eve/v1/mcp",
  scopes: ["agent:invoke"],
});
```

The metadata endpoint serves cross-origin `GET`, `HEAD`, and `OPTIONS` requests so browser-hosted clients can discover the authorization server. MCP protocol requests remain same-origin.

## Other authentication modes

`mcpChannel()` accepts the same inbound auth strategies as other eve channels: Basic auth, HMAC or ECDSA JWTs, generic OIDC, Vercel OIDC, custom `AuthFn` policies, or an ordered array of them. These modes do not automatically produce an interactive login unless wrapped in `oauthResource()`; configure credentials in the MCP client out of band.

When a protected request has no accepted credentials, eve returns a Bearer challenge. A supplied Bearer token rejected by every strategy gets `error="invalid_token"`. To report a verified caller that lacks the necessary scopes, throw `ForbiddenError` with an `error="insufficient_scope"` Bearer challenge. The MCP channel preserves that challenge and adds the protected-resource metadata URL.

For the complete strategy and auth-walk model, see [Authentication](../guides/auth-and-route-protection).

## HTTP security

The MCP transport validates the request before authentication:

* Remote endpoints require HTTPS; HTTP is accepted only on loopback.
* `Host` must match the request URL.
* Browser protocol requests must have an exact same-origin `Origin`.

When a gateway or reverse proxy changes the public origin or path, set `resource` explicitly so metadata and authentication challenges advertise the client-facing MCP resource.

The protected-resource metadata endpoint is intentionally CORS-readable. The MCP transport itself does not enable cross-origin browser access; place a same-origin backend or authenticated server-side proxy in front of it when a browser application needs to connect.

The endpoint serves MCP `2026-07-28` directly and retains stateless `2025-11-25` Streamable HTTP compatibility. eve does not keep an MCP transport session in either mode.

## Invoke the agent

MCP clients receive four tools:

| Tool           | Input                         | Purpose                                                     |
| -------------- | ----------------------------- | ----------------------------------------------------------- |
| `agent_start`  | `{ message, outputSchema? }`  | Start durable work and immediately return an invocation ID. |
| `agent_get`    | `{ invocationId }`            | Read the invocation's complete current state.               |
| `agent_update` | `{ invocationId, responses }` | Answer the complete pending human-input batch.              |
| `agent_cancel` | `{ invocationId }`            | Request cooperative cancellation of non-terminal work.      |

`agent_start` creates one task-mode eve session and returns after durable acceptance. Keep its `invocationId`, then call `agent_get` until the invocation reaches a terminal state. While the status is `working`, wait at least `pollAfterMs` before polling again.

The invocation response is discriminated by `status`:

* `working`: work is active; continue polling according to `pollAfterMs`.
* `input_required`: present `inputRequests`, then send the complete answer batch through `agent_update`. A successful update returns fresh `working` state.
* `authorization_required`: present the returned sign-in URL, user code, or instructions. The connection callback resumes the invocation automatically; continue polling.
* `completed`: consume the optional `result`.
* `failed`: inspect the structured `error`.
* `cancelled`: cancellation reached a terminal state.

Cancellation is cooperative, so call `agent_get` after `agent_cancel` until the state becomes terminal.

Optional output schemas are limited to 64 KiB, 32 levels, and 2,048 nodes. External `$ref` values are rejected.

## Invocation ownership

Every MCP operation reruns the configured auth policy. With authenticated policies, an invocation belongs to the principal that started it; knowing its ID is not sufficient. Bearer tokens are not stored with the invocation or forwarded to the agent's tools.

With `none()`, every caller shares the anonymous principal, so the random invocation ID becomes a bearer capability. Keep it out of logs and URLs and treat it as usable until workflow retention expires. Responses include `expiresAt` when the workflow backend reports a retention deadline.

## What to read next

* [Authentication](../guides/auth-and-route-protection): configure inbound route authentication
* [MCP connections](../connections/mcp): let an eve agent call another MCP server
* [Sessions, runs & streaming](../concepts/sessions-runs-and-streaming): understand the durable sessions behind invocations


---

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)