# 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, 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](https://github.com/OpenVTC/verifiable-trust-infrastructure/tree/main/vta-mcp) is a [Model Context Protocol](https://modelcontextprotocol.io) 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](/products/affinidi-elements/vta/concepts/roles-and-access.md).

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](/products/affinidi-elements/affinidi-messaging/didcomm-mediator.md) if you haven’t set one up.

- 
[Install Rust 1.95 or later](https://www.rust-lang.org/tools/install) on your machine (required to build vta-mcp, which isn’t published to crates.io).

- 
pnm installed and configured with a working VTA connection.

```bash
cargo install pnm-cli@0.16.4 --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.

```bash
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](/products/affinidi-elements/vta/concepts/sealed-transfer.md) for how the digest check and the sealed bundle work, or [Sign application payloads without exposing your keys](/products/affinidi-elements/vta/integration-guides/provisioning-app-signing.md) for another worked example of this same procedure.

```bash
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:

```bash
pnm bootstrap open \
    --bundle        bundle.txt \
    --expect-digest  \
    --out           credential.json
```

credential.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

```bash
pnm services list
```

Copy 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.

```bash
git clone https://github.com/OpenVTC/verifiable-trust-infrastructure.git
cd verifiable-trust-infrastructure
cargo build -p vta-mcp --release
```

The 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:

```json
{
  "mcpServers": {
    "vta": {
      "command": "/path/to/target/release/vta-mcp",
      "args": [
        "--agent-did", "",
        "--vta-did", "",
        "--mediator-did", "",
        "--confirm", "sensitive"
      ],
      "env": { "VTA_MCP_AGENT_KEY": "

" }
    }
  }
}
```

--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.
Secret hygiene

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](/products/affinidi-elements/vta/vta-management/rotate-application-credential.md) 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](https://github.com/OpenVTC/verifiable-trust-infrastructure/tree/main/vta-mcp) 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

  [Sealed transfer](/products/affinidi-elements/vta/concepts/sealed-transfer.md): the mechanism behind the credential bootstrap in Step 2.

  [Give your AI agent persistent memory](/products/affinidi-elements/vta/integration-guides/agent-memory.md): a natural pairing, run the memory tools alongside vta-mcp for the same agent.

  [Roles and access](/products/affinidi-elements/vta/concepts/roles-and-access.md): how the application role bounds what the bridge can reach.

  [Grant and revoke access](/products/affinidi-elements/vta/vta-management/acl-management.md): withdraw the bridge’s credential when you stop using it.

  [Manage the contexts on a VTA](/products/affinidi-elements/vta/vta-management/manage-contexts.md): remove the mcp-bridge context and everything provisioned into it.
