Version: 0.1
Status: Normative
Package: @peac/mappings-mcp
MCP Spec Version: 2025-11-25
Depends on: Evidence Carrier Contract (DD-124)
This document specifies how PEAC evidence carriers are placed within MCP (Model Context Protocol) tool responses using the _meta field. It covers key naming, reserved key guards, legacy compatibility, and security considerations.
The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in BCP 14 (RFC 2119, RFC 8174) when, and only when, they appear in all capitals, as shown here.
MCP (Anthropic) provides a protocol for connecting AI models to external tools and data sources. PEAC provides an evidence layer that MCP servers can use to attach payment receipts and traceability data to tool responses.
Evidence is carried in the _meta field of MCP tool results using reverse-DNS-prefixed keys, following MCP 2025-11-25 conventions.
PEAC evidence uses the org.peacprotocol/ prefix. The following keys are defined:
| Key | Type | Required | Description |
|---|---|---|---|
org.peacprotocol/receipt_ref |
string |
MUST | Content-addressed reference (sha256:*) |
org.peacprotocol/receipt_jws |
string |
MAY | Compact JWS of the PEAC receipt |
org.peacprotocol/policy_binding |
string |
MAY | Policy binding reference |
org.peacprotocol/actor_binding |
string |
MAY | Actor binding reference |
org.peacprotocol/request_nonce |
string |
MAY | Request nonce for replay protection |
These keys carry server metadata and are not part of the carrier structure:
| Key | Type | Description |
|---|---|---|
org.peacprotocol/agent_id |
string |
Agent identifier (from v0.10.13) |
org.peacprotocol/verified_at |
string |
ISO 8601 verification timestamp |
{
"content": [
{
"type": "text",
"text": "Search results for: quantum computing"
}
],
"_meta": {
"org.peacprotocol/receipt_ref": "sha256:abc123def456...",
"org.peacprotocol/receipt_jws": "eyJhbGciOiJFZERTQSJ9.eyJpc3MiOi...",
"org.peacprotocol/agent_id": "https://api.example.com",
"org.peacprotocol/verified_at": "2026-02-23T12:00:00Z"
}
}Per MCP spec 2025-11-25, certain _meta key prefixes are reserved:
"Any prefix where the second label is
modelcontextprotocolormcpis reserved."
The "second label" is the second segment in the dot-separated prefix (before the /):
| Key | Second Label | Reserved? |
|---|---|---|
io.modelcontextprotocol/data |
modelcontextprotocol |
Yes |
dev.mcp/data |
mcp |
Yes |
com.example.mcp/data |
example |
No |
org.peacprotocol/receipt_ref |
peacprotocol |
No |
All PEAC keys use the org.peacprotocol/ prefix, where the second label is peacprotocol. This is NOT reserved per the MCP rule.
Implementations MUST validate that dynamically constructed keys do not use reserved prefixes. The assertNotMcpReservedKey() guard function enforces this:
const MCP_RESERVED_SECOND_LABELS = ['modelcontextprotocol', 'mcp'];
function assertNotMcpReservedKey(key: string): void {
const slashIndex = key.indexOf('/');
if (slashIndex === -1) return;
const prefix = key.substring(0, slashIndex);
const labels = prefix.split('.');
if (labels.length >= 2 && MCP_RESERVED_SECOND_LABELS.includes(labels[1].toLowerCase())) {
throw new Error(`Reserved MCP _meta key: ${key}`);
}
}The original peac_receipt field (pre-v0.11.1) is deprecated in three phases:
| Phase | Version | Behavior |
|---|---|---|
| Phase 1 | v0.11.1 | extractReceipt() reads BOTH legacy and _meta. attachReceipt() defaults to _meta; opts.legacyFormat: true for legacy. |
| Phase 2 | v0.12.x | Console warning on legacy read |
| Phase 3 | v0.13.0 | Remove legacy read |
The v0.10.13 MCP server used these _meta keys:
| Legacy Key (v0.10.13) | New Key (v0.11.1+) |
|---|---|
org.peacprotocol/receipt |
org.peacprotocol/receipt_jws |
| (not present) | org.peacprotocol/receipt_ref |
When the legacy org.peacprotocol/receipt key is found during extraction, implementations MUST:
- Treat its value as a compact JWS
- Compute
receipt_reffrom the JWS usingcomputeReceiptRef() - Return a proper
PeacEvidenceCarrierwith both fields
The org.peacprotocol/agent_id and org.peacprotocol/verified_at keys are unchanged.
attachReceipt(response, receiptJWS);
// Writes to _meta (default, v0.11.1+)
attachReceipt(response, receiptJWS, { legacyFormat: true });
// Writes to peac_receipt field (backward compat)| Transport | Max Carrier Size | Format | Rationale |
|---|---|---|---|
| MCP | 64 KB (65,536 B) | embed | JSON in memory |
Carriers exceeding 64 KB MUST be rejected at attachment time.
MCP tool responses are server-signed: the MCP server (tool provider) signs the receipt. Clients do NOT sign evidence in the response path. This constraint aligns with MCP's request-response model where the server is the authoritative evidence source.
Every extractReceipt() and extractReceiptFromMeta() call validates carrier structure before returning. Malformed keys, invalid receipt_ref format, or invalid JWS format result in null return (no throw).
The assertNotMcpReservedKey() guard MUST be applied to any dynamically constructed key before writing to _meta. Static keys (org.peacprotocol/receipt_ref, etc.) are pre-validated and do not need runtime checks.
Consumers MUST NOT interpret _meta values as executable code. All values are strings (JWS, SHA-256 hashes, URIs, timestamps). There is no eval, template expansion, or dynamic dispatch based on _meta content.
An implementation is conformant with this profile if it:
- Uses
org.peacprotocol/prefixed keys in_meta(not reserved per MCP spec) - Writes
receipt_refas asha256:<hex64>string - Writes
receipt_jwsas a compact JWS (3-part dot-separated) - Reads legacy
peac_receiptfield during Phase 1/Phase 2 - Reads legacy
org.peacprotocol/receiptkey and computesreceipt_ref - Enforces the 64 KB carrier size limit
- Validates carrier structure at extraction time
Before (v0.10.x):
{
"content": [...],
"peac_receipt": "eyJhbGciOi..."
}After (v0.11.1+):
{
"content": [...],
"_meta": {
"org.peacprotocol/receipt_ref": "sha256:abc123...",
"org.peacprotocol/receipt_jws": "eyJhbGciOi..."
}
}No changes are needed on the consumer side during Phase 1: extractReceipt() reads both formats automatically.