Connect an MCP host to your VTA

Run vta-mcp so an MCP-speaking host like Claude Code or Claude Desktop can sign, read the vault, and manage a VTA directly as tools, with no custom integration code.

By the end of this guide, your MCP host lists the VTA’s tools, and a vta_status call returns the bridge’s own identity and policy.

vta-mcp is a Model Context Protocol server that puts a VTA’s operations within reach of an MCP host. It is a bridge, not an authority: VTA operations run as authenticated requests against your VTA, and the VTA’s role, access control list (ACL), and context scope decide what actually happens. Some tools run inside the bridge without a VTA request: vta_status and vta_list_operations report the bridge’s own state and catalogue, issue_vp signs with a holder key configured on the bridge, and resolve_did uses the bridge’s own DID resolver. For how that authorisation is bounded, see Roles and access.

Connecting an agent framework to a signing or secrets backend otherwise means writing and maintaining bespoke client code for each project. Running the agent under your own operator credential avoids that work, at the cost of handing a tool-calling model the full reach of your admin session. The bridge adds its own local policy on top (--read-only, --allow, --deny, --confirm), because an MCP host approves a tool, not an individual call.

Use this guide when:

  • You want an AI coding assistant or agent framework to sign, read the vault, or manage a VTA without you writing an integration.
  • You want that access scoped to a dedicated identity, not your own operator session.
  • You want the agent’s VTA operations to remain authenticated, policy-checked requests against the VTA, not a bypass around it.

Prerequisites

  • A running VTA connected to a DIDComm mediator, the relay service that carries DIDComm traffic between the bridge and the VTA. See DIDComm mediator deployment options if you haven’t set one up.

  • Install Rust 1.95 or later on your machine, to compile pnm and vta-mcp.

  • jq, if you register the bridge with Claude Code, to read values from the credential file in Step 5.

  • pnm installed and configured with a working VTA connection.

    cargo install pnm-cli@0.23.1 --locked --registry crates-io
  • Super-admin access to the VTA (required for pnm contexts create).

  • An MCP-speaking host: Claude Code, Claude Desktop, or similar.

Step 1: Create a context for the bridge

Run the bridge as its own identity, scoped to its own context, not as yourself. This is the isolation boundary: whatever the bridge can reach is exactly what this context’s ACL grants it.

pnm contexts create \
    --id    mcp-bridge \
    --name  "MCP bridge"

Step 2: Provision the bridge’s agent identity

The three commands below deliver the new credential through sealed transfer, with the work split between two roles. One person on one machine can play both:

  • Recipient: the machine where the credential will be used. pnm bootstrap request creates a one-time key pair, keeps the private key in PNM’s local configuration directory, and writes only the public key, a nonce, and an optional label to the request file. The recipient later runs pnm bootstrap open on the same machine, since only that private key can open the bundle.
  • Producer: an administrator whose PNM is connected to the VTA. pnm auth-credential create creates the new DID, adds it to the context’s access control list (ACL), seals its credential to the public key in the request file, and prints a SHA-256 digest of the sealed bundle.

The digest proves that the bundle the recipient opens is the one the producer sealed, because pnm bootstrap open --expect-digest refuses any other bundle. When the roles run on separate machines, move the request file to the producer over a channel you trust, since the producer seals the credential to whichever public key that file carries. The bundle can travel back over any channel, provided the digest reaches the recipient separately over a channel you trust, for example read out over a call.

Here the recipient is the machine that will run the MCP host and the bridge.

pnm bootstrap request --out request.json

pnm auth-credential create \
    --role      application \
    --contexts  mcp-bridge \
    --label     "mcp-bridge-agent" \
    --recipient request.json > bundle.txt

Copy the SHA-256 digest printed to stderr, then open the bundle:

pnm bootstrap open \
    --bundle        bundle.txt \
    --expect-digest <digest-from-previous-command> \
    --out           credential.json

credential.json now holds the three values vta-mcp needs: did, privateKeyMultibase, and vtaDid. The application role covers signing, vault release, and memory in its contexts, without context or ACL management. Delete bundle.txt and request.json once you’ve read credential.json, they’re single-use.

Step 3: Get your mediator DID

pnm services list

Copy the DID from the Mediator: line under DIDComm:.

Step 4: Install vta-mcp

vta-mcp is distributed as source from the same repository as the VTA and pnm, while pnm itself is published on crates.io. Install it straight from that repository, pinned to the same release as the pnm version above so the two match:

cargo install --git https://github.com/OpenVTC/verifiable-trust-infrastructure \
    --tag pnm-cli-v0.23.1 --locked vta-mcp

Cargo fetches and compiles the bridge for you, which takes a few minutes, then installs vta-mcp next to pnm in ~/.cargo/bin. Confirm it is on your PATH:

command -v vta-mcp

Step 5: Register vta-mcp with your MCP host

Register the bridge under the name vta, passing the values from Steps 2 and 3:

  • Claude Code: run claude mcp add from the directory holding credential.json. It reads each value from the file with jq, so the private key stays out of your shell history. --scope user makes the bridge available in every project. Leave out --scope to register it for the current project only.
  • Claude Desktop: add the JSON block to claude_desktop_config.json, found in ~/Library/Application Support/Claude/ on macOS or %APPDATA%\Claude\ on Windows, then restart Claude Desktop.
  • Other MCP hosts: most take the same JSON block in their own MCP server config.
claude mcp add --scope user \
    --env VTA_MCP_AGENT_KEY="$(jq -r .privateKeyMultibase credential.json)" \
    vta -- "$(command -v vta-mcp)" \
    --agent-did    "$(jq -r .did credential.json)" \
    --vta-did      "$(jq -r .vtaDid credential.json)" \
    --mediator-did <mediator-did-from-step-3> \
    --confirm      sensitive
{
  "mcpServers": {
    "vta": {
      "command": "/path/to/.cargo/bin/vta-mcp",
      "args": [
        "--agent-did", "<did-from-credential.json>",
        "--vta-did", "<vtaDid-from-credential.json>",
        "--mediator-did", "<mediator-did-from-step-3>",
        "--confirm", "sensitive"
      ],
      "env": { "VTA_MCP_AGENT_KEY": "<privateKeyMultibase-from-credential.json>" }
    }
  }
}

Use the full path to the binary in command, which command -v vta-mcp prints, since a desktop host may start without your shell’s PATH.

If you keep the server in a project’s .mcp.json file instead, use the same JSON block with the key replaced by "VTA_MCP_AGENT_KEY": "${VTA_MCP_AGENT_KEY}". Claude Code expands the variable from your environment when it starts the server, so the key stays out of the file. Claude Code starts a server from .mcp.json only after you approve it, so approve vta when Claude Code prompts at the start of a session, or later from /mcp.

--confirm sensitive asks a human, through the host’s own approval prompt, before any operation that signs, releases a secret, or moves authority. That is tighter than the bridge’s destructive-only default.

What the bridge exposes

The bridge registers twelve MCP tools. Two of them reach every VTA operation, and the rest are shortcuts for the most common ones:

ToolWhat it does
vta_callRuns any VTA operation by name, with a JSON payload. Every VTA operation is reachable through this tool.
vta_list_operationsReturns the catalogue of operations vta_call can reach. This is where the slugs for --allow and --deny come from.
vta_statusReports the bridge’s identity, its configured transports, and its current policy.
vta_supported_tasksAsks the VTA which tasks it serves.
list_keysLists the signing keys available on the VTA.
signSigns text with a VTA-held key, which stays on the VTA.
vault_listLists vault entry metadata, without secret material.
vault_getFetches one vault entry’s metadata by ID, without secret material.
vault_releaseReleases a vault secret sealed to the bridge and returns its value.
resolve_didResolves a DID to its DID document.
issue_vpIssues a holder-bound Verifiable Presentation, as an OpenID for Verifiable Presentations (OID4VP) vp_token.
device_heartbeatChecks the bridge in with the VTA and returns anything waiting for it.

Because vta_call is a single tool that names its operation per call, an MCP host approving “the vta_call tool” has approved the whole reachable surface at once. That is what the bridge’s own policy flags exist to narrow.

Narrow what the bridge can reach

FlagEffect
--read-onlyRefuses every operation that is not read-only, whatever the ACL permits.
--deny <glob>Refuses matching operation slugs. Checked first, and wins over --allow.
--allow <glob>Once set, anything not matching is denied.
--confirm <level>Asks a human through the host’s approval prompt before an operation runs. Levels are never, destructive (the default), sensitive, which also covers signing, secret release, and ACL changes, and always.

Both filters match operation slugs as globs. Run vta_list_operations first to see the slugs this bridge actually reaches, rather than guessing at names.

These filters are local to the bridge and sit on top of the VTA’s own authorisation. They can only narrow what the bridge’s identity could already do; they never widen it. See the vta-mcp README for the full flag reference and every supported authentication mode.

Confirm

Test 1: the host lists the vta server as connected

In Claude Code, run:

claude mcp list

The vta entry should show Connected. Inside a Claude Code session, /mcp shows the same status along with the bridge’s tools. In Claude Desktop or another host, restart the host and check its MCP server list for vta.

Test 2: a vta_status call reports the bridge as connected

Ask the host to call vta_status. Expect a JSON response with "connected": true, along with the bridge’s identity, its transports, and its current policy. "connected": false means the bridge is running in degraded mode, and its connectError field says why.

Troubleshooting

SymptomLikely causeFix
claude mcp list shows vta as Pending approvalThe server is defined in a project’s .mcp.json, and Claude Code starts those only once approvedStart claude in that project and approve vta at the prompt, or approve it from /mcp.
The host lists no tools for vtaThe server exited before speaking MCP, usually a bad --confirm value or an unopenable pathRead the host’s own MCP server log for the exact startup error.
Every tool except vta_status returns this vta-mcp bridge is not connected to its VTA: …vta-mcp couldn’t connect with the flags given, so it’s serving MCP in degraded mode rather than exiting silentlyThe error names the missing value. Re-check --agent-did, --vta-did, --mediator-did, and VTA_MCP_AGENT_KEY.
A tool call fails with '...' is not in this bridge's --allow list--allow is set and the operation’s slug isn’t in itAdd the slug, or drop --allow if you don’t need it.
vault_release fails with unsupported transport: opening a sealed vault secret requires the DIDComm transportThe released secret is encrypted with DIDComm authcrypt, and opening it needs a DIDComm connectionConfirm you’re using the did:key agent mode from this guide, not a REST/token mode.
Tools return not connected to its VTA: connecting to VTA (did:key-didcomm): … even though the credential looks right--mediator-did is wrong or points at a different VTARe-run pnm services list against the same VTA and copy the DID from the Mediator: line under DIDComm: again.

Next steps

  Sealed transfer: the mechanism behind the credential bootstrap in Step 2.

  Give your AI agent persistent memory: a natural pairing, run the memory tools alongside vta-mcp for the same agent.

  Roles and access: how the application role bounds what the bridge can reach.

  Grant and revoke access: withdraw the bridge’s credential when you stop using it.

  Manage the contexts on a VTA: remove the mcp-bridge context and everything provisioned into it.