mcptest docs GitHub

Trust-boundary conformance

A single MCP server is one trust domain. A multi-server agent is several, and the boundaries between them are where things go wrong. When one server's tool output flows into another server's tool input, the second server implicitly trusts content the first produced. When the transport is stateless, every request re-declares the capabilities it claims, and a server can silently widen those claims past what the client agreed. This page explains the two deterministic checks that catch these cross-server failures, how to assert a boundary in a suite, and how the checks map to their taxonomy axes.

Both checks come from the analysis in Breaking the Protocol and AttestMCP (arXiv:2601.17549), which measured how the MCP architecture amplifies attack success across server boundaries and how attestation closes the gap.

Implicit trust propagation across servers

The cross-server trust-propagation check (rule id SEC-060) reasons about data, not names. It takes the cross-server flows observed during a run, where a flow is one server's recorded tool output reused as a later tool's input on a different server, and flags any flow that carries the producer output into the consumer input unmodified across a server boundary the suite did not declare safe. "Breaking the Protocol" shows this implicit trust propagation amplifies attack success 23 to 41 percent, because a tool result from server A reaches server B with no asserted boundary between them.

The detection is a black-box objective predicate, not an LLM judge: it compares the producer output and the consumer input as structural and textual values. A finding fires only when all of these hold:

Because the predicate is a structural and textual comparison of values a black-box client already captures, the check is black-box probeable: it needs only the recorded tools/call results and the next-call arguments, not any server internals.

Declaring the boundary

A suite asserts the boundary it accepts with a top-level security.trust_boundary block:

# yaml-language-server: $schema=https://mcptest.sh/schema/v1.json
servers:
  fetcher:
    command: ["mcptest", "mock", "--tools-from", "./servers/fetcher.yml"]
  database:
    command: ["mcptest", "mock", "--tools-from", "./servers/database.yml"]

security:
  trust_boundary:
    allowed_flows:
      - from: fetcher
        to: database
    required_sanitizers:
      - sanitize_text

An empty assertion makes no exception: every unmodified cross-server flow is a finding.

A flow that gets flagged

Suppose fetcher.fetch_page returns raw, attacker-controllable page text and an agent passes that text straight into database.run_query. The observed flow is fetcher to database, the consumer input carries the fetcher output verbatim, and (in this run) it did not pass through sanitize_text. With the declaration above, the allow-list clause matches the server pair, but the flow is only cleared when it routes through a required_sanitizers point. A raw flow that skips sanitize_text is not bounded, so it stays a SEC-060 finding. Routing the same text through sanitize_text before the query clears it. The runnable version of this is examples/trust-boundary/.

Capability-negotiation integrity under the stateless model

The 2026-07-28 release candidate is the stateless-transport revision: there is no long-lived session, so every request re-declares the capabilities it claims. That re-declaration is an opening. The capability-negotiation integrity check (rule id SEC-061, capability-negotiation-widening) takes the negotiated baseline (the capability set the client agreed at initialize) and the per-request claims captured during the run, and flags any request whose claimed capability set is not a subset of the baseline. AttestMCP cut attack success from 52.8 to 12.4 percent by checking exactly this: that per-request capability claims stay within the negotiated baseline and are never silently widened.

Detection is an objective set comparison, not an LLM judge: a claim is a widening exactly when it is present in a request but absent from the negotiated baseline. The check applies only under the 2026-07-28 stateless target; for any earlier target it is a no-op, since pre-stateless sessions negotiate capabilities once and do not restate them per request. It is black-box probeable: the negotiated baseline comes from the initialize response and each per-request claim comes from the request itself, both observable to a black-box client.

A suite declares the baseline with a top-level security.capability_negotiation block:

security:
  capability_negotiation:
    target: "2026-07-28"
    negotiated: [tools]

A later request that claims elicitation on top of the negotiated tools is a silent widening and fires SEC-061, naming the extra capability.

Mapping the two checks

CheckRule idTaxonomy axisBlack-box probeableSource
Cross-server trust propagationSEC-060cross-server-trust-propagationyesarXiv:2601.17549
Capability-negotiation wideningSEC-061capability-negotiation-wideningyesarXiv:2601.17549

Preview

The security: block is marked x-mcptest-status: preview in the schema. The detection engines are the deterministic, black-box functions in mcptest-core (the trust_propagation and capability_negotiation security modules, covered by crates/mcptest-core/tests/security_trust_boundary.rs). The runner wiring that captures observed cross-server flows and per-request claims during a run and applies these declarations is still landing.