npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@atrib/mcp

v0.21.0

Published

MCP server middleware for atrib's verifiable action layer. Signs and chains successful tool calls with one-line integration.

Readme

@atrib/mcp

MCP server middleware for atrib's verifiable action layer. One line of code wraps your existing MCP server and emits a signed, chain-linked record of every successful tool call. The tool's actions become independently verifiable; the agent gains a provable history; downstream teams can prove what happened when work crosses sessions, agents, or commerce. Automatic, asynchronous, zero impact on the tool's primary response.

This is the server-side half of the atrib protocol: the package tool creators install. If you're building an agent that calls MCP tools, you want @atrib/agent instead.

Install

pnpm add @atrib/mcp

Verify a local build with pnpm --filter @atrib/mcp test.

Quick start

import { atrib } from '@atrib/mcp'
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
import { z } from 'zod' // ships with @modelcontextprotocol/sdk

const server = atrib(new McpServer({ name: 'my-tool', version: '1.0.0' }), {
  creatorKey: process.env.ATRIB_PRIVATE_KEY!, // Ed25519 seed, base64url, 32 bytes
  serverUrl: 'https://my-tool.example.com', // canonical URL for content_id derivation
  logEndpoint: process.env.ATRIB_LOG_ENDPOINT, // optional in dev. Use @atrib/log-dev locally
})

// Register your tools the normal way; the wrapper is fully transparent.
server.tool('search', { q: z.string() }, async ({ q }) => {
  const results = await mySearchImplementation(q)
  return { content: [{ type: 'text', text: JSON.stringify(results) }] }
})

That's the entire integration. Every successful tools/call your server handles now emits a signed atrib record carrying the spec §1.2 record format, propagates W3C trace context to the response, and submits to your configured log endpoint asynchronously per spec §5.3.5.

What the middleware does on every tool call

Per spec §5.3, on every inbound tools/call:

  1. Reads inbound attribution context from params._meta.atrib, tracestate, and X-atrib-Chain (in priority order). If the calling agent is wrapped with @atrib/agent, the previous record's hash and creator key are extracted from this token to set the next record's chain_root.
  2. Reads session_token from baggage if present, for cross-trace session continuity.

After the tool's own handler returns successfully (isError: false), before returning the response to the caller:

  1. Constructs the attribution record with content_id derived from serverUrl + tool name (§1.2.2), chain_root from inbound context or genesis (§1.2.3), event_type from the optional transactionTools set in your options.
  2. Signs it with Ed25519 using the configured creatorKey.
  3. Computes the propagation token (sha256 of the signed record + creator public key), 87 chars max, fitting the W3C tracestate value limit.
  4. Writes the token to the response at response._meta.atrib, tracestate, and X-atrib-Chain so the calling agent can chain the next call to it.

After the response is sent (non-blocking; see invariant #4 below):

  1. Submits the signed record to the log endpoint with retry (exponential backoff, max 3 attempts, 30s window).
  2. Caches the proof bundle on success: or caches the signed record for flush() retry on failure.

Critical behaviors (degradation contract per spec §5.8)

The middleware is built around one absolute invariant: atrib failures must never affect the primary tool call or agent response. Concretely:

  • If ATRIB_PRIVATE_KEY (or creatorKey) is unset → pass-through mode with one console warning per process. Tools work normally; no records are emitted.
  • All exceptions inside the middleware are caught, logged with the atrib: prefix, and never propagated to the caller.
  • Log submission failures are silent and retried. Records that fail repeatedly are cached locally and given one final retry on flush(), drained in priority order (high before normal; see "How priority works on the wire" below).
  • If a tool handler returns isError: true, no record is emitted per §5.3.3 and no context is written to the response. Errors do not contribute to attribution chains.

Wire format (spec §2.6.1)

The submission queue POSTs each signed record as a bare attribution record to your log endpoint:

POST https://your-log.example.com/v1/entries
Content-Type: application/json
X-atrib-Priority: high

{
  "spec_version": "atrib/1.0",
  "content_id":   "sha256:...",
  "creator_key":  "...",
  "chain_root":   "sha256:...",
  "event_type":   "https://atrib.dev/v1/types/tool_call",
  "context_id":   "...",
  "timestamp":    1743850000000,
  "signature":    "..."
}

The body is the bare record per spec §2.6.1; there is no wrapper object. The example above shows the minimal required fields; records MAY also carry optional fields per spec §1.2: session_token (§1.5.5), informed_by (§1.2.5), provenance_token (§1.2.6), timestamp_granularity (§8.4), and signers array on transaction records (§1.7.6). The X-atrib-Priority header is a non-conflicting HTTP-level extension to the spec used by the dev log's admission queue and by the flush() retry ordering inside this package. See the submission.ts file header for the full rationale on the two real consumers of priority.

The expected response is a proof bundle per §2.6.2 (snake_case fields):

{
  "log_index": 4821936,
  "checkpoint": "log.atrib.dev/v1\n4821937\n...",
  "inclusion_proof": ["...", "...", "..."],
  "leaf_hash": "..."
}

API reference

atrib(server, options): AtribServer

Wraps an McpServer instance in place. The wrapper is idempotent and can be called before or after server.tool() registration (the middleware retroactively wraps a pre-existing tools/call dispatcher if needed).

server; an McpServer from @modelcontextprotocol/sdk/server/mcp.js. The package supports both server.tool() (deprecated low-level) and server.registerTool() (current high-level) registration paths.

options; AtribOptions:

| Field | Type | Required | Description | | ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ | ----------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | creatorKey | string | yes (else pass-through) | Base64url-encoded Ed25519 seed (32 bytes). If absent, the middleware enters pass-through mode with one console warning. | | serverUrl | string | recommended | Canonical URL for content_id derivation per §1.2.2. Required for stdio transports where no host header is available. | | logEndpoint | string | optional in dev | Where to POST signed records. Defaults to https://log.atrib.dev/v1/entries. Use @atrib/log-dev's submission endpoint for local development. | | logSubmission | 'enabled'\|'disabled' | optional | Set to 'disabled' for offline tests and local-mirror-only hosts that should sign records and run onRecord without POSTing to a log. Defaults to 'enabled'. | | archiveSubmission | { endpoint: string; timeoutMs?: number } | optional | Opt-in Record Body Archive submission. The middleware submits the signed record body and selected verifier evidence only after the log returns an inclusion proof. Raw sidecar args and results stay local-only. Accepts either an archive base URL such as https://archive.atrib.dev/v1 or the full .../v1/records endpoint. | | delegationCert | DelegationCertificate | optional | Carries a §1.11 delegation certificate in _local.delegation_cert for mirror consumers. The certificate stays outside the signed record and public log submission bytes. | | authorizationEvidence | boolean \| McpOAuthEvidenceCaptureOptions | optional | Opt-in producer-side MCP/OAuth evidence capture from already-validated extra.authInfo and request metadata. Evidence stays in the local sidecar unless archiveSubmission is configured. | | x401AuthorizationEvidence | boolean \| X401EvidenceCaptureOptions | optional | Opt-in producer-side x401 proof-header capture from request metadata. The host supplies verification outcomes such as resultVerified or tokenVerified; the middleware does not verify credentials or fetch result-by-reference content. | | policy | PolicyDocument | optional | Policy document to serve at /.well-known/atrib-policy.json (for HTTP transports) and embed in serverInfo (for stdio). | | transactionTools | string[] | optional | Tool names that should emit a transaction record (event_type: https://atrib.dev/v1/types/transaction) instead of a tool_call record. This middleware emits tool_call or transaction records per §1.2.4; sibling atrib producers cover observation, directory_anchor, annotation, and revision, and consumers may also mint extension URIs in their own namespaces. Defaults to a built-in heuristic for common checkout/payment tool names. | | autoChain | boolean | optional | Synthesizes chain context for hosts that do not propagate atrib's outbound _meta.atrib token. Defaults to false for raw middleware callers. | | contextIdResolver | () => string \| undefined | optional | Per-call fallback for host session discovery when inbound atrib metadata and traceparent are absent. Invalid values are ignored and callback errors degrade silently. | | autoChainFallback | 'stable-process'\|'fresh' | optional | Controls no-context autoChain behavior. stable-process keeps the historical process-wide context. fresh gives no-context calls separate genesis contexts while preserving chain tails when a context is resolved. | | preCallTransform | PreCallTransform | optional | Pre-call hook for cross-tool causal embedding. When set, the middleware signs the record BEFORE forwarding to the upstream handler and invokes the callback with { toolName, args, receiptId, recordHash, contextId }. The host returns mutated args to inject the receipt_id (or record_hash) into the upstream call. Use case: a wrapper around a database-writing tool that wants the tool to record its own atrib receipt at insert time, letting downstream consumers anchor informed_by references to the row. Errors thrown from the callback degrade silently to the standard post-call signing path (§5.8). Tools without preCallTransform set retain the default post-call signing semantics. | | disclosure | { tool_name?: 'omit'\|'verbatim'\|'hashed'; args?: 'omit'\|'plain-sha256'\|'salted-sha256'; result?: 'omit'\|'plain-sha256'\|'salted-sha256' } | optional | Opt-in disclosure dials per §8 / D061. All three default to 'omit', preserving the §8.1 default posture (existing behavior; record bytes unchanged for callers that don't opt in). tool_name: 'verbatim' writes the raw tool name; 'hashed' writes sha256:<hex> of it. args and result use the same scheme: 'plain-sha256' writes <field>_hash = sha256(JCS(payload)); 'salted-sha256' generates a 16-byte random salt per record and writes both the salt field and <field>_hash = sha256(salt ‖ JCS(payload)). The result is hashed BEFORE atrib mutates result._meta with its own propagation token, so the commitment covers exactly what the upstream handler returned. Compatibility: disclosure.result requires the post-call signing path and is INCOMPATIBLE with preCallTransform (which signs pre-call when no result is available); when both are set, result disclosure is silently inactive on the pre-call path and an init-time warning fires so the conflict is visible at config time. |

Producer-side MCP/OAuth evidence capture

Set authorizationEvidence when an MCP HTTP transport has already validated extra.authInfo and you want the local mirror to preserve verifier-ready evidence for later @atrib/verify checks:

atrib(server, {
  creatorKey: '<base64url-encoded-32-byte-seed>',
  serverUrl: 'https://mcp.example.com/mcp',
  authorizationEvidence: {
    claimSource: 'extraClaims',
    requiredScopes: ['files:read'],
    protectedResourceMetadata: {
      resource: 'https://mcp.example.com/mcp',
      authorization_servers: ['https://auth.example.com'],
    },
    includeDpopProof: true,
  },
})

The middleware stores this evidence only in the local sidecar passed to onRecord or mirror writers. It does not add OAuth claims to the signed AtribRecord, and it does not submit them to the public log. The sidecar contains verified claims from authInfo, a one-way token_hash when a bearer token is present, optional DPoP proof material, and configured constraints. It does not store the raw bearer token by default.

The same sidecar also includes resolvedFacts: { tool_name } for tool calls. Verifiers can pass those facts to verifyRecord(record, { resolvedFacts }) so capability envelopes that constrain tool_names can be evaluated from local body material instead of surfacing as unresolved.

Producer-side AAuth evidence capture

Use buildAAuthEvidenceFromEvent() when a host already receives AAuth client callbacks, server verification results, or audit-sink events and wants verifier-ready evidence in the local sidecar:

import { buildAAuthEvidenceFromEvent } from '@atrib/mcp'

const evidence = buildAAuthEvidenceFromEvent(aauthEvent, {
  tokenKind: 'auth_token',
  accessMode: 'auth-token',
  claimsVerified: true,
  expectedAgent: 'aauth:[email protected]',
  expectedActSubject: 'aauth:[email protected]',
  requiredScopes: ['files:read'],
  httpSignatureVerified: true,
})

The helper accepts event shapes with agentToken, resourceToken, authToken, jwt, claims, decodedClaims, tokenClaims, request, signedRequest, retryRequest, or headers. That covers the current TypeScript createAAuthFetch() callback shape, AAuth server middleware verification results, and Person Server audit-sink style events without importing AAuth packages into @atrib/mcp.

The returned evidence object uses protocol: 'aauth'. It stores decoded token facts, caller-supplied verification status, configured constraints, optional HTTP signature facts, and a one-way token_hash when token material is visible. It does not store the raw AAuth JWT by default.

Producer-side x401 evidence capture

Set x401AuthorizationEvidence when a host has an x401-protected request path and wants verifier-ready proof evidence in the local sidecar:

atrib(server, {
  creatorKey: '<base64url-encoded-32-byte-seed>',
  serverUrl: 'https://mcp.example.com/mcp',
  x401AuthorizationEvidence: {
    resultVerified: true,
    expectedRequestId: 'proof-template-basic-v1',
    expectedAgentId: 'did:web:agent.example',
    expectedNonce: 'nonce-from-verifier',
    agentOriginVerified: true,
    issuerTrustVerified: true,
    proofPaymentBindingVerified: true,
    allowLegacyHeaders: false,
  },
})

The middleware reads PROOF-REQUEST, PROOF-RESPONSE, and PROOF-RESULT from extra.requestInfo.headers. It also recognizes legacy PROOF-REQUIRED and PROOF-PRESENTATION so drift can be surfaced by @atrib/verify. The option copies only header values and caller-supplied verification facts into authorizationEvidence; it does not perform OpenID4VP verification, issuer trust evaluation, token exchange, nonce replay storage, or result-by-reference fetches. Keep those checks at the host or Proof verifier boundary, then pass the accepted outcome as evidence.

When the host has already checked agent-origin, issuer-trust, or proof-payment binding facts, it can pass agentOriginVerified, issuerTrustVerified, issuerTrustRootType, issuerTrustRootRef, proofPaymentBindingVerified, and proofPaymentBindingRef. The verifier exposes reference hashes in public details and fails explicit negative outcomes.

Optional archive submission

Set archiveSubmission when a producer wants public body retrieval for records whose privacy posture allows it:

atrib(server, {
  creatorKey: '<base64url-encoded-32-byte-seed>',
  serverUrl: 'https://mcp.example.com/mcp',
  logEndpoint: 'https://log.atrib.dev/v1/entries',
  archiveSubmission: { endpoint: 'https://archive.atrib.dev/v1' },
})

The archive path is best-effort and non-blocking. It runs after log submission succeeds and uses the returned proof bundle, so the archive can reject uncommitted bodies. When authorization evidence capture is enabled, only authorizationEvidence and resolvedFacts are sent with the archive submission. The middleware does not send local sidecar args or result fields to the archive.

Delegation certificate sidecar

Pass a parsed §1.11 certificate as delegationCert when the middleware signs with its certified run key:

atrib(server, {
  creatorKey: runSeed,
  serverUrl: 'https://mcp.example.com/mcp',
  delegationCert: certificate,
  onRecord: (record, sidecar) => persistMirror({ record, _local: sidecar }),
})

The middleware copies the certificate to sidecar.delegation_cert. It does not add delegation_cert_hash or any other field to the record. Mirror persistence remains best-effort under §5.8, so a sidecar write failure cannot block the tool call.

Parent-child threading

Per D115, a parent producer that has already signed the spawn record for a child should pass the same-session subagent env bundle:

import { buildSubagentProducerEnv } from '@atrib/mcp'

const childEnv = buildSubagentProducerEnv({
  contextId: parentContextId,
  parentRecordHash: parentDispatchRecordHash,
  baseEnv: process.env,
})

The helper sets ATRIB_CONTEXT_ID=<parent-context-id>, ATRIB_CHAIN_TAIL_<parent-context-id>=<latest-tail-record-hash>, and ATRIB_PARENT_RECORD_HASH=<parent-dispatch-record-hash> when the inputs are canonical. Pass chainTailRecordHash when the latest tail differs from the parent dispatch hash. Shape validation is not existence lookup: pass a parent hash the caller just signed or verified, not a temp proof label or output commitment. atrib() reads ATRIB_PARENT_RECORD_HASH at middleware initialization. If the value is valid, the first successful wrapper-signed record adds it to informed_by. Failed tool calls do not consume the seed. The seed merges with informedBy and autoDetectInformedByFromArgs, dedupes, and signs in lexicographic order. Invalid values are ignored.

Source-aware informed_by validation

Per D116, recordReferenceResolver can validate refs before they enter a signed record. The resolver sees recordHash, source, toolName, contextId, and raw params. Refs from informedBy callbacks and structured auto-detect are kept only when the resolver returns true. Resolver errors drop the candidate and do not block the tool call.

ATRIB_PARENT_RECORD_HASH seeds are different: they are producer-owned spawn anchors and bypass resolver lookup because the parent can sign a dispatch record before the child-visible mirror or public log sees it.

Node hosts can use defaultRecordReferenceResolver() to check local mirrors under ATRIB_AUTOCHAIN_SOURCE, ATRIB_MIRROR_FILE, and ATRIB_RECORDS_DIR / ~/.atrib/records, then fall back to log lookup. Pass localLookupTimeoutMs when the host needs a hard wall-clock budget for local mirror scanning. If the local scan times out and log lookup does not find the record, the resolver returns unknown so hosts can drop the ref without claiming it is absent. recordHashExistsInMirror() checks an explicit mirror file, useful when the host configured a custom recordFile.

Returns a SubmissionQueue-aware wrapper exposing:

  • flush(): drain pending submissions before shutdown (idempotent)
  • getProof(recordHash): retrieve a cached proof bundle by record hash

Local substrate coordinator contract

Per D137, @atrib/mcp exports the shared request and health-report contract for optional host-owned local coordinators:

import {
  LOCAL_SUBSTRATE_REQUEST_MODES,
  bindLocalSubstrateCoordinatorNodeServer,
  createLocalSubstrateCoordinatorHttpHandler,
  buildLocalSubstrateHealthReport,
  createHttpLocalSubstrateTransport,
  createInProcessLocalSubstrateCoordinator,
  handleLocalSubstrateCoordinatorHttpRequest,
  validateLocalSubstrateRequest,
  tryLocalSubstrateCoordinator,
  probeLocalSubstrateHealth,
  validateLocalSubstrateHealthReport,
  validateLocalSubstrateResponse,
  hashLocalSubstrateRecordBody,
} from '@atrib/mcp'

These helpers let startup-spawn MCP wrappers, long-lived agents, and watcher WAL pipelines target one boundary before any coordinator becomes a runtime dependency. The invariant is strict: the coordinator request carries an unsigned record_body, and hashLocalSubstrateRecordBody() hashes the canonical body that the existing signing path would sign. Coordinator envelopes, health metadata, WAL join fields, request mode, and fallback policy never enter the signed record bytes.

tryLocalSubstrateCoordinator() is an opt-in client shim. Callers provide the transport, so Unix sockets, launchd-owned localhost services, containers, and tests can share the same validation path without pulling a daemon into this package. It validates the request before transport, validates the response against the request operation, and classifies outcomes as accepted, rejected, invalid_request, invalid_response, or unavailable. createHttpLocalSubstrateTransport() is the explicit JSON-over-HTTP helper for hosts that choose that transport.

createLocalSubstrateCoordinatorHttpHandler() is the matching server-side Fetch handler for hosts that want a supervised local service. It serves POST /atrib/local-substrate for coordinator requests plus GET/HEAD /atrib/local-substrate and /atrib/local-substrate/health for read-only health. handleLocalSubstrateCoordinatorHttpRequest() exposes the same routing as a plain result object for Express, Fastify, Node HTTP, launchd-owned daemons, or tests. Bad JSON and invalid requests return HTTP errors before the coordinator hot path runs; valid application-level rejections still return the signed response envelope so clients can classify them as rejected.

bindLocalSubstrateCoordinatorNodeServer() is the Node HTTP binding for supervised local hosts. It binds to 127.0.0.1:8787 by default, exposes the same coordinator paths, caps request bodies at 1 MiB unless configured otherwise, and returns normal JSON errors for malformed or oversized bodies before calling the coordinator. It does not emit CORS headers by default; browser-facing hosts should set their own origin policy. This gives launchd, shell scripts, or tests a concrete host process without adding a new MCP server or package.

mode: "shadow_probe" is the default startup-spawn rollout path. It asks the coordinator to validate and sign the exact unsigned body, return the hash, and skip coordinator-owned queue or mirror side effects. The middleware still signs, mirrors, and submits locally. This proves wrapper-to-coordinator reachability and record-byte equality without double-committing a record.

The atrib() middleware accepts an opt-in localSubstrate shadow option for this path. Library callers provide any LocalSubstrateCoordinatorTransport; @atrib/mcp-wrap exposes the first JSON config path through createHttpLocalSubstrateTransport().

mode: "commit" is also available for startup-spawn callers through the opt-in localSubstrateCommit option. The middleware still signs locally so it can attach outbound context and persist the local sidecar. After a successful tool call, it sends the same unsigned body to the coordinator and skips this process's log-submission queue only when the coordinator returns the same record_hash. Rejection, timeout, invalid response, or hash mismatch falls back to the existing local queue path. flush() waits for any pending coordinator commit attempts before draining local submissions, so graceful shutdown keeps the commit-or-fallback boundary intact.

createInProcessLocalSubstrateCoordinator() is the first opt-in prototype for startup-spawn, long-lived-agent, and watcher-WAL trials. It exposes a coordinator transport without creating a daemon, signs only when the unsigned body's creator_key matches the coordinator signer, returns the real record_hash and receipt token, accepts caller-owned health counters, and leaves log submission optional for tests. Its default harness scope is still startup-spawn; watcher-WAL callers opt in through supportedHarnessClasses: ['watcher-wal'] and use operation: 'enqueue_record_and_join_receipt' so receipt join metadata stays explicit. Long-lived emit callers can opt into sign_record commit through @atrib/emit by setting ATRIB_LOCAL_SUBSTRATE_MODE=commit.

buildLocalSubstrateHealthReport() and probeLocalSubstrateHealth() build read-only rollout-gate reports for queue depth, WAL join state, active contexts, wrapper counts, and stale child counts. contexts.active is a bounded sample; contexts.active_count carries the full active-context count, and contexts.active_truncated marks clipped samples. Probe warnings are advisory. They do not change signing, submission, or primary tool-call behavior.

createAtribProxy(options): Promise<AtribProxy>

In-process surrogate McpServer that forwards every tool call to an upstream MCP server and attributes them at the proxy layer. Used by the Claude Agent SDK adapter (Case B) and any host that accepts a real McpServer instance but where the actual tools live in a third-party MCP server. See packages/integration/examples/claude-agent-sdk/case-b-third-party-mcp.ts for the full pattern.

Lower-level primitives

For advanced use cases (custom transports, manual signing, recommendation calculation), the package also exports the cryptographic and serialization primitives directly: signRecord, signTransactionRecord, signTransactionAttestation, verifyRecord, canonicalRecord, canonicalCrossAttestationInput, computeContentId, genesisChainRoot, chainRoot, encodeToken, decodeToken, base64urlEncode, base64urlDecode, sha256, hexEncode, hexDecode, plus event-type helpers (EVENT_TYPE_SHORT_NAMES, EVENT_TYPE_SHORT_TO_URI, normalizeEventType), the W3C trace-context helpers (readInboundContext, writeOutboundContext, parseTracestateAtrib, parseBaggageAtribSession, extractTraceId, mergeTracestate, mergeBaggageAtribSession), the record-reference helpers (SHA256_REF_PATTERN, extractRecordHashes, extractRecordReferenceCandidates, ATRIB_PARENT_RECORD_HASH_ENV, parentRecordHashFromEnv, defaultRecordReferenceResolver, recordHashExistsInMirror), the subagent env helpers (ATRIB_CONTEXT_ID_ENV, chainTailEnvName, buildSubagentProducerEnv), the AAuth evidence helper (buildAAuthEvidenceFromEvent), the x401 evidence helper (buildX401EvidenceFromExtra), the harness session-id discovery helpers per D083 (resolveEnvContextId, KNOWN_HARNESS_DISCOVERIES), the read-primitive instrumentation helpers per D084 (logReadPrimitiveCall, extractRecordHashesFromMcpResult), the P042 local-substrate helpers (LOCAL_SUBSTRATE_REQUEST_MODES, validateLocalSubstrateRequest, validateLocalSubstrateResponse, validateLocalSubstrateHealthReport, hashLocalSubstrateRecordBody, tryLocalSubstrateCoordinator, createHttpLocalSubstrateTransport, createLocalSubstrateCoordinatorHttpHandler, handleLocalSubstrateCoordinatorHttpRequest, createInProcessLocalSubstrateCoordinator, buildLocalSubstrateHealthReport, probeLocalSubstrateHealth), the normative content-shape extractors per D086 (extractIndexableText, per-event_type extractors and type defs, see the dedicated section below), and the submission queue itself (createSubmissionQueue).

Read-primitive instrumentation (D084 Surface 6)

logReadPrimitiveCall wraps any read-primitive MCP handler with per-call instrumentation so a host-side unified analyzer (Surface 9, an analyze-substrate.mjs script in the host integration's hook layer; not on npm) can correlate surfacing → reads → writes empirically. Each call appends one jsonl line to ~/.atrib/state/read-primitives/calls.jsonl:

import { logReadPrimitiveCall, extractRecordHashesFromMcpResult } from '@atrib/mcp'

server.registerTool(
  'my_read_tool',
  {
    /* ... */
  },
  async (args) =>
    logReadPrimitiveCall(
      'my_read_tool',
      args,
      async () => handlerImpl(args),
      extractRecordHashesFromMcpResult, // or a tighter caller-supplied extractor
    ),
)

Wire schema (stable for analyzer consumption):

{
  "invoked_at": 1779527000000,
  "session_id": "ef8150a232f140739bec66122aeeda1a",
  "primitive": "recall_my_attribution_history",
  "query_shape": ["context_id", "limit"],
  "result_count": 25,
  "elapsed_ms": 312,
  "sample_result_hashes": ["sha256:...", "sha256:..."],
  "errored": false
}
  • session_id comes from resolveEnvContextId() (32-hex; matches read-primitive responses and fires.jsonl after strip).
  • query_shape lists the input keys the caller set (truthy values only); captures shape without leaking values.
  • result_count is the total record-hash count in the response, or null when the handler errored OR the result shape is not extractable. The companion errored field distinguishes the two cases.
  • sample_result_hashes caps at 10 entries; supplied extractor controls which hashes get sampled. The default extractRecordHashesFromMcpResult deep-walks the MCP tool response for any sha256:<64-hex> reference and dedupes.
  • Silent-failure per §5.8: instrumentation never blocks the primary tool path; write errors are swallowed.
  • ATRIB_READ_PRIMITIVES_LOG env var overrides the default jsonl path (used by tests).

The three read-primitive servers (@atrib/recall family, @atrib/trace, @atrib/summarize) already wrap their handlers with this helper at version @atrib/[email protected]+. New read-primitive tools should follow the same pattern per the DOC-SYNC-TRIGGERS entry for Surface 6.

Normative content-shape contracts (D086)

Per spec §1.2, AtribRecord carries structural metadata only. The actual content body lives in the local mirror's D062 sidecar at _local.content. The shape of that content varies per event_type (observation has { what, why_noted, topics }; tool_call has { tool_name, args, result }; etc.). Before D086, each consumer that wanted to read sidecar content reimplemented per-event_type parsing. The content-shapes module codifies the shape contract once so producers and consumers round-trip via the same definition.

import {
  extractIndexableText,
  type ObservationContent,
  type AnnotationContent,
  type RevisionContent,
  type ToolCallContent,
  type TransactionContent,
  type DirectoryAnchorContent,
  type ExtractIndexableTextOptions,
  DEFAULT_FIELD_CAP,
} from '@atrib/mcp'

// Dispatch on the record's event_type URI; returns a flat string of indexable
// text suitable for tokenization (BM25, embeddings) or display synthesis.
const text = extractIndexableText(
  record.event_type, // 'https://atrib.dev/v1/types/observation' etc.
  record.sidecar.content, // typed as `unknown`; runtime shape-checking handles malformed input
  { fieldCap: 2048 }, // optional; defaults to DEFAULT_FIELD_CAP
)

Per-event_type extraction (also exported individually for callers that know the event_type at compile time):

| event_type | Indexable fields | Function | | ------------------ | -------------------------------------------------------------------------------------- | -------------------------------------------------------- | | observation | what + why_noted + intent + rationale + topics | extractObservationText(content, fieldCap) | | annotation | summary + topics | extractAnnotationText(content, fieldCap) | | revision | prior_position + new_position + reason + topics | extractRevisionText(content, fieldCap) | | tool_call | tool_name + args/input/arguments + result/output/response (JSON-stringified, capped) | extractToolCallText(content, fieldCap) | | transaction | counterparty + memo + protocol fields | extractTransactionText(content, fieldCap) | | directory_anchor | tree_root + epoch_id | extractDirectoryAnchorText(content, fieldCap) | | extension URI | generic recursive string-walk (depth ≤ 4, DEFAULT_FIELD_CAP=2048 per field) | (internal; falls out of extractIndexableText dispatch) |

The shapes are normative. Producers that emit these event_types are expected to write content matching the documented field names; consumers can rely on them. Extension URI producers SHOULD adopt one of the recognizable normative-shape field names (what, why_noted, summary, description, topics) so the generic walker picks them up naturally, OR call atrib-annotate on important records to lift them via the curator path. Per-field length caps prevent giant tool_call payloads from dominating the corpus; field-length defaults to DEFAULT_FIELD_CAP (2048 chars).

@atrib/[email protected]+ uses extractIndexableText to build its BM25 corpus; before D086 it indexed only annotation summaries. @atrib/[email protected]+ uses the same shapes in summarizeSidecar to surface revision content alongside observation what and annotation summary.

Harness discovery: env-var + file-fallback (D083 v1 + v2 + v3)

resolveEnvContextId derives a default context_id for cognitive-primitive MCP servers (@atrib/emit, @atrib/recall, @atrib/trace, @atrib/summarize) when the caller omits one. Precedence:

  1. ATRIB_CONTEXT_ID env (D078; explicit operator/harness intent).
  2. For each entry in KNOWN_HARNESS_DISCOVERIES:
    • discovery.envVar in env (D083 v1; per-session-spawn harnesses like Inspect arms).
    • discovery.fallbackFile() readable + parseable (D083 v2; startup-spawn harnesses like Claude Code).
  3. undefined (caller falls through to its own resolution chain).

The HarnessDiscovery interface:

interface HarnessDiscovery {
  envVar: string // documented env var name
  fallbackFile?: (env?: NodeJS.ProcessEnv) => string // optional state file path
  parse(value: string): string | null // env or file value → 32-hex or null
}

The file-fallback path is for harnesses that spawn MCP children at process startup, before any per-session env exists. The harness's hook layer (operator-side) writes the state file from a session-aware context; the registry entry's fallbackFile thunk returns the matching path. File-read constraints: maximum 128 bytes, trimmed whitespace, silent failure on all errors.

The included Claude Code entry uses ~/.claude/state/active-session-id-${process.ppid} (per-PPID keyed so concurrent Claude Code instances don't collide). The matching writer is a SessionStart-equivalent hook in the host's hook layer; the writer reads CLAUDE_CODE_SESSION_ID from its env (Claude Code provides it to hook subprocesses) and writes the file atomically. The per-PPID fallback is skipped when process.ppid <= 1, because launchd-owned hosts must use the profile fallback instead of treating active-session-id-1 as session evidence.

The included Codex entry reads CODEX_THREAD_ID when a child process inherits Codex's per-thread env. For long-lived local primitive hosts whose parent process is not the Codex app server, the registry also includes a profile fallback entry. If ATRIB_ACTIVE_SESSION_PROFILE or ATRIB_AGENT is a safe profile name, the reader checks ~/.claude/state/active-session-id-<profile>. The Codex hook writer prefers CODEX_THREAD_ID and maintains active-session-id-codex, so a launchd-owned atrib-primitives process with ATRIB_AGENT=codex can find the active session even when process.ppid is 1. Hook-envelope session ids are not trusted by default; ATRIB_ACTIVE_SESSION_TRUST_HOOK_SESSION_ID=1 is only for harnesses whose envelope id is known to be stable.

The profile fallback is a host-owned single-active-session route. If two simultaneous sessions share the same profile name, the latest writer wins. Callers that need per-call disambiguation must pass context_id explicitly.

Adding a new harness: add a registry entry. If the harness spawns MCP children per-session, set only envVar. If at startup, add fallbackFile and ship a corresponding writer in the harness's host integration (typically a SessionStart-equivalent hook).

Serving well-known endpoints (§5.3.5, §5.3.6)

For HTTP transports, the spec requires serving the policy document at /.well-known/atrib-policy.json and cached inclusion proofs at /.well-known/atrib-proof/{record_hash}. Two helpers make this easy.

Web-standard handler (Hono, Deno, Bun, Cloudflare Workers)

createAtribHttpHandler() returns a function that accepts a Request and returns a Response for matched routes, or null for unmatched routes.

import { atrib, createAtribHttpHandler } from '@atrib/mcp'
import { Hono } from 'hono'

const mcpServer = atrib(new McpServer({ name: 'my-tool', version: '1.0.0' }), {
  creatorKey: process.env.ATRIB_PRIVATE_KEY!,
  serverUrl: 'https://my-tool.example.com',
  policy: myPolicyDocument, // optional: your attribution policy (§4.2)
})

const app = new Hono()
const atribHandler = createAtribHttpHandler(mcpServer)

// Mount before your other routes
app.all('/.well-known/*', (c) => {
  const response = atribHandler(c.req.raw)
  return response ?? c.notFound()
})

Framework-agnostic handler (Express, Fastify, or custom)

handleAtribRequest() returns a plain { status, headers, body } object. Adapt it to your framework.

import { atrib, handleAtribRequest } from '@atrib/mcp'
import express from 'express'

const mcpServer = atrib(new McpServer({ name: 'my-tool', version: '1.0.0' }), {
  creatorKey: process.env.ATRIB_PRIVATE_KEY!,
  serverUrl: 'https://my-tool.example.com',
  policy: myPolicyDocument,
})

const app = express()

app.use((req, res, next) => {
  const result = handleAtribRequest(mcpServer, req.method, req.path)
  if (!result) return next()
  res.status(result.status).set(result.headers).send(result.body)
})

Endpoints served

| Route | Method | Behavior | | ------------------------------------- | --------- | ----------------------------------------------------------------------------------- | | GET /.well-known/atrib-policy.json | GET, HEAD | Returns policy with Cache-Control: max-age=300, or 404 if no policy configured | | GET /.well-known/atrib-proof/{hash} | GET, HEAD | Returns cached inclusion proof (content-addressed, immutable), or 404 if not cached |

Both handlers return null (or pass through) for any other path, so they compose safely with your existing routes. Non-GET/HEAD requests to matched paths return 405 with an Allow header.

For stdio transports where no HTTP server is available, the policy is embedded in the MCP serverInfo field during the initialize handshake. No HTTP handler is needed.

Local development with @atrib/log-dev

Until the production Tessera-backed log at log.atrib.dev/v1 is deployed, you can run a faithful in-memory log stub for local development:

import { startDevLog } from '@atrib/log-dev'
import { atrib } from '@atrib/mcp'

const log = await startDevLog({ port: 0 })
console.log(`dev log at ${log.url}`)

const server = atrib(myMcpServer, {
  creatorKey: process.env.ATRIB_PRIVATE_KEY!,
  serverUrl: 'https://my-tool.example.com',
  logEndpoint: log.submissionEndpoint,
})

// Subscribe to record admissions for visibility
log.onSubmit((entry) => {
  console.log('record stored:', entry.record.event_type, entry.logIndex)
})

// On shutdown:
await server.flush()
await log.close()

@atrib/log-dev implements spec §2.6.1 wire format conformance exactly; anything that flows through it would also be accepted by a real Tessera log. It uses placeholder Merkle hashes and is not for production use. See packages/log-dev/README.md for the full warning and the package's purpose.

Test coverage

The test suite covers:

  • Wire-format conformance to spec §2.6.1 + §2.6.2
  • Wycheproof Ed25519 test vectors (signing/verification)
  • Offline adversarial signing vectors for malformed records, bit-flipped signatures, truncated signatures, wrong creator keys, and JCS optional-field ordering
  • JCS canonicalization edge cases (RFC 8785)
  • Transaction cross-attestation signing and counterparty signer entries via signTransactionRecord() and signTransactionAttestation()
  • Token encoding/decoding round-trips
  • Chain integrity across multiple sequential records
  • W3C Trace Context propagation (traceparent, tracestate, baggage)
  • The setRequestHandler monkey-patch shape regression test against @modelcontextprotocol/[email protected]
  • The retroactive register-then-wrap path
  • createAtribProxy end-to-end with real upstream MCP servers
  • §5.8 degradation contract; every failure mode caught, never propagated
  • Submission queue retry, backoff, and flush() priority ordering

Run them with pnpm --filter @atrib/mcp test.

Spec references

| Spec section | What this package implements | | ----------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------- | | §1.2 | Attribution record format | | §1.3 | JCS canonicalization (RFC 8785) | | §1.4 | Ed25519 signing and verificat