Trust and safety
The exact set of changes Mend can make, what is deliberately impossible, the fail-open guarantees, and what Spanly can and cannot see or change.
This page is written for a security reviewer. It describes the exact boundary of what Mend can do to your MCP server, and every claim here is a property of the shipped code, not an aspiration. Where a guarantee comes from a specific mechanism, that mechanism is named.
The short version: Mend can only clarify or tighten what a client is told about your tools, only on the manifest version a fix was written for, only when you have allowed it, and never at the cost of blocking or delaying your traffic. Everything below is how that is enforced.
The operation allowlist
A patch is a list of typed overlay operations. There are exactly six, and the type of a patch is a discriminated union with no other members, so a parsed patch cannot instruct the wire layer to do anything outside this list:
| Operation | Effect |
|---|---|
set_description | Set a tool's description text. |
set_param_description | Set the description of one input parameter. |
add_annotation | Set one annotation hint: readOnlyHint, destructiveHint, idempotentHint, or openWorldHint. |
add_param_constraint | Set one JSON Schema keyword on a parameter: enum, pattern, minimum, maximum, or format. |
add_required | Mark an already-declared parameter as required. |
set_output_schema | Set a tool's outputSchema. |
Every operation is a plain "set", so applying the same patch twice is a no-op. Operations address tools by name and parameters by top-level name.
What is deliberately impossible
The op set has no variant for any of the following, so these cannot happen by construction:
- No renames. There is no operation that changes a tool name or a parameter name.
- No removals. There is no operation that deletes a tool, a parameter, or a schema constraint.
- No loosening. Operations only add constraints or clarify text. There
is no operation that widens an
enum, relaxes apattern, or makes a required parameter optional. - No touching
tools/call. Mend rewritestools/listresponses only. It never inspects, alters, blocks, or delays atools/callrequest or response. Server behavior is never in the loop.
Because the manifest tells the client what the server can do, and the server behind it is untouched, the worst a patch can do is describe your tools more precisely than your manifest already does.
Fail-open guarantees
Mend never blocks or delays your MCP traffic and never depends on Spanly being reachable. Any Mend error results in your original manifest being served untouched. Concretely:
- No delivery state means pass-through. Until the collector has fetched a delivery-state document, and any time it has none, manifests are served exactly as your server produced them.
- All fetches are background. The collector reads only an in-memory snapshot synchronously on the hot path. Fetching delivery state is fire-and-forget and never delays a flush or an MCP message.
- Every fetch failure keeps the last known-good state. A missing
endpoint, a network error, a 10-second timeout, malformed JSON, a
404for a stale digest, or a digest that does not match the document it addresses all leave the current state in place. A digest that just failed is not retried for a cool-down window, so a broken digest cannot turn every flush into a failing request. - The rewrite itself cannot throw. Rewriting a
tools/listresponse is synchronous local JSON work. Any unexpected shape or internal error returns the original message object unchanged. - A patch that no longer parses is simply not served. On the backend, composing the delivery-state document skips any patch whose stored operations no longer validate, rather than failing the whole document.
- A missing Pulse header is never an error. When the sync header is absent, the collector keeps its current state and re-reads the digest on a later flush.
- The Pulse header costs one cache read. On the ingest hot path, attaching the header is at most a single Redis read; a miss or error skips the header and the client keeps its state.
The Pulse page documents the sync mechanism and its fail-open rules in full.
Manifest-hash anchoring
Every patch is authored against exactly one version of your manifest,
identified by its content hash (the same toolListContentHash the scanner
already stores). When the collector is about to apply a patch, it hashes
the tool list your server actually returned and compares:
- Hash matches: the patch is applied to a private deep copy of the response.
- Hash does not match: the patch is not applied, and the response is served as your server produced it.
This is the drift-safety invariant. If you change your manifest, a patch written against the old version stops applying on the wire instantly, and server-side the affected shim is auto-suspended and reclassified on the next scan (see the Upstreaming guide). A patch can never land on a manifest version it was not written for.
Session stickiness
A session never sees the manifest change mid-conversation. The
delivery-state snapshot and the canary arm are pinned at the session's
first tools/list and reused for the session's entire lifetime,
regardless of how the environment's state moves afterward. This holds even
for a session that started before the collector finished its first fetch:
it stays pass-through for its whole life.
Exposure logging
Spanly records what was actually served, not what was assigned. Each
rewritten tools/list response carries a record of the shims applied, the
canary and arm if any, the delivery-state digest in effect, and whether
the overlay actually applied. When a patch does not apply (for example a
hash mismatch), that is recorded as such, and those sessions are excluded
from candidate metrics. Canary verdicts and gates read from this
served-truth log.
Kill switch
Every environment has a kill switch, off by default. When it is on, the delivery-state document carries no shims and no canaries, and the collector serves every manifest untouched.
- Who can flip it. The kill switch and all Mend settings are gated to your organization's admins in the Spanly app. This is your app admin, not Spanly staff.
- How fast it takes effect. Flipping the kill switch refreshes the delivery state through the same single choke point every Mend state write uses, so the digest changes at once. Every collector picks up the new digest on its next flush and fetches the empty state. The propagation bound is one Pulse cycle, which is one flush. New sessions stop receiving any overlay within that bound; sessions already in flight keep the manifest they were pinned to, by session stickiness, until they end.
Modes and defaults
Nothing changes on your wire without an action you took. Every environment starts in suggest mode, with the kill switch off and no policy. In suggest mode Mend opens faults and drafts patches and stops there: a shim or a canary only exists because a person applied a patch or launched a canary. Moving to auto-canary or autopilot is an explicit, audited choice that widens what Mend may do within a policy you set (severity ceiling, maximum concurrent canaries, which operation kinds are allowed, and gate thresholds).
The audit log
Every Mend state write flows through a single choke point that records an audit event and refreshes the delivery-state cache in the same step, so a change can never be recorded by one and forgotten by the other. That includes a shim activated or suspended, a canary started, stopped, or rolled back, the kill switch toggled, and settings changed. Each event records the actor, the action, the subject, an optional detail payload, and a timestamp, scoped to the environment. You can read it from the Mend audit view in the dashboard.
What Spanly can and cannot see or change
Can see. Spanly sits in-line on your JSON-RPC stream through the SDK or CLI you run, so it observes your MCP traffic. That is the observability product, and it is unchanged by Mend.
Can change. Only tools/list responses, only through the six
client-facing operations above, only when the manifest hash matches, and
only when your mode and your actions have allowed a shim or canary to
exist.
Cannot change. Server behavior, any tools/call, tool or parameter
names, the presence of a tool or parameter, or the looseness of a schema.
It cannot apply a patch to a manifest version it was not written for, and
it cannot serve anything while the kill switch is on.
How canaries work
Session bucketing, arms, primary metrics, the guardrail gates, and the sequential test that lets a canary conclude as early as the data allows.
SDK and CLI reference
How the collector syncs Mend delivery state, how to verify what is being served, and how to disable Mend for an environment.