@agledger/mcp-server
Official# @agledger/mcp-server
The official [MCP](https://modelcontextprotocol.io) server for the [AGLedger](https://agledger.ai) API: change control for AI agents. Agent memory, approvals, audit trail, and notifications: one API, one signed ledger, self-hosted.
Requires an AGLedger Server 2.0 or later. For a 1.x Server, use `@agledger/mcp-server@2.11.0`, the last release that supports it.
Connects any MCP-compatible AI agent (Claude, Cursor, Windsurf, etc.) to the AGLedger API with 2 universal API-pass-through tools plus an offline audit verifier. No SDK code required. Just point your agent at this server.
**Learn more**
- [agledger.ai](https://agledger.ai): what AGLedger is and who needs it
- [How it works](https://agledger.ai/how-it-works) walks the lifecycle: record, completion, verdict
- [Glossary](https://agledger.ai/glossary): canonical definitions of Record, Completion, SCITT Receipt, Verdict, Settlement Signal
- [MCP Server guide](https://agledger.ai/docs/guides/mcp-server): installation and agent workflow
## Install
```bash
npm install -g @agledger/mcp-server
```
## Quick Start
Add to your MCP client configuration (e.g. `claude_desktop_config.json`):
```json
{
"mcpServers": {
"agledger": {
"command": "agledger-mcp",
"args": [
"--api-key", "your-api-key",
"--api-url", "https://your-agledger-instance"
]
}
}
}
```
Both flags are required, unless an OIDC token source stands in for the key (see
[below](#authenticating-with-oidc-instead-of-an-api-key)). AGLedger is
self-hosted, so there is no default server to call: without `--api-url`, or
with one that is not an absolute `http` or `https` URL, the server exits before
it accepts a connection.
Or run directly:
```bash
agledger-mcp --api-key <key> --api-url <url>
```
Exit codes: `0` clean, `1` runtime failure, `2` usage or configuration error
(missing flag, unknown flag, an API URL that is not an `http(s)` URL), so a launcher can tell a misconfiguration from a
crash.
## Authenticating with OIDC instead of an API key
When the operator of your AGLedger Server has registered your identity
provider as a trusted issuer for agents, the MCP server can run with no API key
at all. Give it a source of OIDC tokens instead. It exchanges a token for a
short-lived cert signed by the Server (`POST /v1/auth/oidc/cert`), presents the
cert as its bearer, and signs each request body with an Ed25519 key that exists
only in its own memory. The Server verifies that signature and seals it into
every chain entry the agent writes (`predicate.on_behalf_of.agent_signature`).
Nothing is written to disk.
| Env var | Description |
|---------|-------------|
| `AGLEDGER_OIDC_TOKEN_FILE` | A file holding an OIDC JWT, such as a Kubernetes projected service-account token. Read on every exchange, so a token rotated on disk is picked up. |
| `AGLEDGER_OIDC_TOKEN_CMD` | A shell command whose stdout is an OIDC JWT. Run on every exchange. |
| `AGLEDGER_OIDC_AGENT_ID` | Optional. An assertion of the agent id the token binds to, sent on the exchange. It never chooses the agent: when it names a different one, or the token binds none, the Server refuses the exchange with 403 `CERT_AGENT_BINDING_MISMATCH` rather than issue a cert for an agent you did not expect. Leave it unset unless you want that check. |
An API key wins when one is set, then the command, then the file.
**The token decides which agent the cert binds to**, never the MCP server's
configuration. An administrator binds an agent to the token's identity in one
of two ways: `PATCH /v1/agents/{id}` with `oidcIss` and `oidcSub` (the token's
issuer and subject), or a `claimMapping` of `agent_id` on the trusted issuer,
which reads the agent id from a claim in the token. An issuer that
auto-provisions agents creates one for an unbound subject instead. A token that
binds no agent is refused, and the Server's `recoveryHint` says which binding is
missing. A token bound to a federation shadow agent (one listed at
`GET /v1/peer-agents`) is refused with 403 `SHADOW_AGENT_CERT_FORBIDDEN`: that
agent authenticates on its own Server.
```bash
AGLEDGER_OIDC_TOKEN_FILE=/var/run/secrets/agledger/token \
agledger-mcp --api-url https://your-agledger-instance
```
In an MCP client configuration, the token source goes in `env`:
```json
{
"mcpServers": {
"agledger": {
"command": "agledger-mcp",
"args": ["--api-url", "https://your-agledger-instance"],
"env": { "AGLEDGER_OIDC_TOKEN_FILE": "/var/run/secrets/agledger/token" }
}
}
}
```
The cert is re-exchanged once half its lifetime has passed, and once more if
the Server answers 401 (a revoked cert), after which the request is retried a
single time. A refresh that fails while the current cert is still valid keeps
the current cert and prints one warning on stderr. When the exchange itself is
refused, the tool result carries `code: OIDC_EXCHANGE_FAILED`, the Server's
status and error body, and its `recoveryHint` as the result's `suggestion`.
The Server exchanges a token id (`jti`) only once, so every exchange needs a
new token. A command runs on every exchange; a file is read on every exchange,
and while it still holds the token already exchanged, the server keeps using
the current cert rather than asking. The kubelet rewrites a projected token at
80% of `expirationSeconds`, so keep that interval shorter than the trusted
issuer's `maxCredentialTtlSeconds`, or the cert expires before a new token
appears:
```yaml
volumes:
- name: agledger-token
projected:
sources:
- serviceAccountToken:
audience: agledger
expirationSeconds: 600
path: token
```
## Acting on behalf of someone
When the agent does work for a person or another party rather than for
itself, the operator can give the server an RFC 8693 delegation token: the
token-exchange result your IdP issues, whose `act` claim names the agent. The
server sends it as the `AGLedger-On-Behalf-Of` header on every POST, and the
Server validates it against a trusted issuer registered with
`appliesTo: principal` and seals it into the chain entry as
`predicate.on_behalf_of`. The token comes from the process, never from a tool
argument, so the model can neither read nor choose it; `agledger_discover`
reports only whether a delegation is configured and where it comes from.
| Env var | Description |
|---------|-------------|
| `AGLEDGER_ON_BEHALF_OF_CMD` | A shell command whose stdout is the delegation token. Wins over the file. |
| `AGLEDGER_ON_BEHALF_OF_FILE` | A file holding the delegation token. |
```bash
AGLEDGER_OIDC_TOKEN_FILE=/var/run/secrets/agledger/token \
AGLEDGER_ON_BEHALF_OF_FILE=/var/run/secrets/agledger/on-behalf-of \
agledger-mcp --api-url https://your-agledger-instance
```
A delegation token is not single use, so it is kept until its `exp` and read
again after that, or when the Server refuses it with a 401 that names the
delegation (the request is then retried once). With the OIDC cert credential
the delegation is recorded `bound`: the token's `act.sub` and actor issuer must
be the cert's own subject and issuer, or the Server answers 403
`ACTOR_BINDING_MISMATCH`. With an API key there is no validated caller identity
to compare, and it is recorded `unbound`.
## Tools
| Tool | Description |
|------|-------------|
| `agledger_discover` | Returns API health, your identity, available scopes, and a quickstart workflow. Call this first. |
| `agledger_api` | Make any AGLedger API call (method, path, params). Every POST carries a generated `Idempotency-Key`; pass `idempotencyKey` to reuse the first attempt's key when retrying a call that may already have landed. The API returns `nextSteps` on every response for self-guided workflow discovery. |
| `agledger_verify` | Verify a record audit export offline (COSE_Sign1 envelopes per RFC 9052, hash chain + envelope signatures, Ed25519 or ES256). No network calls. Returns the verifier's result as one JSON text block (the same object as `structuredContent`), with `summary` stating the verdict and the next step. Pass `trustAnchors` (`["sha256:<digest>"]`, the SPKI digest of a vault key the operator took out of band; the installer prints the first one) so the export's signed key statements are walked from a key you trust: an entry under a key the walk does not reach fails `CHAIN_SIGNING_KEY_UNANCHORED`, and a statement that does not hold fails `KEY_STATEMENT_INVALID`, `KEY_CLOSURE_INVALID` or `CHAIN_KEY_WINDOW_DRIFT`. The result carries `verdict`: `trusted` (text `Verdict: PASS`) only when the chain is valid and the walk reached a signature, `failed` (`Verdict: FAIL`), or `unanchored` (`Verdict: VERIFIED, NOT ANCHORED`), which is not a trusted verdict. A run is unanchored without `trustAnchors` (`keyTrust.status` `no_anchor`, the keys taken on the export's word) and when the pin was walked but no signature verified under a key it anchors (`no_anchored_signature`, as for an export of unsigned entries). `distrustedKeys` takes the operator's `VAULT_DISTRUSTED_KEYS` entries and needs `trustAnchors`, and a key both pinned and distrusted is refused (`INVALID_ARGUMENT`), as the Server refuses to start with it; a key statement a distrusted key signed counts for nothing at any instant, because the write times an export carries are not signed. Where an export lists a key retired at the instant the Server distrusts it from (`distrustedFrom`), earlier than its signed retirement, a pinned call without the same `distrustedKeys` entry fails on that key's window (`CHAIN_KEY_WINDOW_DRIFT`, or `KEY_CLOSURE_INVALID` where no closure retires it), and the finding names the entry (`distrustedKeys sha256:<hex>@<distrustedFrom>`), the listing's unsigned word, to confirm with the operator before passing it. `publicKeys` supplies keys (the `GET /v1/verification-keys` response works as is), `requireSuppliedKeys` refuses the export's embedded keys, and `agentKeys` (the agents' cert public keys as JWKs) re-verifies the agent signatures sealed in the chain. Input verify-core refuses, such as a supplied key window that is not RFC 3339, is `INVALID_ARGUMENT`. Built on the shared `@agledger/verify-core`. |
## Resources
Both are fetched live from the running instance, so neither can go stale.
| Resource | Description |
|------|-------------|
| `agledger://llms.txt` | The API's agent-oriented documentation narrative (the llms.txt convention): what the product does, the vocabulary, and how records, completions, gates and webhooks fit together. Read this first if you are new to the API. |
| `agledger://openapi` | The OpenAPI 3.0 specification, for exact routes and request/response shapes. |
### Agent workflow
The `agledger_discover` tool returns a quickstart workflow that guides agents through the accountability flow:
1. `GET /v1/schemas` -- list available Record types
2. `GET /v1/schemas/{type}` -- get required fields and examples
3. `POST /v1/records` -- create a record
4. `POST /v1/records/{id}/completions` -- submit a completion (evidence) when done
Every API error response includes a `recoveryHint` naming the fix, so an agent can correct itself without a human. Errors raised by the MCP server itself (an argument the tool does not declare, a timeout, a credential failure) carry a `code` and a `suggestion` instead.
## Configuration
| Flag | Env Var | Description |
|------|---------|-------------|
| `--api-key` | `AGLEDGER_API_KEY` | AGLedger API key. Required unless an OIDC token source is set (see above). |
| `--api-url` | `AGLEDGER_API_URL` | API base URL of your instance (required). AGLedger is self-hosted, so there is no default; the server refuses to start without it. |
| `--timeout` | `AGLEDGER_TIMEOUT` | Seconds to wait for each API response (default 30). A positive number up to 2147483; anything else exits 2. |
A call that times out may still have been applied. On a POST, the `TIMEOUT` error carries the `idempotencyKey` that was sent: retry with exactly that key and the Server returns the original result instead of creating a second record. Reads and other methods carry no key.
## What is AGLedger?
AGLedger is change control for AI agents, delivered as a signed ledger for agentic work. Agents notarize what they intended and what they did, principals approve the work that needs a decision, and every entry is signed, hash-chained and verifiable offline.
- **Records** -- structured commitments with acceptance criteria and tolerance bands
- **Completions** -- performer evidence recording what was reported to be done
- **Verdicts** -- principal accept/reject decisions on a Completion (the Gate), with settlement signals
- **Audit chain** -- notarized records as COSE_Sign1 envelopes, hash-chained, Ed25519-signed, tamper-evident, verifiable offline
Learn more at [agledger.ai](https://www.agledger.ai) | [API docs](https://www.agledger.ai/docs/)
Each self-hosted AGLedger instance also serves interactive Swagger UI at `{AGLEDGER_API_URL}/docs`.
## Requirements
- Node.js >= 24
- A running self-hosted AGLedger API instance, and an API key or a trusted OIDC issuer (see the self-hosted install guide at [agledger.ai](https://www.agledger.ai))
## License
Proprietary. See [LICENSE](./LICENSE).
AGLedger is a trademark of AGLedger LLC, and Settlement Signal is a pending trademark of AGLedger LLC. All other trademarks are the property of their respective owners. Patent pending.
TDQS
Scored across 3 tools
Each tool has a clearly distinct role: discover for orientation, api for arbitrary API calls, and verify for offline audit verification. There is no meaningful overlap in purpose, even though api could technically call discover endpoints; the descriptions direct the agent to use discover first.
All tool names share the consistent agledger_ prefix, but the second parts mix verb forms (discover, verify) with a noun (api). This mixed convention makes the naming pattern less predictable, though still readable.
With only 3 tools, the set is well-scoped. The generic agledger_api tool serves as a catch-all for API operations, while discover and verify fill specialized roles, so each tool earns its place.
The generic agledger_api tool provides access to the full API surface, covering schemas, records, completions, and exports. Discover and verify add orientation and offline verification, leaving no obvious gaps in the tool surface.