Detectors

Integrity & injection

Four detector IDs, one severity model. Deterministic rules — not an LLM — decide severity and enforcement.

Detector IDs

DetectorKindFires on
integrity.definition_driftINTEGRITY_DRIFTA tool changed against a recorded baseline
integrity.description_injectionINTEGRITY_INJECTIONA first-sighting description already reads as instruction
integrity.tool_squattingINTEGRITY_INJECTIONA tool name mixes character scripts
integrity.result_injectionINTEGRITY_INJECTIONA tool result contains model-directed instruction

Only one increments the drift metric

mcp.integrity.drift.count is incremented only by integrity.definition_drift. Everything else increments kams.finding.count. That is what keeps the critical alert narrow enough to trust.

Change classification

diff_tools() compares per-tool digests and emits typed changes. Because the description and schema digests are tracked separately, a single tool can produce two changes.

ClassBase severityDetected by
DESCRIPTION_CHANGEDHighdescription digest moved
SCHEMA_WIDENEDMediumnew properties, or required fields dropped
TOOL_ADDEDMediumname absent from baseline
SCHEMA_CHANGEDMediumboth widened and narrowed
SCHEMA_NARROWEDLowproperties removed, or new required fields
TOOL_REMOVEDLowbaseline name absent from current

On a provisional baseline every severity is reduced by one level, floored at LOW. On a pinned baseline it is not. See Tool-definition integrity.

Injection scoring

Scoring runs only on text added by a change, not on the whole description. Rewriting a typo does not resemble inserting an instruction block, and scoring the delta is what makes that distinction possible.

Independent signals are combined with noisy-OR:

python
score = 1 - Π (1 - weight_i × signal_i)

Why noisy-OR rather than a weighted sum

The signals are largely independent — invisible Unicode tells you nothing about whether the text also contains an exfiltration URL — and a single strong signal should be able to carry a verdict on its own. A sum would dilute one damning signal among several quiet ones, and would need clamping to stay in range.

Signals

  • model-directed imperatives
  • instruction blocks (<IMPORTANT>-style delimiters)
  • exfiltration shapes (URLs, "send to", file reads)
  • cross-tool references — one tool's description talking about another's behaviour
  • invisible Unicode: zero-width and bidi codepoints
  • encoded blobs
  • rewrite magnitude relative to the original

Thresholds

ThresholdDefaultEffect
THRESHOLD_HIGH0.55Critical when pinned, High when provisional
THRESHOLD_MEDIUM0.30High when pinned, Medium when provisional

Both are overridable from policy.yaml:

yaml
thresholds:
  integrity.description_delta:
    high: 0.55
    medium: 0.30

Resulting severity

BaselineScore ≥ 0.55Score ≥ 0.30Below 0.30
PinnedCRITICALHIGHHIGH (base class severity)
ProvisionalHIGHMEDIUMMEDIUM / LOW (class severity − 1)

Confidence is 0.99 for a pure digest comparison — the comparison itself is exact — and max(0.6, score) when injection signals contributed.

Tool squatting

A tool name mixing character scripts renders identically to an ASCII name while being a different string. There is no benign explanation for that in server-authored metadata, so it is a fixed HIGH at 0.9 confidence, and the finding carries the full codepoint list as evidence:

json
{ "tool_name": "sаve_note", "codepoints": ["U+0073", "U+0430", "U+0076", "…"] }

Result-side injection

Injection is not confined to descriptions — any server output reaches the model's context. Kams scans text blocks in tools/call results with the same scorer, at the HIGH threshold only.

The default policy treats this differently from description drift, and the reasoning is worth stating: results are per-call, so one poisoned response does not mean every tool on the server is compromised. Policy therefore blocks the tool, not the server:

yaml
- name: block-tool-on-result-injection
  match:
    detector: integrity.result_injection
    min_severity: HIGH
  action: block_tool
  ttl: 15m

Evidence is quotable here

Tool descriptions and schemas are server-advertised metadata, not user secrets. Quoting the matched text is safe and is the whole value of the finding — a human needs to see what the injected text actually said. Findings carry description_before and description_after, truncated to 300 characters.

That is the opposite of the egress detector, which never emits a value. Redaction is a property of the detector, not of the finding type.