/** * @atrib/mcp middleware. the atrib() wrapper function (§5.3). * * Wraps an MCP server to automatically emit attribution records and * propagate context. Zero ongoing surface area: one init call, then * everything is automatic. */ import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; import type { AnchorSetConfig } from './anchors.js'; import { type LocalSubstrateCoordinatorRequest, type LocalSubstrateCoordinatorTransport, type LocalSubstrateDegradationPolicy, type LocalSubstrateProducer, type TryLocalSubstrateCoordinatorResult } from './local-substrate.js'; import type { AtribRecord } from './types.js'; import type { ArchiveSubmissionOptions, ProofBundle } from './submission.js'; import type { CapturedMcpOAuthEvidence, McpOAuthEvidenceCaptureOptions } from './oauth-evidence.js'; import type { CapturedX401Evidence, X401EvidenceCaptureOptions } from './x401-evidence.js'; import type { DelegationCertificate } from './delegation.js'; /** Context passed to a {@link PreCallTransform} callback. */ export interface PreCallTransformContext { /** MCP tool name (params.name), e.g. "post_context". */ toolName: string; /** Arguments the upstream handler will see. The host returns a replacement object to mutate. */ args: Record; /** * §1.5.2 propagation token for the record about to be emitted: * `base64url(record_hash) + "." + base64url(creator_key)`. Self-contained * reference to the (about-to-be-signed) record. Useful for embedding into * the upstream tool's own data so cross-tool consumers can derive a causal * edge back to the signed record. */ receiptId: string; /** Canonical record_hash reference: `sha256:<64-hex>`. Same value the host would receive in `informed_by`. */ recordHash: string; /** Per-call context_id (32 hex chars). Stable across calls in the same session under autoChain. */ contextId: string; } /** * Pre-call transform callback. When set on {@link AtribOptions}, atrib signs * the record BEFORE forwarding to the upstream handler so the host can embed * the resulting receipt_id (or record_hash) into the upstream call's args. * * Return contract: * - **Return a new object** to replace `request.params.arguments` for the * upstream call. The canonical pattern is `return { ...ctx.args, my_field: ctx.receiptId }`. * - Return `undefined` to leave arguments unchanged (observe-only mode). * - Mutating `ctx.args` in place AND returning the same reference is NOT * supported: the middleware uses reference equality to detect a transform, * so same-reference returns are treated as "no change." If the upstream * request had no `arguments` field at all, in-place mutations to `ctx.args` * are silently lost. Always allocate and return a fresh object. * * Errors thrown from the callback are caught; the middleware then falls back * to the standard post-call signing path so the tool call itself never fails * because of attribution (§5.8). * * Use case: cross-tool causal embedding. e.g., a wrapper around an MCP server * that writes rows to a database can use this to write the atrib receipt_id * into the row at insert time, letting downstream consumers anchor their own * `informed_by` references to the row's atrib record. */ export type PreCallTransform = (ctx: PreCallTransformContext) => Record | undefined; export type RecordReferenceSource = 'parent-env' | 'informedBy-callback' | 'auto-detect'; export interface RecordReferenceCandidate { /** Candidate record_hash, `sha256:<64-hex>`. */ recordHash: string; /** How the candidate entered the producer. */ source: RecordReferenceSource; /** MCP tool name (params.name), e.g. "post_context". */ toolName: string; /** Per-call context_id that the record being signed will use. */ contextId: string; /** Full MCP tool-call params object. */ params: Record; } export type RecordReferenceResolver = (candidate: RecordReferenceCandidate) => boolean | Promise; export interface LocalSubstrateShadowAttempt { request: LocalSubstrateCoordinatorRequest; result: TryLocalSubstrateCoordinatorResult; expectedRecordHash: string; recordHashMatches?: boolean; } export interface LocalSubstrateShadowOptions { /** Coordinator transport. Library callers can use HTTP, Unix sockets, or tests. */ transport: LocalSubstrateCoordinatorTransport; /** Producer identity written into the coordinator envelope. */ producer: LocalSubstrateProducer; /** Per-attempt timeout. Defaults to the shared client helper default. */ timeoutMs?: number; /** Fallback posture written into the coordinator envelope. */ degradation?: LocalSubstrateDegradationPolicy; /** * Optional observer for rollout telemetry. It fires after the direct path has * signed locally, and any observer error is caught per §5.8. */ onAttempt?: (attempt: LocalSubstrateShadowAttempt) => void | Promise; } export interface LocalSubstrateCommitAttempt { request: LocalSubstrateCoordinatorRequest; result: TryLocalSubstrateCoordinatorResult; expectedRecordHash: string; responseRecordHash?: string; recordHashMatches: boolean; } export interface LocalSubstrateCommitOptions { /** Coordinator transport. Library callers can use HTTP, Unix sockets, or tests. */ transport: LocalSubstrateCoordinatorTransport; /** Producer identity written into the coordinator envelope. */ producer: LocalSubstrateProducer; /** Per-attempt timeout. Defaults to the shared client helper default. */ timeoutMs?: number; /** Fallback posture written into the coordinator envelope. */ degradation?: LocalSubstrateDegradationPolicy; /** Observer for rollout telemetry. It never affects the tool response. */ onAttempt?: (attempt: LocalSubstrateCommitAttempt) => void | Promise; /** Warning hook for mismatches or observer errors. It never affects the tool response. */ onWarning?: (message: string, detail?: unknown) => void; } export interface DisclosureOptions { tool_name?: 'omit' | 'verbatim' | 'hashed'; args?: 'omit' | 'plain-sha256' | 'salted-sha256'; result?: 'omit' | 'plain-sha256' | 'salted-sha256'; } /** * Recommended action-evidence preset. It commits to tool identity, arguments, * and the pre-mutation result while keeping their raw values in the local * sidecar. Low-level middleware remains byte-compatible unless callers select * `evidenceMode: 'verifiable-action'`. */ export declare const VERIFIABLE_ACTION_DISCLOSURE: Readonly; /** * Pre-sign payload context passed to `onRecord` alongside the signed * AtribRecord. The signed record commits to this content (via content_id / * args_hash / result_hash) but does not itself contain it. Hosts can use * this sidecar to populate a richer local mirror that surfaces semantic * context (tool name, raw args, raw result) for consumers like atrib-trace * and atrib-summarize, without leaking the same content to the public log. * * All fields are optional and best-effort. The sidecar is informational * for observers; the canonical record bytes remain the AtribRecord. */ export interface OnRecordSidecar { /** Request/outcome phase when verifiable-action mode emits a paired record. */ actionPhase?: 'request' | 'outcome'; /** MCP tool name (params.name), e.g. "post_context". */ toolName?: string; /** Tool call arguments (params.arguments) at invocation time. */ args?: Record; /** Upstream tool's result object, BEFORE atrib mutated _meta with its token. */ result?: Record; /** Verifier-ready authorization evidence captured from MCP request metadata. */ authorizationEvidence?: Array; /** Local facts callers can feed into @atrib/verify capability_check. */ resolvedFacts?: { tool_name?: string; }; /** Full §1.11 certificate carried only in the local mirror envelope. */ delegation_cert?: DelegationCertificate; } /** Options for the atrib() middleware (§5.3.1). */ export interface AtribOptions { /** Base64url-encoded Ed25519 private key (32 bytes). Required. */ creatorKey?: string; /** URL of the Merkle log submission endpoint. */ logEndpoint?: string; /** * Set to 'disabled' for offline tests and local-mirror-only hosts that * should still sign records and run onRecord, but must not submit to a log. * Defaults to 'enabled'. */ logSubmission?: 'enabled' | 'disabled'; /** * Opt-in anchor plurality (D138, §2.11.12). When set, every record handed * to the log submission queue also fans out to the configured anchor set, * fire-and-forget (§5.3.5) and silent-failure (§5.8). Absent = current * single-log behavior, unchanged. */ anchors?: AnchorSetConfig; /** * Opt-in record body archive submission. When set, the middleware submits * the signed record body plus selected verifier evidence to the archive * only after the log returns an inclusion proof. Raw sidecar args/results * stay local-only. */ archiveSubmission?: ArchiveSubmissionOptions; /** * Optional §1.11 delegation certificate for this producer's run key. * The middleware carries it only in `_local.delegation_cert`; it does not * add fields to the signed record or change log submission bytes. */ delegationCert?: DelegationCertificate; /** Inline attribution policy document (§4.2). */ policy?: Record; /** Canonical URL of this MCP server for content_id derivation. */ serverUrl?: string; /** Tool names that complete commerce transactions (§5.4.5 Path 1). */ transactionTools?: string[]; /** * Observer invoked once per signed record AFTER signing and BEFORE log * submission. Lets the host persist or audit the record locally, without * this hook the original signed JSON is unrecoverable (the log stores only * commitments). Errors thrown from the observer are caught and logged; they * do not block submission or affect the tool response (§5.8). * * The optional `sidecar` argument carries pre-sign payload context that the * record's content_id / args_hash / result_hash COMMIT TO but does not * itself contain. Hosts can use it to persist a richer local mirror that * surfaces tool name + args + result alongside the signed record without * leaking that content to the public log (the public log only ever sees * the bare AtribRecord). Backward-compatible: existing observers that * ignore the second argument behave unchanged. * * Use cases: dogfood verification (replay verifyRecord against creator_key), * local audit trail, debugging "what exactly did we sign?", richer recall * surfaces (atrib-trace, atrib-summarize) that need semantic context. */ onRecord?: (record: AtribRecord, sidecar?: OnRecordSidecar) => void | Promise; /** * Opt-in producer-side MCP/OAuth evidence capture. When enabled and the * MCP transport provides `extra.authInfo`, atrib writes verifier-ready * authorization evidence into the local-only sidecar. The bearer token is * not stored; the sidecar contains verified claims, optional token hash, * optional DPoP proof material, and configured constraints for later * `@atrib/verify` checks. */ authorizationEvidence?: boolean | McpOAuthEvidenceCaptureOptions; /** * Opt-in producer-side x401 proof evidence capture. When enabled and the * host passes HTTP proof headers through `extra.requestInfo.headers`, atrib * writes verifier-ready x401 evidence into the local-only sidecar. Raw * credential payloads stay in the header values supplied by the host; archive * projections should store verifier output rather than raw inputs. */ x401AuthorizationEvidence?: boolean | X401EvidenceCaptureOptions; /** * Maximum number of records held in the in-memory submission queue while * the log is unreachable. When this cap is hit, the queue evicts the * oldest 'normal'-priority entry first (then 'high'-priority if needed). * Defaults to 10000. Forwarded to createSubmissionQueue. */ maxQueueDepth?: number; /** * Opt-in: synthesize chain context within a single middleware instance when * the calling agent does not propagate atrib's outbound `_meta.atrib` token * back into subsequent requests. Default false. * * When true: * - If a request arrives with no inbound atrib chain context AND no * `_meta.traceparent`, the middleware uses a stable process-lifetime * context_id so successive calls share a trace. * - Within each context_id, the middleware remembers the most recent * signed record's hash and uses it as `chain_root` for the next call, * forming `CHAIN_PRECEDES` edges between successive tool calls. * * Why opt-in: spec-conformant behavior is "chain reflects propagated agent * context." For agents that DO propagate, autoChain would clobber explicit * intent. For agents that don't (Claude Code, Cursor, generic stdio hosts), * autoChain turns "every call is a genesis" into "every call links to its * predecessor in this process," which is the dogfood-loop's intended * semantic, the agent observed every prior call's result before making * the next one. * * Hosts that wrap atrib over an upstream MCP server typically expose this via an env var (e.g. `ATRIB_AUTO_CHAIN`). */ autoChain?: boolean; /** * Optional per-call context resolver used when the caller did not provide * `_meta.atrib` or a valid traceparent. This lets long-lived MCP children * inherit the active host session from a harness state file instead of * minting a wrapper-owned session. * * Returning undefined falls through to the normal autoChain fallback. * Invalid values are ignored. Exceptions are caught so the tool call still * succeeds when context discovery is stale or unavailable. */ contextIdResolver?: () => string | undefined; /** * Controls what `autoChain` does when no inbound or resolved context exists. * * `stable-process` preserves the historical wrapper behavior: a process-wide * context_id is minted and reused so non-propagating hosts still get a chain. * * `fresh` keeps autoChain active for resolved contexts but refuses to create * a wrapper-wide session. Each no-context call gets its own genesis context. */ autoChainFallback?: 'stable-process' | 'fresh'; /** * Seed records used to populate the in-memory `lastRecordHashByContext` * map on startup. Without this, autoChain breaks across process restarts: * the first call after a wrapper restart becomes a fresh genesis even * though prior records exist. Pass the on-disk record mirror's most-recent * record per context_id (or just all records, the middleware will pick * the most-recent per context_id). * * Only consulted when `autoChain: true`. Records are not verified here; * the caller is expected to filter to records they trust (e.g. their own * signed-record mirror persisted by `onRecord`). */ autoChainSeed?: AtribRecord[]; /** * Optional callback invoked once per outbound tool call. Returns the list * of prior `record_hash` values (each `sha256:<64-hex>`) that this call * is informed by. The middleware injects the result into the signed * record's `informed_by` field (D041 / spec §1.2.7), letting verifiers * derive INFORMED_BY graph edges. * * Receives the MCP tool-call params object (so the host can inspect * arguments, names, etc.). Returning `undefined` or an empty array omits * the field. The host is responsible for accuracy, informed_by is a * provenance claim, not a heuristic. */ informedBy?: (params: Record) => string[] | undefined; /** * Optional source-aware validator for `informed_by` candidates. * * When supplied, refs from `informedBy` callbacks and * `autoDetectInformedByFromArgs` are kept only if the resolver returns * true. `ATRIB_PARENT_RECORD_HASH` env seeds are producer-owned spawn * anchors and bypass this lookup because the parent may have just signed * the dispatch record before mirror or public-log visibility catches up. * * Resolver errors are caught; the candidate is dropped and the wrapped tool * call still succeeds per §5.8. */ recordReferenceResolver?: RecordReferenceResolver; /** * Mechanical auto-detection of `informed_by` references from tool args. * * When `true`, the middleware extracts refs only from structured * record-reference fields such as `record_hash`, `record_hashes`, * `accepted_record_hashes`, `annotates`, and `revises`. It does not scan * arbitrary prose or commitment fields. Detected references are merged with * the explicit `informedBy` callback result, de-duped, and lex-sorted per * §1.2.5. Default `false` preserves backward compat. * * Per spec §1.2.5: informed_by is a provenance claim. Auto-detect is only a * convenience for structured record refs; hosts that know a tool-specific * path should prefer the explicit `informedBy` callback. */ autoDetectInformedByFromArgs?: boolean; /** * Optional pre-call transform. When set, atrib signs the record BEFORE * forwarding to the upstream handler so the host can embed the resulting * receipt_id into the upstream call's args. See {@link PreCallTransform}. * * When unset, signing happens AFTER the upstream returns (the default, * spec-aligned with §5.3 latency contract: attribution must not block the * tool's response shape). preCallTransform is opt-in because pre-call * signing trades a small latency penalty (one signature on the critical * path) for the ability to causally embed records into downstream data. */ preCallTransform?: PreCallTransform; /** * Named evidence posture. `verifiable-action` applies * `VERIFIABLE_ACTION_DISCLOSURE` unless `disclosure` is supplied. * `minimal` preserves the §8.1 byte-compatible omission posture. */ evidenceMode?: 'minimal' | 'verifiable-action'; /** * Optional disclosure controls per spec §8 / D061. Each dial picks how * much the signed record discloses about the underlying tool call. All * default to `'omit'`, preserving the §8.1 default posture (existing * behavior; no change to record bytes for callers that don't opt in). * * - `tool_name: 'omit' | 'verbatim' | 'hashed'` (§8.2). 'verbatim' * writes the raw tool name. 'hashed' writes `sha256:` of the * name (verifiers configured with a name registry can resolve; * others see only the hash). 'omit' adds nothing. * - `args: 'omit' | 'plain-sha256' | 'salted-sha256'` (§8.3). * 'plain-sha256' writes `args_hash = sha256(JCS(arguments))`. * 'salted-sha256' generates a 16-byte random salt, writes both * `args_salt` (the salt, base64url) and `args_hash = * sha256(salt || JCS(arguments))`. 'omit' adds nothing. * - `result: 'omit' | 'plain-sha256' | 'salted-sha256'` (§8.3). * Same scheme as `args` but for the tool's response. Hashes the * JCS canonicalization of the result object captured BEFORE * atrib mutates `result._meta` with its own fields. * * **Compatibility note**: `result` disclosure requires the post-call * signing path. It is INCOMPATIBLE with `preCallTransform` (which * signs BEFORE the handler returns). When both are set, `result` * disclosure is silently ignored and a warning is logged. */ disclosure?: DisclosureOptions; /** * Opt-in local substrate shadow probe. The middleware sends the exact * unsigned record body to a coordinator in `shadow_probe` mode, then still * signs, mirrors, and submits locally. This proves startup-spawn reachability * without moving ownership of queue or mirror side effects. */ localSubstrate?: LocalSubstrateShadowOptions; /** * Opt-in local substrate commit path. The middleware signs locally so it can * attach outbound context and persist the local sidecar, then sends the exact * unsigned record body to a coordinator in `commit` mode. If the coordinator * returns the same record_hash, this process skips its own log-submission * queue. Rejection, timeout, or hash mismatch falls back to the local queue. */ localSubstrateCommit?: LocalSubstrateCommitOptions; /** * Opt-in `dev.atrib/attribution` MCP extension receipts (D141 / spec * §1.5.4.1; extension spec docs/extensions/dev.atrib-attribution/v0.1.md). * * When true, successful tool calls whose client declared the extension on * THAT request (per-request `io.modelcontextprotocol/clientCapabilities` * in `_meta`, or a legacy `initialize`-time declaration supplied by the * host) additionally receive the gated `dev.atrib/attribution` block in * `result._meta`: the propagation token plus an attestation receipt naming * the already-signed record, with `log_submission` reported as a queue * status (`'disabled'` under `logSubmission: 'disabled'`, else `'queued'`) * — never an awaited proof (§5.3.5). * * Default false: zero behavior change. The legacy unprefixed result keys * (`atrib`, `tracestate`, `X-Atrib-Chain`) are written unconditionally * either way, and receipt emission failures degrade silently per §5.8. */ extensionAttribution?: boolean; } /** Extended McpServer with atrib-specific methods. */ export interface AtribServer extends McpServer { /** Flush pending log submissions (for testing/shutdown). */ flush(): Promise; /** The policy document this server exposes, if any (§5.3.6). */ readonly policy: Record | null; /** Retrieve a cached proof bundle by record hash (§5.3.5). */ getProof(recordHash: string): ProofBundle | undefined; /** * Zero the private key and prevent further signing (§5.6.3). * Call on graceful shutdown. After destroy(), tool calls pass through * without attribution records. */ destroy(): void; } /** * Wrap an MCP server with atrib attribution middleware (§5.3). * * If creatorKey is not provided, operates in pass-through mode: * all requests and responses forwarded without modification. */ export declare function atrib(server: McpServer, options?: AtribOptions): AtribServer; //# sourceMappingURL=middleware.d.ts.map