Skip to content
mcp-data-platform composable mcp data platform
v1.x part of txn2 ↗

Provenance

An asset saved to the portal says which calls produced it. Provenance is what turns a dashboard into a claim someone else can check: the queries behind it, the API responses it drew on, the reason each call was made, and whether each one succeeded.

What is recorded

Every asset write — save_asset, a content update or patch through manage_asset, trino_export, api_export — takes a capture: the calls that write was built from. An asset accumulates one capture per write, in order, so its provenance reads as the history of what fed each of its versions.

A capture holds two things:

  • References. The audit event ids of the captured calls. The audit log is the platform's full record of a call — arguments, identity, persona, connection, timing — and the ids are how an asset points at it.
  • A snapshot. The same calls as they stood at write time: kind, tool, connection, statement or request line, stated purpose, outcome, duration, and timestamp. Audit rows are retained for a fixed window (90 days by default) and assets are not, so the snapshot is what keeps an old asset able to answer the question after its audit rows have aged out.

Each captured call carries a kind, because assets are built from queries and from API invocations alike:

Kind What it is What the call carries
sql A statement run against a query engine statement, connection
api An HTTP invocation through the API gateway request, method, path, operation_id, connection
tool Any other data-access call (catalog lookups, object reads, upstream MCP tools) summary — what the call addressed

The request on an api call is what it asked for: the path it addressed with the values it passed substituted in, the query string it sent, and its request body on the line below. A call addressed by operation_id carries no path of its own — the template the values went into is in the connection's API catalog — so the capture resolves it there and records the path the call took. When the catalog no longer resolves the operation, the request records the operation and the values it was given, which still tells two calls to it apart. It is bounded: a large body is cut and marked, and the audit log keeps the full arguments for as long as the row is retained.

A failed call is captured with outcome: "error" and its error message. A query that failed is part of how an answer was reached, and hiding it would make the record of the work untrue.

The purpose on each call is the one sentence the caller stated for it (see Audit Logging). It is why an asset can say not only what ran but what it was for.

How the calls are chosen

By default, a capture holds every data-access call the session made since its previous capture in that session. Saving a second asset in the same session therefore records the calls made since the first save, not the whole session again. The boundaries are the writes themselves: save_asset, any *_export tool, and a manage_asset content edit.

That default window is the record of what the session did. It is deliberately wide, and being in it is not a claim that a given call produced the asset: a session that read a notification history and looked up a user before saving had both captured, and neither answered the question the asset answers. What distinguishes the two is naming, described below, and the call catalog derives a call's outcome from naming rather than from the window.

Calls the platform serves for its own bookkeeping are never an asset's source: saving an asset, managing memory, searching the catalog. What counts is the toolkit a call was routed to — the query engines, the API gateway, the catalog, object storage, and upstream MCP gateways.

The sources are resolved by reading the audit log, not by accumulating state in the serving process. That is what makes a capture correct in a multi-replica deployment: the calls a session made against one replica are recorded by an asset saved through another.

Naming the sources exactly

An agent that knows which calls produced the content can say so, and should: naming is the only evidence the platform has that a call answered anything. Every data call's result carries its own identifier:

{"call_reference": {"call_id": "8kQ2f1uVQ2S1p0aT4Hn2Zw", "reference": "mcp:call:8kQ2f1uVQ2S1p0aT4Hn2Zw"}}

Passing those ids (bare, or in mcp:call:<id> form) as sources on save_asset or on a manage_asset content edit replaces the default window with exactly the calls named:

{
  "name": "Q4 revenue by region",
  "content": "...",
  "content_type": "text/html",
  "sources": ["mcp:call:8kQ2f1uVQ2S1p0aT4Hn2Zw"]
}

A cited id only resolves among the caller's own calls. One person's query can never be recorded as another person's provenance, and an id that names nothing the caller ran is dropped, with the capture reporting that it holds fewer calls than were asked for.

The same id names the call in the call catalog, where the call is a record in its own right: what it was for, what it addressed, and what came of it. An asset naming a call is what makes that record read as satisfied; a call the default window swept up reads ran, and the asset is not listed as an artifact of it. memory_capture takes the same ids in its own sources for the answer that never became an asset.

Two things name a call, and only these two:

  • A caller's sources argument on save_asset, a manage_asset content edit, or memory_capture. The whole capture is marked cited, and the portal shows a Cited badge on it.
  • A capturing call's own record of itself. trino_export and api_export stream the result of a statement into the asset, so that statement is not a call that happened to be in scope — it is the content. The export names it without being asked to, and the portal marks that one call Source inside a capture that also holds the window around it.

Reading it back

The portal's asset page groups provenance by capture: which version each capture produced, whether the agent named the sources itself, and one card per call showing its kind, tool, connection, stated purpose, duration, and whether it failed. A call named as a source inside a capture the caller did not name wholesale is badged Source, which is how an export's own statement is told apart from the session's work around it.

Provenance panel on the asset viewerProvenance panel on the asset viewer

The newest capture is shown expanded. Every earlier one sits behind a disclosure that says how many there are, and each opens on its own. An asset a scheduled script refreshes gets a capture per run, so the panel would otherwise be as long as the asset's whole history. The disclosure counts every capture the asset holds, including the ones the page has not read yet: opening it shows the ones it has and offers a control that loads the next twenty.

The earlier captures, with one of them openedThe earlier captures, with one of them opened

Opening a call shows the full statement or request, its outcome, and its mcp:call: reference, both copyable.

A captured call, with its stated purpose, its failure, and its call referenceA captured call, with its stated purpose, its failure, and its call reference

The panel also links to the session the calls belong to, which holds everything that session did — before and after the write.

What each read carries

A capture is appended on every write and nothing removes one while the version it describes is kept, so an asset refreshed on a schedule accumulates them without bound. Reads are shaped around that.

A listing carries a summary, never the captures. manage_asset action=list and action=search, GET /api/v1/portal/assets, GET /api/v1/admin/assets, GET /api/v1/portal/assets/search and GET /api/v1/portal/shared-with-me return provenance_summary on each row and no provenance:

Field What it says
captures How many captures the asset holds
calls How many calls those captures record between them
first_captured_at, last_captured_at When the first and last were taken
last_tool The tool that took the newest capture
last_session_id The session it was taken in

A single asset read carries the newest 20 captures. manage_asset action=get, GET /api/v1/portal/assets/{id} and GET /api/v1/admin/assets/{id} return provenance.captures holding the newest twenty in the order they were written, alongside provenance.captures_total saying how many the asset holds. An asset with twenty or fewer reads exactly as it always has, with no captures_total.

The rest are read a page at a time, newest first. manage_asset action=provenance with asset_id, offset and limit (default 20, maximum 100), and the routes GET /api/v1/portal/assets/{id}/provenance and GET /api/v1/admin/assets/{id}/provenance with the same two query parameters. A page carries captures, total, offset and limit; offset counts back from the newest capture, so offset=20 is the page after the one a get carries. The page is authorized exactly as the asset is.

What is kept

A capture belongs to the version it produced. When version retention removes a version, the same transaction removes its capture, so list_versions and captures_total agree on what the asset still says about itself. Two captures are kept whatever the cap: the one that produced version 1, which is where the asset came from, and any capture taken before the platform recorded which version it produced.

An asset keeping every version (max_versions: 0) keeps every capture. Nothing prunes there, and the bounded reads above are what keep such an asset readable.

On startup a deployment trims the captures that outlived their versions on assets written before this rule existed, logging what it removed per asset. It is a pass rather than a migration because the cap it applies depends on the deployment's portal.max_versions.

Limits

  • A capture records at most 100 calls, and the default window looks back at most 500 calls for the previous capture. A capture that hit either bound sets truncated, and the portal says so rather than presenting a partial list as complete.
  • Capture depends on the audit log. With audit.enabled: false, with no database, or when a host application supplies its own audit logger (platform.WithAuditLogger, which the platform writes to but cannot read back), an asset records its owner and session but no calls, and data calls are handed no call_reference.
  • With audit.log_parameters: false or a redacted argument (audit.redact_keys), a captured call still records its tool, connection, purpose, outcome, and timing — but not the statement or request, which are argument values the audit policy withheld. A redacted value renders as the redaction rather than as what it was.
  • Assets written before provenance was recorded by reference carry a flat list of tool calls with their raw arguments. The portal renders both shapes; the older one is never written any more.