@atrib/mcp-wrap
v0.3.4
Published
Generic config-driven MCP wrapper for atrib. Spawns any upstream MCP server and applies @atrib/mcp middleware so every tool call is signed and logged. Wrap any MCP via JSON config, no per-server code.
Maintainers
Readme
@atrib/mcp-wrap
Generic config-driven MCP wrapper. Spawns any upstream MCP server and applies
the @atrib/mcp middleware so every tool call becomes a signed, chain-linked
record submitted to the atrib log.
Why this exists
createAtribProxy from @atrib/mcp does the MCP plumbing: spawn an upstream,
forward tool calls, apply atrib() middleware. But every wrapper that calls
it ends up reinventing the same operational shell: key resolution (env / file
/ Keychain / 1Password), file logging, signed-record mirror persistence,
autoChain seed loading from disk, secure file permissions, per-tool gating for
the preCallTransform hook. That's hundreds of lines of boilerplate per
upstream MCP server.
@atrib/mcp-wrap lifts the operational shell into a reusable service and
exposes the upstream + per-tool behavior via a JSON config. Wrap any
upstream by writing a config; no per-server code.
Install + run
The wrapper is a workspace package; build then point an MCP host at the binary:
pnpm --filter @atrib/mcp-wrap build
node ~/repos/atrib/services/mcp-wrap/dist/main.js path/to/wrap-config.jsonOr set ATRIB_WRAP_CONFIG in the host's MCP server entry. With no argument
and no env var, the wrapper reads ~/.atrib/wrap-config.json.
Config shape
{
"name": "agent-bridge",
"agent": "claude-code",
"upstream": {
"command": "agent-bridge",
"args": [],
"env": { "AGENT_BRIDGE_URL": "...", "AGENT_BRIDGE_KEY": "..." }
},
"serverUrl": "mcp://agent-bridge.local",
"logEndpoint": "https://log.atrib.dev/v1/entries",
"autoChain": true,
"tools": {
"post_context": { "injectReceiptId": true },
"checkout": { "transactionTool": true }
}
}| Field | Required | Default | Notes |
| ----------------- | -------- | ----------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ |
| name | yes | (no default) | Logical wrapper name. Surfaced to host as McpServer name + used in default file paths. |
| agent | no | claude-code | Identity hint. Picks the atrib-creator-<agent> Keychain service before falling back to atrib-creator. |
| upstream.command| yes | (no default) | Binary to spawn for the upstream MCP server. |
| upstream.args | no | [] | Args for the upstream binary. |
| upstream.env | no | inherited | Extra env merged with the parent process env (parent wins on conflicts). |
| serverUrl | yes | (no default) | Canonical URL for content_id derivation per spec §1.2.2. Path segment for agent is appended automatically. |
| logEndpoint | no | https://log.atrib.dev/v1/entries | Submission endpoint. Override for local development against @atrib/log-dev or a local log-node. |
| autoChain | no | true | Chain successive tool calls within this wrapper's process lifetime. Required for CHAIN_PRECEDES edges from stdio hosts. |
| tools[<name>] | no | (none) | Per-tool overrides. transactionTool: true emits a transaction event_type record. injectReceiptId: true enables D057 preCallTransform. |
| logFile | no | ~/.atrib/logs/<name>-<agent>.log | Wrapper debug log (jsonl). Set to "" to disable. |
| recordFile | no | ~/.atrib/records/<name>-<agent>.jsonl | Signed-record mirror (jsonl). Set to "" to disable. |
Key resolution
The wrapper picks the signing key in this order (first hit wins):
ATRIB_PRIVATE_KEYenv var (legacy / dev path).ATRIB_KEY_FILEenv var → 0600-mode file containing the base64url seed.- macOS Keychain entry for the current user, services tried in order:
atrib-creator-<agent>(agent-scoped; matches the wrapper convention).atrib-creator(generic fallback).
- 1Password CLI (
op read) as a recovery path. SetATRIB_OP_REFERENCEto a validop://<vault>/<item>/<field>reference. Off by default; activates only when the env var is set.
If none yields a key, the wrapper exits non-zero. Operator misconfiguration should surface immediately rather than silently degrading.
What you get end-to-end
For each tool call through the wrapped MCP:
- Wrapper signs the record (Ed25519 over the JCS-canonical record).
- Optionally injects the §1.5.2 receipt token into the upstream args
(when
tools[<name>].injectReceiptId === true). - Forwards to the upstream MCP server.
- On success, persists the signed record to the local jsonl mirror (closes the chain seed → pubkey → record signature → log inclusion verification path).
- Submits to the log endpoint via the priority queue.
- autoChain bookkeeping advances so the next call links to this one.
Records are byte-identical to those signed by @atrib/agent or any other
caller of @atrib/mcp middleware. The wrapper is a transport, not a
protocol participant.
Library surface
import { wrap, parseConfig } from '@atrib/mcp-wrap'
const config = parseConfig(JSON.parse(rawJson))
const { proxy } = await wrap(config)
await proxy.server.connect(transport)Useful when you want the wrapper's plumbing but a different bootstrap (custom config source, embedded inside another long-running service, etc.).
