Connect an MCP host to your VTA
By the end of this guide, an MCP-speaking host such as Claude Code or Claude Desktop can call your VTA’s signing, vault, and management operations directly as tools, with no custom integration code.
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: every call still runs as an authenticated request against your VTA, and the VTA’s role, access control list (ACL), and context scope decide what actually happens. 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 every tool call to remain an authenticated, policy-checked operation 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 (required to build
vta-mcp, which isn’t published to crates.io).pnminstalled and configured with a working VTA connection.cargo install pnm-cli@0.16.4 --locked --registry crates-ioSuper-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
This is the same sealed-transfer bootstrap used to provision any application credential. See Sealed transfer for how the digest check and the sealed bundle work, or Sign application payloads without exposing your keys for another worked example of this same procedure.
pnm bootstrap request --out request.json
pnm auth-credential create \
--role application \
--contexts mcp-bridge \
--label "mcp-bridge-agent" \
--recipient request.json > bundle.txtCopy 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.jsoncredential.json now holds the three values vta-mcp needs: did, privateKeyMultibase, and vtaDid. The application role grants only what a signing/vault-reading agent needs, not 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 listCopy the DID from the DIDComm row.
Step 4: Build vta-mcp
vta-mcp ships in the same source repository as the VTA and pnm, but isn’t published as a crate, so cargo install doesn’t reach it.
git clone https://github.com/OpenVTC/verifiable-trust-infrastructure.git
cd verifiable-trust-infrastructure
cargo build -p vta-mcp --releaseThe binary is at target/release/vta-mcp.
Step 5: Configure your MCP host
Add vta-mcp to your host’s MCP server config, passing the values from Steps 2 and 3. For Claude Code, this is .mcp.json in your project (or claude mcp add); for Claude Desktop, claude_desktop_config.json. Most other hosts take the same shape:
{
"mcpServers": {
"vta": {
"command": "/path/to/target/release/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>" }
}
}
}--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.
Keep VTA_MCP_AGENT_KEY in env, not args. A command-line argument is readable by any other process on the machine; an environment variable set this way is not.
VTA_MCP_AGENT_KEY is the bridge’s private key. Never commit the MCP host config with the key inlined. Most hosts can reference a value from your secrets manager or a local file instead, which keeps the key out of what you check in.
Confirm the key is not echoed in the host’s MCP server log, which captures the server’s stderr. Rotate the bridge’s credential through Rotate an application credential if the config is ever shared or exposed.
What the bridge exposes
The bridge registers four MCP tools, rather than one per VTA operation:
| Tool | What it does |
|---|---|
vta_status | Reports the bridge’s identity, its configured transports, and its current policy. |
vta_list_operations | Returns the catalogue of operations this bridge can reach. This is where the slugs for --allow and --deny come from. |
vta_supported_tasks | Asks the VTA which tasks it serves. |
vta_call | Runs one named operation. Every VTA operation is reached through this tool. |
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
| Flag | Effect |
|---|---|
--read-only | Refuses 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. sensitive is tighter than the destructive default. |
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
Restart your MCP host so it picks up the new server, then ask it to call vta_status. Expect a response naming the bridge’s identity, its configured transports, and its current policy, confirming the connection to your VTA is live.
Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
The host lists no tools for vta | The server exited before speaking MCP, usually a bad --confirm value or an unopenable path | Read the host’s own MCP server log for the exact startup error. |
| Every tool returns “not connected to its VTA” | vta-mcp couldn’t authenticate with the flags given, so it’s serving MCP in degraded mode rather than exiting silently | The 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 it | Add the slug, or drop --allow if you don’t need it. |
vault_release fails with UnsupportedTransport | It opens a sealed bundle using this client’s own keys, which requires DIDComm | Confirm you’re using the did:key agent mode from this guide, not a REST/token mode. |
| “Not authenticated” even though the credential looks right | --mediator-did is wrong or points at a different VTA | Re-run pnm services list against the same VTA and copy the DIDComm row again. |
Next steps
Glad to hear it! Please tell us how we can improve more.
Sorry to hear that. Please tell us how we can improve.
Thank you for sharing your feedback so we can improve your experience.