Skip to main content
Glama
@proof-holdings/mcp-server

@proof-holdings/mcp-server

Official
README.md
# @proof-holdings/mcp-server

MCP (Model Context Protocol) server for the [proof.holdings](https://proof.holdings) API. Exposes 179 tools for AI agents to create verifications, validate proofs, manage assets, and more.

## Two ways to connect

**Hosted — nothing to install.** The same tools are served over HTTP at
`https://api.proof.holdings/mcp`. Any MCP client or agent framework with HTTP transport can
connect — add the URL as a remote MCP server in its settings (the config key varies by client).
Authentication happens in the browser the first time a tool needs an account. As a config fragment:

```json
{ "mcpServers": { "proof": { "type": "http", "url": "https://api.proof.holdings/mcp" } } }
```

⚠️ **Versions before `1.1.0` predate the delegation tools and the keyless public mode** and expose
an older, smaller surface than this README describes. If a client is pinned to `1.0.0`, upgrade it
or use the hosted server above. The current instructions are always at
<https://proof.holdings/md/docs/mcp.md>.

**Local — this package.** Installs and runs as a stdio server:

```bash
npm install -g @proof-holdings/mcp-server
```

Or run directly with npx (no install needed):

```bash
npx @proof-holdings/mcp-server
```

## Configuration

| Variable | Required | Default | Description |
|---|---|---|---|
| `PROOF_API_KEY` | No | — | API key (`pk_live_...` or `pk_test_...`). Without it the server still starts in **public mode**: the keyless tools (account bootstrap, login, proof and delegation verification) work, and every other tool answers `api_key_required`. |
| `PROOF_BASE_URL` | No | `https://api.proof.holdings` | API base URL |

Get your API key from the [proof.holdings dashboard](https://proof.holdings/dashboard/api-keys), or let the
server get one for you with `login` (below).

## Sign in from a terminal

```bash
npx @proof-holdings/mcp-server login
```

1. Pick how to log in — Telegram, WhatsApp, SMS or email (`--channel`, or asked). The terminal shows the
   link and a QR code to scan with your phone, the text and number to send for SMS, or asks for the code
   from the email.
2. Creating an API key needs one more confirmation, preferably on a different channel; it also runs in the
   terminal.
3. The new key is saved in `~/.proof-holdings/credentials` (readable by you only), under the API in
   `PROOF_BASE_URL`. The server uses it from its next start, so restart your MCP client's server.
4. The sign-in session the terminal used is then signed out. If that fails, `login` says so; sign it out
   in the dashboard under Settings → Security → Signed-in devices.

`PROOF_API_KEY`, when set, always wins over the saved key, and a key saved for one API (say, staging) is
never sent to another. Then leave `env` out of the client config:

```json
{ "mcpServers": { "proof-holdings": { "command": "npx", "args": ["-y", "@proof-holdings/mcp-server"] } } }
```

| Command | What it does |
|---|---|
| `login` | Signs in, confirms, creates and saves a key. `--test` creates a test key (`pk_test_`); `--scopes a,b` limits it (default: full access); `--email you@example.com` logs in by email. |
| `status` | Shows which key the server would start with and where it comes from. No network call. |
| `logout` | Removes the saved key for this API from this computer. |

The key does not expire. Logging in again replaces the saved key but does not revoke the old one, and
`logout` only forgets it locally: revoke keys you no longer use in the dashboard (API keys) or with the
`revoke_api_key` tool. `login` needs an interactive terminal and an `https://` API (plain `http` only for
localhost).

## Client Setup

Add the following to your MCP client config file:

```json
{
  "mcpServers": {
    "proof-holdings": {
      "command": "npx",
      "args": ["-y", "@proof-holdings/mcp-server"],
      "env": {
        "PROOF_API_KEY": "pk_live_your_key_here"
      }
    }
  }
}
```

Config file location by client:

| Client | Config file |
|---|---|
| Claude Desktop (macOS) | `~/Library/Application Support/Claude/claude_desktop_config.json` |
| Claude Desktop (Windows) | `%APPDATA%\Claude\claude_desktop_config.json` |
| Cursor | `.cursor/mcp.json` in your project root |
| Claude Code | Run `claude mcp add proof-holdings -- npx -y @proof-holdings/mcp-server` |
| Windsurf | `~/.codeium/windsurf/mcp_config.json` |

Keep the scoped `npx -y @proof-holdings/mcp-server` command even after a global install. A bare command
name is looked up on `PATH`, or by `npx` in the npm registry, where it can belong to someone else's package.

## Tools (179 tools)

Group totals are exact. The tables name the tools you are most likely to reach for rather than all
of them — your MCP client's own `tools/list` is the complete, current list, and it is the one this
server answers from.

### Verifications & requests (28 tools)

| Tool | Description |
|---|---|
| `create_verification` | Create a verification challenge |
| `get_verification` | Get verification status |
| `submit_verification_code` | Submit a verification code |
| `trigger_verification` | Trigger a verification check |
| `wait_for_verification` | Poll until it completes |
| `create_multi_channel_verification` | One phone, up to three channels, first completion wins |
| `create_verification_request` | Create a multi-asset request |
| `get_request_by_reference` | Look a request up by reference id |

### Domains & DNS (23 tools)

| Tool | Description |
|---|---|
| `add_domain` | Add a domain to verify |
| `verify_domain` | Check the challenge record and mint the proof |
| `connect_cloudflare` | Connect Cloudflare so records are written for you |
| `verify_domain_with_credentials` | Prove control using stored credentials |
| `setup_domain_email` | Set up sending from the domain |

### Account, settings & billing (39 tools)

| Tool | Description |
|---|---|
| `get_platform_summary` | One-call snapshot of the account |
| `get_usage` | Quota and usage for the period |
| `get_balance` | The prepaid balance and the auto top-up setting |
| `search` | Search across the account |
| `create_account` | Bootstrap a new account (no key needed) |
| `create_api_key` | Create a scoped API key |
| `list_assets` | List verified assets and their proof handles |

### HITL approvals & consent (22 tools)

| Tool | Description |
|---|---|
| `create_hitl` | Create a human-approval configuration |
| `create_confirmation` | Send an approval request to a person |
| `wait_for_confirmation` | Poll until a person approves or denies |
| `create_authorization` | Ask a person to consent to being contacted |
| `revoke_authorization` | Withdraw a consent |

### Circles & Proof of Me (15 tools)

| Tool | Description |
|---|---|
| `create_circle` | Create a circle of trusted contacts |
| `add_circle_member` | Add a contact |
| `invite_circle_member` | Send a single-use enrollment link |
| `create_identity_challenge` | Run a cross-channel identity check |

### Public profiles (12 tools)

| Tool | Description |
|---|---|
| `create_profile` | Create a public profile |
| `claim_username` | Claim a public username |
| `update_public_proofs` | Choose which proofs a profile shows |

### Templates & webhooks (11 tools)

| Tool | Description |
|---|---|
| `list_templates` | List a sub-account's message templates |
| `render_template` | Render a template with variables |
| `list_webhook_deliveries` | List webhook deliveries |
| `retry_webhook_delivery` | Retry a failed delivery |

### Webhook endpoints (7 tools)

Each endpoint has its own URL, events and signing secret, and serves either every sub-account or
the ones you select. A signing secret is returned only on create and on rotate — store it then.

| Tool | Description |
|---|---|
| `list_webhook_endpoints` | List the account's webhook endpoints |
| `get_webhook_endpoint` | Get one endpoint (no secret) |
| `create_webhook_endpoint` | Add an endpoint; returns its signing secret |
| `update_webhook_endpoint` | Change URL, events, scope, sub-accounts or status |
| `delete_webhook_endpoint` | Delete an endpoint |
| `rotate_webhook_endpoint_secret` | Replace an endpoint's signing secret |
| `rotate_callback_secret` | Replace a sub-account's callback signing secret |

### Delegations & proofs (13 tools)

| Tool | Description |
|---|---|
| `create_delegation` | Authorize an artifact from a domain you have proven |
| `revoke_delegation` | Revoke a delegation |
| `verify_delegation` | Check whether an artifact is authorized by the domain it claims |
| `validate_proof` | Verify a signed proof token (no key needed) |
| `get_proof_status` | Read a proof's status by its public handle |
| `list_revoked_proofs` | Read the revocation list (no key needed) |
| `get_proof_token` | Fetch the current (re-signed) token of a proof you own (not one of your own verified assets: that is read in the dashboard) |
| `list_reissued_proofs` | List your proofs re-signed since an instant, except those of your own verified assets |
| `get_delegation_token` | Fetch an active delegation's current token by handle (no key needed) |

### Sign-in & sessions (9 tools)

| Tool | Description |
|---|---|
| `start_login` | Begin a sign-in (no key needed) |
| `wait_for_login` | Poll until sign-in completes |
| `get_current_user` | Who the current session belongs to |
| `render_auth_link` | Render a sign-in link for the user to open |

## Test Mode

Use a test-mode API key (`pk_test_*`) to interact with the API without creating real verifications. Test-mode keys are available in your [dashboard](https://proof.holdings/dashboard/api-keys).

## Transport

Two transports ship in this package:

- **stdio** (default, what `npx -y @proof-holdings/mcp-server` starts) — the server communicates over
  stdin/stdout. One user per process; this is what a client launches locally.
- **Streamable HTTP** (`node dist/remote.js`) — a remote server that many users connect to over the
  network by URL, with no install. A connection starts ANONYMOUS — the keyless surface (account
  bootstrap, login, proof and delegation verification) works with no credential at all — and a tool
  that needs an account answers `401` with a `WWW-Authenticate` challenge naming the authorization
  server, which is what a standards-compliant client follows to sign in. The 401 lands on the TOOL
  CALL and never on a bare `initialize` or `tools/list` FOR AN ANONYMOUS CONNECTION: measured
  against live clients, refusing an anonymous handshake reads to the user as a connection timeout
  rather than as an invitation to log in. Three shapes are refused at the handshake instead — a
  presented token that does not resolve (there the 401 is what makes a client refresh), a request
  whose credential does not match the session it names, and an opening batch that smuggles a keyed
  tool call alongside `initialize`. A
  signed-in client sends the API key it was granted in the `Authorization` header — the only place a
  credential is read, never a query parameter — and each connection gets its own server and HTTP
  client, so one user's key or session can never reach another. `/mcp` serves `POST` (every
  JSON-RPC message) and `DELETE` (ending a session); every other method — including the optional
  standalone `GET` event stream — answers `405` with `Allow: POST, DELETE`, whatever session or
  credential the request carries. Nothing on this server pushes notifications, so declining the
  stream costs a client nothing, and it is what lets a client on Node 26's built-in `fetch` work:
  over HTTP/2 to the edge, with that stream open, it never sent its next request. `PORT` (default 3100),
  `MCP_MAX_SESSIONS` (default 100), `MCP_SESSION_TTL_MS` (default 30 min, counted from the last POST
  the server ACCEPTED — one it answered below 400. A POST refused before any work happens does not
  postpone it, whether the refusal is ours (body over 4MB) or the transport's (unparseable or empty
  body, unsupported `mcp-protocol-version`, a second `initialize`), and neither does a refused method.
  **The official client does not recover from a sweep on its own**: measured against SDK 1.27.1 —
  while the event stream was still offered, so not re-measured since — the next tool call fails with
  `unknown_session` until the host reconnects the server. Size the TTL with that in mind — it is a
  memory bound paid for in reconnects, not a transparent one);
  `/healthz` reports the live session count.

## Delegation (`_meta`)

The server card (`server.json`) can carry a **Proof of Delegation** publication under the
namespaced `_meta` key `holdings.proof/delegation`:

```json
{
  "_meta": {
    "holdings.proof/delegation": { "token": "<delegation JWT>" }
  }
}
```

The token is an ES256 JWT minted by [proof.holdings](https://proof.holdings) attesting exactly
one thing: **the controller of the `principal` domain authorized the `delegate` artifact.** It is
not a statement that the server is safe, audited, or endorsed. A verifier checks the signature
against the issuer JWKS, then compares `principal` and `delegate` to facts it resolved itself —
a token copied into another package fails that comparison, because its `delegate` names the
genuine artifact. Details: [Delegations — API reference](https://proof.holdings/docs/api#delegations).

A card published through the official MCP registry must carry the entry under
`_meta["io.modelcontextprotocol.registry/publisher-provided"]` (the registry drops every other
`_meta` key), so readers check both locations. This package's own card carries only the top-level
key, so its registry entry carries no delegation; the website card
`https://proof.holdings/.well-known/mcp/server-card.json` does.

Maintainers: the entry is written by the fail-closed publish tool, never by hand — from the
repository root, after the delegation is minted for `pkg:npm/@proof-holdings/mcp-server`:

```bash
npm run delegation:publish -- --target mcp --token <jwt>
# or mint + publish in one step (needs PROOF_API_KEY):
npm run delegation:publish -- --target mcp --mint --control-proof ph_ctl_<32hex>
```

The tool refuses any token whose claims do not name this exact package with
`principal: proof.holdings` (the same check `--target a2a` performs against the A2A agent
card's own `url` before regenerating `/.well-known/agent-card.json`). Besides `server.json`,
`--target mcp` writes the website card's source (`web/src/lib/geo/mcp-delegation-publication.json`)
and regenerates `/.well-known/mcp/server-card.json` and `/.well-known/mcp.json`, so it needs
`web/node_modules` installed. For either target, if the card-regeneration step fails after the
source file is written, just re-run the command — the token is already validated and the
regeneration is idempotent. The website cards reach readers only with the next website deploy.

### Verifying someone else's delegation (`verify_delegation`)

This server also **checks** delegations, not just publishes one. The `verify_delegation` tool
runs the reference verifier ([`@proof-holdings/delegation-verifier`](https://github.com/ProofHoldings/delegation-verifier))
over another server's card and needs **no API key** — verification runs against public surfaces
only.

```jsonc
{
  "card": { /* the MCP server.json or A2A agent card you fetched */ },
  "delegate": { "type": "purl", "value": "pkg:npm/postmark-mcp" },
  "expected_principal": "postmarkapp.com"
}
```

Both pins are **required**, and they close different attacks:

- `delegate` must be the artifact identity **you** resolved — the package you are about to
  install, the endpoint you are about to call. Never copy it out of the card being checked: a
  published token is a bearer artifact, so comparing it against a field of the same card would
  bless a token pasted in from somewhere else.
- `expected_principal` is the domain you expect to stand behind it. An issuer binds the
  artifact to nothing, so any domain owner can mint a genuine, signature-valid delegation
  naming someone else's package. Without this pin a verdict would only mean "some domain
  claims this".

### Re-checking everything you already trust (`verify_delegations`)

`verify_delegation` answers "is this one good, right now, because you asked". If an agent has
already resolved and verified thirty-five artifacts, re-verifying them one call at a time does
not scale — `verify_delegations` batch-checks up to 50 in a single call, also with **no API key**.

```jsonc
{
  "items": [
    { "card": { /* ... */ }, "delegate": { "type": "purl", "value": "pkg:npm/postmark-mcp" }, "expected_principal": "postmarkapp.com" },
    { "token": "<jwt>", "delegate": { "type": "url", "value": "https://example.com/mcp" }, "expected_principal": "example.com" }
  ]
}
```

Each item takes exactly the shape `verify_delegation` requires (`card` XOR `token`, `delegate`,
`expected_principal`) and is verified **independently** — no
cross-item state, nothing persisted, and one item failing never affects another item's result.
Revocation is always checked (there is no `check_status: false` on this tool — the entire point
of a batch re-check is to see what changed).

**Two things this tool deliberately does NOT do:**

- **It does not discover what you have installed.** You must already hold each artifact's
  `card` or `token`. It has no DNS-pointer resolution and fetches no caller-supplied URL —
  the same trust boundary `verify_delegation` already draws, kept narrow on purpose (see
  `src/services/delegationPointer/resolve.ts`'s documented gaps in the main repository, which
  this tool stays outside of).
- **It does not run continuously.** Each call is a single point-in-time check. There is no
  cadence, no scheduler, no push notification — call it again whenever you want a fresh answer.

Each result in `results[]` carries an `outcome`, one of four buckets:

| Outcome | Meaning |
| --- | --- |
| `confirmed_valid` | The delegation verified — same meaning as `verify_delegation`'s `verified: true`. |
| `confirmed_invalid` | A genuine negative verdict: revoked, suspended, expired, a mismatched principal or delegate, a token of schema 1.x (`unsupported_version`), or a malformed/untrusted/badly-signed token. |
| `no_claim_found` | The artifact publishes no delegation at all. An absence, never an accusation. |
| `unconfirmed` | We could not reach the issuer or otherwise get a confident answer right now (e.g. its JWKS or status endpoint is unreachable). **Never treat this as a bad verdict** — it means "ask again later", not "revoked". |

A result also carries `checked_at` — the ISO timestamp of the moment **that item's own check**
completed, not one timestamp shared across the whole call — so "established locally" is never
presented as "established by reaching us, at this moment" without saying which.

### ⚠️ Release order (maintainers)

`package.json` declares `@proof-holdings/delegation-verifier` as a **runtime** dependency, so the
range it names has to be resolvable on the registry before this package is uploaded:

**`@proof-holdings/delegation-verifier` is published BEFORE `@proof-holdings/mcp-server`, on every
release that moves the range.** This server is launched via `npx` by every documented client, so a
release whose dependency the registry cannot resolve makes `npx @proof-holdings/mcp-server` fail
with E404 for everyone until the verifier lands. A run of
`.github/workflows/publish-packages.yml` with `target: all` enforces the order by its step order;
a publish by hand from a terminal has nothing enforcing it but `docs/runbooks/npm-release.md`.

Working on the verifier and the server together does not need a publish. Link the sibling instead:

```bash
npm run verifier:link   # cd mcp && npm install ../packages/delegation-verifier --no-save
```

In this repository the test suite needs no link at all: it resolves the verifier's SOURCE through a
vitest alias rather than `node_modules`, so it is green on a fresh clone and picks up an uncommitted
verifier change without a build. The alias is keyed on the sibling verifier package being present;
in the public mirror, which holds this package alone, the suite runs against the verifier `npm ci`
installs.

## Requirements

- Node.js >= 18.0.0
- A proof.holdings API key for the keyed tools. The server starts and answers the keyless ones
  without it — see Configuration above.

## Links

- [proof.holdings](https://proof.holdings) — Product website
- [API Documentation](https://proof.holdings/docs/api) — REST API reference
- [Dashboard](https://proof.holdings/dashboard) — Manage your account and API keys

## License

[MIT](LICENSE)

TDQS

B3.3/5.0

Scored across 179 tools

Disambiguation2/5

Several near-identical creation flows coexist: create_verification, create_session, create_multi_channel_verification, create_verification_request, create_user_request, start_domain_verification, start_user_domain_verification, and start_add_phone all appear to start some kind of verification, and an agent must read deeply to pick the right one. Domain checking is likewise split across verify_domain, verify_domain_with_credentials, trigger_verification, check_domain_verification, and check_user_domain_verification, while listing tools (list_verifications, list_verification_requests, list_my_requests, list_incoming_requests) overlap by subject vs creator. Descriptions are rich but the boundaries between tools remain unclear at the set level.

Naming Consistency3/5

Most names are snake_case verb_noun, but the verb vocabulary is inconsistent for parallel concepts: start_login vs create_account vs create_session, start_add_phone vs create_session, add_domain vs setup_domain_email, plus bare outliers like search and test_verify. Readable, but there is no single predictable convention an agent can rely on to guess a tool name.

Tool Count1/5

179 tools is an extreme surface for a single server — far beyond what an agent can survey, let alone select from reliably. Much of the volume is granular CRUD across a dozen sub-domains (2FA, circles, HITL, delegations, templates, billing, DNS credentials), which should be separate servers or consolidated into fewer parameterized tools.

Completeness4/5

Coverage is genuinely broad: verification lifecycle, domains, phones, emails, API keys, webhooks, HITL, circles, delegations, billing, templates, login and 2FA flows all have create/read/update/delete-ish operations. Minor gaps exist (e.g. a confirmation resend tool is explicitly noted as absent, and some poll-only flows), but agents can usually work around them.

Maintenance

ActivityMaintained
ResponsivenessNo recent community issues