Core concepts
Tool-definition integrity
The dangerous part of an MCP server may be the sentence your model reads. Kams makes that sentence a tracked, pinned artifact.
The threat
MCP tool descriptions are injected verbatim into the model's context. That makes them executable instruction, not documentation. A server can therefore:
| Threat | Shape |
|---|---|
| Rug pull | A definition is swapped after trust is established |
| Tool poisoning | Malicious instructions live inside a description |
| Cross-server shadowing | One server describes behaviour for another server's tools |
| Tool squatting | Homoglyph or near-miss names that render identically |
In every case the tool's code may be unchanged. Only the advertised metadata moves.
Why not just hash the response
The obvious approach — hash the whole tools/list response — does not survive contact
with reality. Servers legitimately reorder tools, add tools, and bump versions, so a
whole-response hash fires on every benign change.
An alert that fires constantly gets muted
And a muted alert is worse than no alert, because it creates the belief that something is being watched.
What Kams does instead
Each tool is canonicalised and digested separately over exactly the triple that can reach the model or constrain its calls:
digest = sha256({
name,
description → NFC-normalised, trimmed,
inputSchema → recursively key-sorted,
})
Formatting churn that cannot influence the model — key ordering, whitespace, tool ordering — is normalised away. Two sub-digests are kept alongside it so a description change can be told apart from a schema change, because those are different threats.
Invisible characters survive on purpose
Normalisation is NFC only. Zero-width and bidi codepoints are a real poisoning vector, so they must reach the digest and be caught by the detector. Stripping them here would hide exactly the attack Kams exists to find.
Provisional vs pinned
kams.lock distinguishes two very different states:
Provisional
Created on first sighting. Kams remembers what it saw, but nobody vouched for it. Drift here is informational — severity is reduced by one level.
Pinned
Created by kams pin. The definition is now a human assertion. Drift violates
something a person approved, so severity is not reduced.
kams pin notes-mcp # promote one server
kams pin # promote every recorded server
kams status # see state and per-tool digests
This is the single most important idea in the detector. The same byte change is more severe after pinning, because it is no longer just a difference from a remembered sample — it contradicts a review that actually happened.
A server can arrive already poisoned
Trust-on-first-use is not a reason to skip inspection. On first sighting Kams still scores every description as it stands, and reports anything that already reads as model-directed instruction.
Severity, in one table
| Change | Default severity | Why |
|---|---|---|
| Description changed | High — Critical when pinned and injection-like | Descriptions enter the model's context |
| Schema widened | Medium | Expands the egress surface |
| Tool added | Medium | Introduces an unreviewed capability |
| Schema narrowed | Low | Usually a compatibility or bug fix |
| Tool removed | Low | Availability, not instruction integrity |
"Widened" means more ways in: new properties, or previously-required fields becoming
optional. A new optional callback_url parameter is a very different event from a
tightened type constraint.
One metric, one meaning
Only integrity.definition_drift increments mcp.integrity.drift.count. Result-side
injection and every other finding family increment the generic kams.finding.count.
That separation is what lets the critical SigNoz alert stay narrow enough to be trustworthy — it cannot fire on the wrong condition. See Integrity & injection for the scoring model, and SigNoz for the alert definition.