MCP Server

The AGLedger MCP server connects any MCP-compatible agent runtime (Claude Desktop, Cursor, Windsurf, ChatGPT MCP, Gemini) to your AGLedger Server. It is a thin pass-through: the agent calls a small set of MCP tools, and those tools relay HTTP calls to the API and forward the responses verbatim. Nothing about the record lifecycle lives in the server - it carries the agent's calls and the API's guidance back and forth.

Use this guide when you want an agent to notarize through MCP rather than calling the HTTP API directly, or rather than embedding the SDK. For endpoint and field-level detail, the API reference is canonical - this page covers only the MCP setup and the tool surface.

The package is @agledger/mcp-server, pinned here at v3.0.0. It targets an API 2.0 Server, requires Node.js 24 or newer, and speaks MCP over stdio.

1. Install the server

npm install -g @agledger/mcp-server@3.0.0

On Linux hosts where the global npm prefix is root-owned (the NodeSource default), npm install -g fails with EACCES - run it with sudo, or point npm at a user-writable prefix first (npm config set prefix ~/.npm-global).

This installs the agledger-mcp binary. Confirm it runs and points at the version you expect:

agledger-mcp --help
AGLedger MCP Server v3.0.0

Usage: agledger-mcp --api-key <key> --api-url <url>
       AGLEDGER_OIDC_TOKEN_FILE=<path> agledger-mcp --api-url <url>

Options:
  --api-key, -k     AGLedger API key (or AGLEDGER_API_KEY env var).
  --api-url, -u     Base URL of your AGLedger instance (or AGLEDGER_API_URL env
                    var). Required: AGLedger is self-hosted, so there is no
                    default server to call.
  --timeout         Seconds to wait for each API response (or AGLEDGER_TIMEOUT
                    env var). Default 30, at most 2147483. A call that times
                    out may still have been applied: retry a POST with the
                    idempotencyKey the timeout error names, never a new one.
  --help, -h        Show this help message

Credentials: one is required. An API key wins when set; otherwise an OIDC
token source is exchanged for a short-lived cert (POST /v1/auth/oidc/cert),
re-exchanged at half its lifetime and on a 401, and the cert's key signs
request bodies. Nothing is written to disk.
  AGLEDGER_API_KEY           API key (same as --api-key)
  AGLEDGER_OIDC_TOKEN_CMD    Shell command whose stdout is an OIDC JWT; run on
                             every exchange. Wins over the token file.
  AGLEDGER_OIDC_TOKEN_FILE   File holding an OIDC JWT; read on every exchange,
                             so a rotated Kubernetes projected token is picked
                             up.
  AGLEDGER_OIDC_AGENT_ID     Optional assertion of the agent id the token
                             binds to. The token decides the agent; a
                             different id, or any id on a token bound to
                             no agent, is 403 CERT_AGENT_BINDING_MISMATCH.
                             Bind an agent with PATCH /v1/agents/{id}
                             (oidcIss, oidcSub) or claimMapping.agent_id
                             on the trusted issuer.

Delegation (optional): when the agent acts for a person or another party, an
RFC 8693 token-exchange result token is sent as AGLedger-On-Behalf-Of on every
POST. It comes from the process, never from a tool argument.
  AGLEDGER_ON_BEHALF_OF_CMD  Shell command whose stdout is the token. Wins over
                             the file.
  AGLEDGER_ON_BEHALF_OF_FILE File holding the token.
  Either is read again once the token's exp has passed, or when the Server
  refuses the delegation with a 401.

Tools:
  agledger_discover   Returns API health, your identity, and available scopes
  agledger_api        Make any AGLedger API call; the API guides you via nextSteps
  agledger_verify     Verify an audit export offline (hash chain + signatures)

Exit codes: 0 clean, 1 runtime failure, 2 usage or configuration error.

The server needs two things: a credential and the base URL of your Server. The URL comes from --api-url or AGLEDGER_API_URL. The credential is an API key (--api-key or AGLEDGER_API_KEY), which the server attaches to every call as a Bearer token, or an OIDC token source (below). Mint a key as described in Authentication. If either is missing the server prints an error and exits with code 2 rather than guessing - AGLedger is self-hosted, so there is no default Server to fall back to. A URL that is not an absolute http or https URL (localhost:3000 with no scheme, for example) is refused at startup the same way. --timeout <seconds> (or AGLEDGER_TIMEOUT) sets how long each call waits for the Server, default 30.

2. Wire it into your MCP client

Add the server to your client's MCP configuration. For Claude Desktop this is claude_desktop_config.json; other clients use the same mcpServers shape.

{
  "mcpServers": {
    "agledger": {
      "command": "agledger-mcp",
      "args": ["--api-url", "https://agledger.example.com"],
      "env": {
        "AGLEDGER_API_KEY": "agl_agt_…"
      }
    }
  }
}

Keep the API key in the env block, not in args - process arguments are visible in process listings, environment variables are not. Restart the client so it spawns the server. On connect the client lists three tools (agledger_discover, agledger_api, agledger_verify) and two resources (agledger://openapi and agledger://llms.txt).

Authenticate with OIDC instead of an API key

When your operator has registered your identity provider as a trusted issuer for agents, the server can run with no API key. Give it a token source in env, and it exchanges each token for a short-lived certificate the Server signs (POST /v1/auth/oidc/cert), presents the certificate as its bearer, and signs each request body with an Ed25519 key that exists only in its memory, so the chain entry records the agent's own signature:

{
  "mcpServers": {
    "agledger": {
      "command": "agledger-mcp",
      "args": ["--api-url", "https://agledger.example.com"],
      "env": { "AGLEDGER_OIDC_TOKEN_FILE": "/var/run/secrets/agledger/token" }
    }
  }
}

AGLEDGER_OIDC_TOKEN_CMD runs a command instead. The token decides which agent the certificate binds to; AGLEDGER_OIDC_AGENT_ID is only an assertion of that agent, and an id that names a different agent, or any id on a token bound to none, is refused 403 CERT_AGENT_BINDING_MISMATCH. Bind the agent with PATCH /v1/agents/{id} (oidcIss, oidcSub) or claimMapping.agent_id on the trusted issuer. An API key wins when one is set, then the command, then the file. The Server exchanges each token id once, so the source has to yield a new token for every exchange; a file still holding the token already exchanged keeps the current certificate until it expires. The kubelet rewrites a projected token at 80% of expirationSeconds, so keep that interval shorter than the trusted issuer's maxCredentialTtlSeconds. The Server side of the exchange is the OIDC-cert path.

Act on behalf of someone

When the agent works for a person or another party, set AGLEDGER_ON_BEHALF_OF_CMD or AGLEDGER_ON_BEHALF_OF_FILE to the RFC 8693 delegation token your IdP issued. The server sends it as AGLedger-On-Behalf-Of on every POST, and the Server seals the delegation into the chain entry: bound under the OIDC certificate, unbound on an API key. The token comes from the process, never from a tool argument, so the model can neither read nor choose it. See Authentication for the trusted issuer it needs.

3. The tools

The server exposes exactly three tools. The agent calls agledger_discover once to orient, drives the record lifecycle through agledger_api, and verifies an export with agledger_verify.

agledger_discover

No arguments. Returns your Server's health (fetched without a credential, so it answers even when the credential is broken), the calling identity (GET /v1/auth/me), the available scope profiles (GET /v1/scope-profiles), whether a delegation is configured and where it comes from (never the token), and a four-step quickstart. The agent calls this first - the response tells it who it is, what scopes it holds, and what to do next. It is read-only and makes no changes.

agledger_api

The workhorse. Makes any AGLedger API call and forwards the response unmodified. Four arguments:

ArgumentTypeNotes
methodGET / POST / PUT / PATCH / DELETErequired
pathstringmust start with /; all routes use the /v1/ prefix
paramsJSON-encoded string (or native object)optional. For GET/DELETE it becomes query parameters; for POST/PUT/PATCH it becomes the JSON body
idempotencyKeystringoptional. Dedup key for POST calls, max 256 characters; one is generated per call when omitted, so pass your own only when retrying a call that may have already reached the Server

The agent notarizes through this one tool. The tool's own description hands the agent the lifecycle, and every API response carries nextSteps and (on errors) a recoveryHint, so the agent self-guides:

  1. GET /v1/schemas - list the available record types (contract types).
  2. GET /v1/schemas/{type} - read the required fields and examples for the type.
  3. POST /v1/records - notarize a record. For a notarize-only type this terminalizes the record at RECORDED in that one signed call.
  4. POST /v1/records/{id}/completions - submit a Completion (the performer's evidence) when the work is done, for types that run the gated lifecycle.

Errors the tool raises itself carry a code and a suggestion: a bad path returns PATH_INVALID; malformed params returns INVALID_JSON; an argument the tool does not declare (such as body in place of params) returns UNKNOWN_ARGUMENT before anything is sent; a slow Server returns TIMEOUT; a connectivity failure returns NETWORK_ERROR. When either happens to a POST, the error carries the idempotencyKey the call sent and says to retry with that exact key, so the Server replays the first attempt if it was applied instead of recording the work twice. API-origin errors (a 403 for a missing scope, a 422 for a schema violation) are forwarded byte-for-byte - the API owns that guidance, and the agent reads its recoveryHint to recover.

agledger_verify

Verifies a record audit export entirely offline. The agent first pulls the export through agledger_api (method=GET, path=/v1/records/{id}/audit-export), then passes the response body as the export argument.

ArgumentTypeNotes
exportJSON-encoded string (or native object)required. The body from GET /v1/records/{id}/audit-export
trustAnchorsarray of stringsrecommended. SPKI digests (sha256:<64 hex>) of vault keys the operator took out of band: the installer prints the first vault key's, and the Server's signing-key-digest.js derives one from any key the operator holds. The signed key statements the export carries are walked from them. Do not take it from the export (exportMetadata.anchoredFrom) or from GET /v1/verification-keys, which are the Server's own word
distrustedKeysarray of stringsoptional. The operator's VAULT_DISTRUSTED_KEYS entries, sha256:<64 hex> optionally followed by @<RFC 3339 instant>. What such a key signed from that instant counts for nothing. Requires trustAnchors; without it the call is refused with INVALID_ARGUMENT
publicKeysJSON-encoded string (or native object)optional. Map of signingKeyId → SPKI-DER base64 public key, a [{keyId, publicKey}] list, or the GET /v1/verification-keys response as returned; merged over any keys embedded in the export, and each key's signed statements are walked from trustAnchors. Where a key came from is not whether it is trusted: the Server serves these from the same database as the export
agentKeysJSON-encoded string (or native array)optional. Ed25519 public keys (JWKs) of agent certificates. An entry whose sealed agent signature names one of them by thumbprint is re-verified and fails CHAIN_AGENT_SIGNATURE_INVALID if it does not verify; without it those signatures are counted in agentSignatures and not checked
requireKeyIdstringoptional. Reject any entry not signed by this key, even if otherwise valid
requireSuppliedKeysbooleanoptional. Refuse keys embedded in the export; an entry whose only key is export-embedded fails CHAIN_KEY_POLICY_VIOLATION, forcing verification against keys supplied via publicKeys. Replaces requireOutOfBandKeys, which is now refused as an unknown argument

The verifier decodes each entry's tagged COSE_Sign1 envelope (RFC 9052) over its in-toto v1 Statement payload, recomputes the SHA-256 over the envelope bytes, walks the hash chain, cross-checks the protected-header chain claim against the row columns, and verifies every envelope signature (Ed25519 or ES256, dispatched from the trusted key material; anything else fails closed). It makes no network calls.

The result leads with a verdict and a summary that states it in words with the next step:

verdictsummary beginsMeaning
trustedVerdict: PASSThe chain is valid and keyTrust.status is walked: every key is linked to a trustAnchors pin and a signature verified under one
unanchoredVerdict: VERIFIED, NOT ANCHOREDNothing failed, but this is not a trusted verdict. Either no trustAnchors were given (keyTrust.status no_anchor), so the keys were taken on the export's word, or the pin was walked and no signature verified under a key it anchors (no_anchored_signature), as for an export of unsigned entries. isError stays false
failedVerdict: FAILThe export did not verify; isError is true

On a failure, brokenAt pinpoints the first failing entry with a machine-readable code from the SCREAMING_SNAKE FailureCode taxonomy (CHAIN_HASH_MISMATCH, CHAIN_LINK_BROKEN, CHAIN_COSE_DECODE_FAILED, CHAIN_SIGNATURE_INVALID, and so on). An entry whose actorId, actorRole or actorOwnerId disagrees with the signed actor claim fails CHAIN_ACTOR_ATTRIBUTION_MISMATCH. An entry with no signing key written while the vault was signing fails CHAIN_ENTRY_UNSIGNED; unsigned history written before the first key stays reduced coverage. With trustAnchors, an entry signed by a key the walk does not reach fails CHAIN_SIGNING_KEY_UNANCHORED, and a key statement that does not hold fails at position 0 with KEY_STATEMENT_INVALID, KEY_CLOSURE_INVALID or CHAIN_KEY_WINDOW_DRIFT, listed under keyTrust.findings.

Do not read valid: true alone as a trusted result: it is also true on an unanchored run. Surface verdict, and signatureCoverage ({ signed, unsigned, skipped, total }) so an unsigned-but-chain-intact entry is not mistaken for a signed one.

The agledger://openapi resource

Alongside the tools, the server exposes the live OpenAPI spec as an MCP resource. Clients surface it in their resource picker, so the agent (or operator) can pull the full route catalog into context without constructing a call. The resource proxies GET /openapi.json at read time - it is always current with the running Server, never a stale local copy. A second resource, agledger://llms.txt, proxies the Server's GET /llms.txt the same way: the prose orientation the API writes for agents, worth reading before the spec.

Verify it worked

After wiring the config, have the agent call agledger_discover. A healthy connection returns your identity and scopes; the identity block mirrors GET /v1/auth/me and should show your role and authType. If health carries an error instead, the server reached your runtime but could not reach the API - check --api-url. If health is fine and identity carries an error, the credential is the problem.

Failure modes

SymptomCauseFix
Server exits immediately on start (code 2)No credential, no API URL, a URL with no http or https scheme, an invalid --timeout, an unknown flag, or a token file it cannot readSet AGLEDGER_API_KEY or an OIDC token source in the client's env block, and pass the full --api-url (scheme included) or set AGLEDGER_API_URL
agledger_discover returns health.errorAPI URL wrong or Server unreachableConfirm --api-url resolves from where the client runs; try GET /health directly
agledger_api returns NETWORK_ERRORDNS or transport failure to the APICheck the URL; the suggestion points you at GET /health
agledger_api returns TIMEOUTServer slow to respond (default 30s)The call may still have been applied. Retry a POST with the idempotencyKey the error names, never a new one; raise the wait with --timeout <seconds> or AGLEDGER_TIMEOUT
agledger_api returns a 403 bodyKey lacks the scope for the callMint a key with the right scope profile (see Authentication)
agledger_api returns INVALID_JSONparams was not a JSON objectPass params as a JSON-encoded object string, e.g. {"type":"notarize-generic-v1"}
A tool returns OIDC_TOKEN_SOURCE_FAILEDThe token command failed or the token file is unreadableThe result carries the command's stderr with tokens removed; fix the source
A tool returns OIDC_EXCHANGE_FAILEDThe Server refused the certificate exchangeRead the forwarded status and recoveryHint; the trusted issuer or the agent binding is usually the cause
agledger_verify returns INVALID_ARGUMENTA malformed trustAnchors or distrustedKeys entry, distrustedKeys without trustAnchors, or a pinned key distrusted with no instantPass trustAnchors as ["sha256:<64 hex>"]; give a distrusted key that is also pinned its instant
agledger_verify returns verdict: "unanchored"No trustAnchors, or none of the export's signatures verified under an anchored keyAsk the operator for the digest of a vault key and call again with it in trustAnchors
agledger_verify returns INVALID_EXPORTInput was not an audit exportFetch it first with GET /v1/records/{id}/audit-export, then pass the response body
agledger_verify reports UNSUPPORTED_FORMATThe export is not format 2.0Re-export the chain from a current Server, or upgrade the MCP server to match it

Air-gapped operation

The MCP server has no runtime dependency on the AGLedger website, Docker Hub, or npm once it is installed. Every flow above works against a Server on your own network with no outbound internet.

Install without npm at runtime. On a connected staging host, fetch the package tarball once and carry it across the boundary:

npm pack @agledger/mcp-server@3.0.0

This produces agledger-mcp-server-<version>.tgz. Transfer it into the air-gapped environment and install from the file - no registry contact:

npm install -g ./agledger-mcp-server-<version>.tgz

Point it at your internal Server. Set --api-url (or AGLEDGER_API_URL) to your in-network Server. All three tools then operate without leaving the network: agledger_discover and agledger_api call only that Server, and agledger_verify is offline by construction.

Verification needs no Server at all. Because agledger_verify makes no network calls and roots trust in a vault key you pin, an auditor can verify a chain on a fully disconnected machine. Carry the audit-export JSON and the operator's vault-key digest across the boundary, pass the digest in trustAnchors (and requireKeyId to require one signing key), and the verdict is self-contained. The same offline verification is also available as a standalone step - see Verify offline.