Kontour Surface Snapshot ViewerGitHub
Browse the docs

Agents and MCP

surface mcp serves portable trust state to AI agents over the Model Context Protocol. It is the same trust state a human reads — derived by the kernel, never summarized by a model — exposed as tools an agent can call before acting on a claim.

The shipped server has no required runtime dependencies and speaks MCP over

stdio, so any MCP-capable client (Claude Code, IDE agents, custom runtimes) can

connect without adding network services or credentials.

Protocol compatibility

One server definition serves both MCP eras:

  • MCP 2026-07-28 clients open with server/discover.
  • Existing clients continue to use the legacy initialize flow.

There is no Surface flag or configuration choice for this. The opening request

selects the compatible era automatically, and the same tools, resources, and

trust derivation run behind both. Modern cacheable responses are private with a

zero TTL because each call re-reads the configured input; legacy responses keep

their legacy wire shape.

Start the server

surface mcp                                              # input-agnostic: input supplied per call
surface mcp --input path/to/trust-bundle.json            # optional single-report default
surface mcp --input path/to/export.json --adapter my-producer

--input is optional. With it, the given file is the default input for every call. Without it, the server runs input-agnostic — it holds no default and each tool call supplies its own input (see Multiple or evolving inputs). There is deliberately no baked-in example fallback: a server with no input configured returns an honest "No trust input configured" error rather than silently deriving demo data. --adapter defaults to the native surface passthrough, matching surface report.

A typical MCP client configuration:

{
  "mcpServers": {
    "surface": {
      "command": "npx",
      "args": ["surface", "mcp", "--input", ".surface/trust-bundle.json"]
    }
  }
}

Tools

ToolArgumentsReturns
surface_summaryinput?, adapter?The human-readable report summary: claim counts by status, producer surfaces, high-impact unsupported claims, stale and disputed claims, transparency gap counts
surface_stale_claimsinput?, adapter?Claims whose verification is no longer current under their freshness policy
surface_missing_evidenceinput?, adapter?Policy-required evidence that has not been supplied
surface_get_claimclaimId, input?, adapter?One claim with evidence, events, policy, authority trace, transparency gaps, and derivation drilldown
surface_waiver_validityclaimId?, input?, adapter?Per-claim accepted-gap waiver validity verdicts (not-applicable, bare-assumed, complete-waiver, incomplete-waiver, stale-or-revoked-waiver, command-backed-waiver-rejection); with claimId, just that claim's verdict, otherwise every claim in the report
surface_policypolicyId?, claimId?, input?, adapter?Policy drilldown, or all policies with claim ids and gap counts when called without arguments

input and adapter default to the values the server was started with. Every call re-derives the report from the input file, so agents always read current trust state — the same TrustBundle produces the same answer on every call, on every machine.

Multiple or evolving inputs

--input is a single-report default, but a producer that emits many, evolving trust inputs (e.g. a workflow engine with per-task trust.bundle files) does not need to restart the server or reconfigure --input: pass the per-call input argument on any tool to point that call at a different file. This is the supported multi-input pattern. A veritas evidence-record envelope can be unwrapped per call with adapter: "veritas". A current-aware resolver that follows a moving "current" subject without per-call path threading is a deferred, tracked direction (see the MCP Trust-Input Ingestion decision).

A realistic session

An agent asked to "use the latest verified pricing data" should not have to guess what "verified" means. With the server pointed at the producer's export, the agent calls surface_stale_claims before relying on anything. Against the repo's own example input, the actual tool result is:

[
  {
    "claimId": "claim.field-attested-records.registration-status",
    "surface": "field-attested-records.public-data",
    "status": "stale",
    "impactLevel": "high",
    "claimType": "public-data-field",
    "subject": {
      "subjectType": "attested-record",
      "subjectId": "field-attested-records:denver-example-record"
    },
    "policyId": "policy.public-data-field.short-lived"
  }
]

The registration status was verified once, but its 14-day freshness window expired — so instead of acting on it, the agent follows up with surface_get_claim for the evidence trace, asks the producer to reverify, or surfaces the uncertainty to the user. No prompt engineering decides this; the policy already did.

Interactive trust panel (MCP UI)

surface_summary and surface_get_claim results also carry an embedded MCP UI resource — a ui://surface/trust-panel/… entry with MIME type text/html;profile=mcp-app. Hosts that render MCP UI show the report as an interactive <surface-trust-panel> directly in the conversation; hosts that don't simply ignore the entry and read the text content, which always comes first and is complete on its own.

The embedded document is fully self-contained: the trust panel module and the report data are inlined, no network requests are made, and theming follows the host's light/dark preference through the standard --k-* token contract. Pass --no-ui to surface mcp to omit UI resources entirely.

Two ways hosts find the panel

The panel is offered under both UI conventions, so one server renders across hosts:

  • Embedded (mcp-ui.dev): the ui:// resource is included directly in the surface_summary / surface_get_claim tool result, as above.
  • Declared (MCP Apps): surface_summary advertises the canonical nested

_meta.ui.resourceUri pointing at ui://surface/trust-panel/summary, the

server advertises the resources capability and

io.modelcontextprotocol/ui extension, and the panel is served through

resources/read. The flat _meta["ui/resourceUri"] key remains compatibility

output for older Apps hosts; new integrations should read the nested shape.

--no-ui suppresses this surface too (no resources capability and no tool

UI metadata).

Behavior contract

  • Tool results carry the same JSON shapes as the corresponding CLI commands (surface report --format summary, surface stale, surface missing, surface get, surface policy).
  • Domain failures (unknown claim id, unreadable input) return tool results with isError: true rather than protocol errors, so agents can read the message and recover.
  • Unknown tools and unknown methods return standard JSON-RPC errors.
  • The server advertises the tools capability and (unless --no-ui) the resources capability plus the io.modelcontextprotocol/ui extension for the declared trust panel. It exposes no write operations: an agent can inspect trust state through MCP, but changing claims or evidence goes through the producer or the claim authoring commands.
  • Standard output is reserved for MCP JSON-RPC. SDK and transport diagnostics

are sanitized and written to standard error so they cannot corrupt the

protocol stream.

The discipline the kernel hands the agent

The server's instructions to clients state the intended use: act on verified claims, reverify stale ones, escalate disputed ones, and treat transparency gaps as a reason to ask before acting. The agent's job is to read; the kernel's job is to derive the same verdict, deterministically, from the same inputs. See Use Cases for how this plays out per domain.