> ## Documentation Index
> Fetch the complete documentation index at: https://docs.atlasent.io/llms.txt
> Use this file to discover all available pages before exploring further.

# MCP server

> Authorize-before-execute for any MCP-compatible AI agent: evaluate an action, get a Permit, verify it before the side effect runs.

`@atlasent/mcp-server` brings AtlaSent authorization to agents that speak the [Model Context Protocol](https://modelcontextprotocol.io): Claude Desktop, Claude Code, Cursor, Windsurf, or your own MCP host. The agent asks AtlaSent before a consequential action, receives a Decision, and on `allow` gets a bounded Permit that must pass verification before the side effect runs.

* npm: [`@atlasent/mcp-server`](https://www.npmjs.com/package/@atlasent/mcp-server) (see npm for the current release)
* MCP Registry: `io.github.Atlasent/mcp-server`
* Source: [Atlasent/atlasent-mcp-server](https://github.com/Atlasent/atlasent-mcp-server) (Apache-2.0)

## How it works

```text theme={null}
Action proposed
  → evaluate: organizational policy + context
  → Decision
      deny / hold / escalate → STOP
      allow → bounded Permit
  → verify the Permit at the execution boundary
      invalid / expired / replayed / mismatched / error → STOP
      verified → the side effect may run
```

Evaluation isn't execution. An `allow` Decision alone is not the gate: the Permit has to verify immediately before the protected side effect.

## Modes

The server runs in one of two modes, chosen by `ATLASENT_MODE`:

| Mode | What decides | Use it for |
| - | - | - |
| `remote` | Your organization's published policies on the hosted AtlaSent API. Permits are Ed25519-signed and single-use, and every decision lands in the tamper-evident audit trail. | Real enforcement. |
| `local` | A small built-in rules engine that runs in process, offline, with no account. Permits are **unsigned**. | Trying it out, demos, and CI. |

<Warning>
  Local mode isn't an enforcement boundary. Its permits are unsigned, it has no audit evidence, and its built-in rules allow anything they don't recognize. Use remote mode for anything that must actually be protected. The server refuses to start in local mode when `NODE_ENV=production`.
</Warning>

When `ATLASENT_MODE` is unset, the server uses `remote` if `ATLASENT_API_KEY` is set and `local` otherwise.

## Install

### Try it with no account (local mode)

```bash theme={null}
npx -y @atlasent/mcp-server
```

Add it to your MCP host with `ATLASENT_MODE=local`, then ask your agent to "deploy billing-api to production." The built-in rules deny it because it has no approvals.

### Connect to AtlaSent (remote mode)

1. [Create an account](https://console.atlasent.io/auth/sign-up?utm_source=docs\&utm_medium=docs).
2. In the console, open **API keys** and create a key with the `evaluate:write` and `verify:execute` scopes.
3. Add the server to your MCP host with the key, as shown below.

### Claude Desktop

Add to `claude_desktop_config.json`, then restart Claude Desktop:

* **macOS:** `~/Library/Application Support/Claude/claude_desktop_config.json`
* **Windows:** `%APPDATA%\Claude\claude_desktop_config.json`

```json theme={null}
{
  "mcpServers": {
    "atlasent": {
      "command": "npx",
      "args": ["-y", "@atlasent/mcp-server"],
      "env": {
        "ATLASENT_MODE": "remote",
        "ATLASENT_API_KEY": "ask_live_...",
        "ATLASENT_BASE_URL": "https://api.atlasent.io/functions/v1",
        "ATLASENT_MCP_READONLY": "1"
      }
    }
  }
}
```

### Cursor and Windsurf

Use the same `mcpServers` block:

* **Cursor:** `.cursor/mcp.json` in the project, or `~/.cursor/mcp.json` globally.
* **Windsurf:** `~/.codeium/windsurf/mcp_config.json`.

Any other MCP host works the same way: command `npx`, arguments `["-y", "@atlasent/mcp-server"]`, and the same environment variables. Registry-aware hosts can install it directly as `io.github.Atlasent/mcp-server`.

### Streamable HTTP transport

The server speaks stdio by default. For hosts that connect over HTTP, start it with `--transport streamable-http` (or set `ATLASENT_MCP_TRANSPORT=streamable-http`):

```bash theme={null}
ATLASENT_API_KEY=ask_live_... npx -y @atlasent/mcp-server --transport streamable-http
```

By default it listens on `http://127.0.0.1:3333/mcp`. `ATLASENT_MCP_HTTP_HOST`, `ATLASENT_MCP_HTTP_PORT`, and `ATLASENT_MCP_HTTP_PATH` change that. Set `ATLASENT_MCP_HTTP_BEARER` to require hosts to send `Authorization: Bearer <value>`; when it's unset, the server accepts requests without host authentication and logs a warning at startup, so leave it unset only for loopback, single-user use. The bearer authenticates your host to this server; `ATLASENT_API_KEY` still authenticates this server to AtlaSent.

### Claude Code plugin

A separate, local-only Claude Code plugin makes destructive and shipping commands (for example `DROP TABLE`, `terraform destroy`, `git push --force`, production deploys) wait for a person at the keyboard. It needs no account.

```text theme={null}
/plugin marketplace add Atlasent/atlasent-mcp-server
/plugin install atlasent-guard@atlasent
```

The plugin records that someone at the keyboard said yes. It doesn't issue an organizational Permit; for that, use the MCP server in remote mode.

### Docker

The repository ships a `Dockerfile`. Build it from a clone of the source repository:

```bash theme={null}
docker build -t atlasent-mcp .
docker run -i --rm atlasent-mcp                                          # local mode, stdio
docker run -i --rm -e ATLASENT_API_KEY -e ATLASENT_BASE_URL atlasent-mcp  # remote mode
```

## Environment variables

| Variable | Default | Purpose |
| - | - | - |
| `ATLASENT_API_KEY` | none | API key from the AtlaSent console. Setting it selects remote mode when `ATLASENT_MODE` is unset. |
| `ATLASENT_BASE_URL` | `https://api.atlasent.io/functions/v1` | AtlaSent API base address. One value serves every tool. |
| `ATLASENT_MODE` | `remote` with a key, else `local` | `remote` or `local`. |
| `ATLASENT_MCP_READONLY` | unset | `1` or `true` removes the mutating administrative tools (policy, webhook, permit revocation, SCIM, SIEM, evidence-export and VQP writes). Recommended for demos. It reduces the tools an agent can reach; it isn't a security boundary by itself. |

## Core tools

| Tool | Mode | What it does |
| - | - | - |
| `evaluate` | local or remote | Ask whether an action is allowed. Returns `allow`, `deny`, or `hold` (remote `escalate` maps to `hold`), with a `permit_token` on `allow`. On `allow`, don't execute yet. |
| `verify_permit` | local or remote | Verify a Permit at the execution boundary. Proceed only when `valid` is `true`. A successful verification consumes a single-use Permit. |
| `deploy_service` | local or remote | Protected deployment decision demo. It evaluates the request and may return a Permit, but it does not perform execution-boundary Permit Verification internally. Call `verify_permit` before any native side effect. |
| `atlasent_evaluate` | remote | Evaluate against your published policies with full context. Accepts `execution_payload_hash`, `target_id`, and `change_plan` bindings. Returns `allow`, `deny`, `hold`, or `escalate`. |
| `atlasent_verify_permit` | remote | Verify a Permit from `atlasent_evaluate` before executing. |
| `atlasent_await_approval` | remote | Wait for a person to decide a held action in the console. On approval it returns a Permit that must still pass `atlasent_verify_permit`. |
| `atlasent_check_permit` | remote | Read a Permit's status (`active`, `revoked`, `consumed`, `expired`) without consuming it. Not authorization. |
| `atlasent_get_permit`, `atlasent_get_decision` | remote | Read one Permit or one Decision record. |
| `atlasent_record_execution_evaluation` | remote | Record the observed outcome after the side effect. Doesn't replace verification beforehand. |
| `atlasent_lookup_action` | local or remote | Find the canonical action type for what the agent wants to do, for example `production.deploy` or `agent.tool.invoke`. Never invents an action type. |

The server also exposes read and administrative tools for policies, permits, audit events, evidence exports, webhooks, SCIM, SIEM, and VQP. Call MCP `tools/list` for the exact inventory of the version you installed.

No tool approves an action. Approvals are made by a person in the AtlaSent console, never by an agent.

## Agent pattern

```ts theme={null}
const decision = await evaluate({
  action_type: "agent.tool.invoke",
  actor_id: "agent:research-bot",
  environment: "production",
});
if (decision.decision !== "allow") throw new Error("Not authorized");

const verification = await verify_permit({
  permit_token: decision.permit_token,
  action_type: "agent.tool.invoke",
  actor_id: "agent:research-bot",
  environment: "production",
});
if (!verification.valid) throw new Error("Permit did not verify");

// Only now may the protected side effect run.
await runProtectedTool();
```

A prompt, wrapper, or MCP tool definition isn't automatically a gate. The side effect must be unreachable unless authorization and Permit verification succeeded.

## Fail-closed behavior

Treat each of these as **stop**:

* any Decision other than `allow`
* a missing Permit
* an authentication, network, or API error (the server turns errors into `deny`, or into `valid: false` on verify)
* an invalid, expired, revoked, replayed, or mismatched Permit
* a missing required binding

## Binding the Permit to the exact payload and target

To make a Permit valid only for one exact payload, bind a digest at evaluate time and present the same digest at verify time.

* Send `execution_payload_hash` as a **bare 64-character hexadecimal SHA-256 digest** at the **top level** of the evaluate request, not inside `context` and not with a `sha256:` prefix. For ordinary action types the API drops a value, without an error, in any other shape and binds the Permit to its own hash of the request instead, so your digest never constrains execution. Change-control action types deny it outright.
* The MCP server helps: it strips a `sha256:` prefix, converts the digest to lowercase, and refuses anything that isn't a 64-hex SHA-256 value rather than sending it.
* Check `execution_payload_hash_accepted` in the evaluate response. `false` means the Permit isn't bound to your digest.
* Pass `target_id` to bind the Permit to one target. Presenting a different target at verify time fails. Presenting a target at verify time that was never bound at evaluate time checks nothing.

## Change plans and approvals

`production.deploy`, `infrastructure.change`, `production.rollback`, and `secret.configuration.change` require a `change_plan` (`{ operation, revision?, artifact_ref? }`, with a revision or an artifact reference). Pass it to `deploy_service`, `evaluate`, or `atlasent_evaluate`.

When an action is held for a person, the result carries an `approval_request_id`. Call `atlasent_await_approval` with it. A rejection, expiry, or timeout returns no Permit, and the action doesn't run. If your plan changed while you waited, pass the new plan as `change_plan` and the server files one linked re-request for it.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.