Specification · Markdown source · Edit on GitHub · CC0 1.0

VRP Composition Profile — MCP (Model Context Protocol)

Profile version: 1.0 Layer: transport Status: Reference profile for the VRP receipt envelope v1 (see ADR 0007). Reference implementation: lib/vrp-mcp-profile.ts; verified by lib/vrp-receipt.ts. Sibling: node-tool-binding-v0.1.md binds the tool name get_verified_stay_offer and its argument keys across a node's MCP, A2A and WebMCP surfaces.

Purpose

This profile defines how an MCP interaction becomes a transport attestation in a VRP receipt (spec/vrp-receipt.v1.schema.json). It is the first composition profile because the node is already MCP-native (api/mcp.ts), so it is the fastest credible proof that the envelope composes real layers.

The core rule: sign the assertion, not the transport (ADR 0007 D6)

MCP has no native message signing, and the deployed MCP endpoint (api/mcp.ts) is a stateless per-request transport. A tool call is therefore not a signed artifact and MUST NOT be treated as one.

Instead, the node signs an assertion about the interaction:

"This verified offer was served in response to MCP tool call <tool> with arguments <arguments_sha256> at <served_at>, by <issuer>."

The subject of the attestation is the node's assertion about the interaction, not the (unsigned) tool call itself. The node signs the assertion with the same Ed25519 key it uses for VRP signed offers (did:web / JWKS over .well-known), so the transport layer and the offer layer share one key and one verifier path.

Assertion object

The signed payload (buildMcpTransportAssertion) is:

{
  "vrp_mcp_profile_version": "1.0",
  "type": "vrp.mcp.transport.1",
  "tool": "get_verified_stay_offer",
  "arguments_sha256": "<hex sha-256 of canonical arguments>",
  "served_at": "2026-06-24T11:40:00Z",
  "issuer": "villaakerlyckan.se",
  "offer_ref": "offer:villaakerlyckan.se:2026-09-02/05",   // optional: binds to the offer attestation
  "session_id": "mcp-sess-abc"                              // optional
}

As a receipt attestation

The node signs the assertion as a compact JWS (alg: EdDSA) and wraps it (mcpTransportAttestation) as a flat attestations[] entry:

{
  "layer": "transport",
  "source": "https://villaakerlyckan.se/.well-known/jwks.json",
  "signature": "<compact JWS over the assertion>",
  "ref": "offer:villaakerlyckan.se:2026-09-02/05",
  "valid_from": "2026-06-24T11:40:00Z",
  "valid_until": "2026-06-24T12:40:00Z"
}

A verifier checks it with verifyReceipt exactly like the offer attestation: signature over the JWS bytes (D5), mandatory freshness window (D2), per-layer status with partial verification (D4). A transport layer that fails to verify never silently passes — it surfaces as unverifiable / invalid, distinct from verified.

Binding the assertion to an observed call

assertionMatchesToolCall(assertion, { tool, arguments }) lets a verifier confirm a signature-verified transport assertion actually describes the call the agent claims it made (same tool, same canonical arguments_sha256). Signature validity is the receipt verifier's job; this binds the verified assertion to the claimed interaction.

Security considerations

Relationship to ADR 0007

Implements D6 (MCP composition profile) on top of the v1 envelope (D1 flat attestations, D2 freshness, D3 optional tlog, D4 partial verification + error registry, D5 verify JWS bytes). It is the first per-layer composition profile.

Argument key casing — NORMATIVE

The keys inside the tool-call arguments object that feed arguments_sha256 MUST be snake_case: check_in, check_out, guests. This matches the VRP core endpoint parameters (v0.1.md §4), so the transport assertion hashes the same canonical key names the node already serves.

This is deliberately distinct from the camelCase query parameters used in the human/agent booking URL (v0.1.md §5.1: checkIn, checkOut) — those are a URL convention, not the hashed assertion input. An observer that captured a tool call using camelCase keys MUST normalize them to snake_case (checkIn→check_in, checkOut→check_out) before computing hashArguments(...), or assertionMatchesToolCall() will not bind. Canonical JSON (recursive key sort) does not change casing — normalization is the caller's responsibility.

The section above is the VRP-spec normative pin for argument-key casing; implementations (the HemmaBo reference implementation included) conform to it.