
mcpsnoop v0.12.0
Wireshark for MCP. Un proxy transparent qui montre chaque appel d'outil réel entre votre client IA et vos serveurs MCP, en direct dans votre terminal.
Wireshark pour MCP. Un proxy transparent qui affiche chaque appel d'outil réel entre votre client IA et vos serveurs MCP, en direct dans votre terminal.
Le problème
L'inspecteur MCP officiel se connecte en tant que son propre client, il ne voit donc jamais ce que votre client (Cursor, Claude Code, Codex) envoie réellement à votre serveur. Et tout ce qui attend qu'une requête arrive ne peut pas afficher l'appel que le modèle n'a jamais fait, ou qu'il a fait avec les mauvais arguments. Lorsqu'un outil n'est pas appelé silencieusement, que les capacités ne correspondent pas, ou qu'un appel reste bloqué, vous vous retrouvez à fouiller les journaux et à deviner.
mcpsnoop se place plutôt dans le chemin de données réel. Enveloppez votre commande serveur avec lui et observez chaque trame JSON-RPC en direct, pendant que votre vrai client et votre serveur dialoguent.
Démarrage rapide
Voyez-le immédiatement, sans rien à configurer.```bash mcpsnoop demo
Pour l'utiliser pour de vrai, enveloppez votre serveur dans la config MCP de votre client.```json
{
"mcpServers": {
"my-server": {
"command": "mcpsnoop",
"args": ["--", "node", "build/index.js"]
}
}
}
Tout ce qui suit -- est la commande qui lance normalement votre serveur. Remplacez-la
par ce que vous utilisez déjà, comme python server.py, npx -y @scope/server, ou un
binaire compilé.
Sur Claude Desktop, vous n'avez pas à faire cette modification à la main.```bash mcpsnoop wrap my-server # route my-server through mcpsnoop mcpsnoop unwrap my-server # put it back
`wrap` trouve `claude_desktop_config.json`, le copie vers
`claude_desktop_config.json.mcpsnoop.bak` la première fois, et réécrit uniquement
l'entrée de ce serveur, afin que votre mise en forme et tous les autres serveurs soient laissés intacts.
Dans l'entrée réécrite, les clés reviennent dans l'ordre alphabétique. `unwrap`
restaure le fichier et supprime la sauvegarde une fois qu'aucun serveur n'est plus enveloppé.
Redémarrez Claude Desktop après l'une ou l'autre opération, car les serveurs MCP sont lancés une seule fois au
démarrage.
Ensuite, utilisez votre client comme d'habitude et ouvrez l'interface.```bash
mcpsnoop
Aucun flag, aucun chemin de socket, aucun ordre de démarrage à retenir. Le shim et l'interface se découvrent automatiquement, et l'interface reconstitue les sessions passées depuis le disque.
Pour un serveur HTTP streamable, exécutez mcpsnoop comme proxy inverse.```bash mcpsnoop http --target http://localhost:3000/mcp --listen :7000
Le statut HTTP de chaque réponse apparaît dans le flux, de sorte qu'une réponse qui ne porte
aucun message JSON-RPC qui lui soit propre reste une trame visible plutôt que rien : le
défi 401, le 403 sur une Origin rejetée, le 202 qui accuse réception d'une
notification, et le 502 lorsque la cible est totalement injoignable. L'en-tête
`WWW-Authenticate` d'un 401 est conservé tel quel et affiché dans l'inspecteur, car il
nomme le schéma d'authentification et les métadonnées de ressource à consulter ensuite. Filtrez par statut
avec `status:401` dans la TUI, ou par toute erreur avec `status:err`. Un 4xx ou 5xx
est considéré comme une erreur, donc une exécution par défaut de `mcpsnoop check` échoue dessus.
Pas de serveur à vous ? [Essayez pour de vrai](https://github.com/kerlenton/mcpsnoop/blob/HEAD/docs/TRY_IT.md) contre un serveur de test
publié, piloté par votre propre client. Pour inspecter une session après coup,
voir [examiner les sessions passées à partir des journaux](https://github.com/kerlenton/mcpsnoop/blob/HEAD/docs/POST_MORTEM.md).
### Fichier de configuration
Si vous réutilisez les mêmes options shim dans un projet, placez-les dans un
fichier `.mcpsnoop.toml` dans le répertoire de travail courant.```toml
label = "filesystem"
trace-file = "trace.jsonl"
redact-secrets = true
redact-key = "token,authorization"
redact-value = "sk-[A-Za-z0-9]+"
redact-path = "$.params.arguments.password"
no-trace = false
Répétez redact-key, redact-value et redact-path sur leurs propres lignes pour en ajouter plus d'un de chaque.
Ce sont toutes les clés qu'il prend en charge.
Le fichier n'est recherché que dans le répertoire de travail courant, pas dans les répertoires parents.
Les options explicites de la ligne de commande remplacent les valeurs du fichier de configuration.
Commandes
| Commande | Ce qu'elle fait |
|---|---|
mcpsnoop -- <server> | envelopper un serveur stdio en tant que shim transparent |
mcpsnoop | ouvrir la TUI en direct |
mcpsnoop http --target <url> | faire office de proxy pour un serveur HTTP streamable |
mcpsnoop export | générer une session vers json, html, text, har ou otlp |
mcpsnoop check | faire échouer la CI en cas d'erreurs, de trames invalides, d'avertissements, d'inadéquations de routage, d'appels bloqués ou de résultats tardifs |
mcpsnoop baseline | inspecter, accepter ou réinitialiser les définitions d'outils de confiance |
mcpsnoop diff | comparer les outils et les appels entre deux sessions capturées |
mcpsnoop open | ouvrir une session sauvegardée dans la TUI |
mcpsnoop prune | supprimer les journaux de sessions sauvegardées plus anciens qu'un seuil |
mcpsnoop wrap <server> | router un des serveurs de Claude Desktop via mcpsnoop |
mcpsnoop unwrap <server> | rétablir l'entrée de ce serveur telle qu'elle était |
mcpsnoop remote <user@host> | afficher la commande de tunnel SSH |
mcpsnoop demo | jouer une session scriptée |
Exécutez mcpsnoop help pour la liste complète, ou mcpsnoop help <command> pour les options d'une commande.
Comparaison
| MCP Inspector | mcpsnoop | |
|---|---|---|
| Voit le trafic réel entre votre client et votre serveur | non | oui |
| Signale les appels bloqués et les erreurs de flux | non | oui |
| Signale les sorties parasites qui corrompent le flux | non | oui |
| Signale les trames JSON-RPC malformées | non | oui |
| Détecte la dérive des définitions d'outils après approbation | non | oui |
| Interface terminal interactive | non | oui |
| Zéro configuration, ni options ni ordre | non | oui |
| Inspecteur de capacités | partiel | oui |
| Rejouer un appel capturé | non | oui |
| Export de session (json / html / text / otlp) | non | oui |
| Binaire unique, sans dépendances d'exécution | non | oui |
Installation
Go```bash
go install github.com/kerlenton/mcpsnoop/cmd/mcpsnoop@latest
### Homebrew```bash
brew install mcpsnoop
Des binaires précompilés pour chaque plateforme sont disponibles sur la page Releases.
Complétions shell
mcpsnoop fournit des complétions pour bash, zsh, fish et PowerShell. Exécutez mcpsnoop completion <shell> --help pour connaître les étapes de configuration, qui couvrent l'activation de la complétion et le chemin d'installation pour votre système d'exploitation.
Comment ça fonctionne
mcpsnoop joue deux rôles dans un seul binaire. mcpsnoop -- <server> est le shim transparent que votre client lance, transmettant les octets tels quels tout en envoyant une copie de chaque trame au hub. mcpsnoop sans argument est ce hub et son TUI en direct. Ils s'apparient via un socket bien connu et des journaux sur disque, de sorte qu'aucun des deux n'a besoin de démarrer en premier.
Le hub charge par défaut les 100 sessions sauvegardées les plus récentes, afin de limiter le travail au démarrage sans supprimer les traces plus anciennes. Utilisez mcpsnoop --history-limit N pour choisir une autre limite, ou mcpsnoop --history-limit 0 pour charger tout l'historique. Les sessions plus anciennes restent disponibles via mcpsnoop open <session-id> et mcpsnoop export <session-id>.
La limite d'historique borne ce qui est chargé ; mcpsnoop prune borne ce qui est conservé. Il supprime les journaux de sessions sauvegardées antérieurs à un seuil, et ne s'exécute jamais tout seul.```bash
mcpsnoop prune --older-than 30d --dry-run # list what would go, remove nothing
mcpsnoop prune --older-than 30d # delete after confirming
mcpsnoop prune --older-than 72h --yes # skip the prompt in a script
`--older-than` est requis (il n'y a pas de valeur par défaut qui supprimerait quoi que ce soit) et
accepte un nombre de jours comme `30d` ou une durée Go comme `72h`. Les baselines d'outils ne sont pas touchées,
car une baseline est identifiée par l'étiquette du serveur plutôt que par la session.
Comme il se trouve dans le flux réel, pas sur le côté comme l'Inspector, il
voit exactement ce que votre client réel et votre serveur se disent, quel que soit
le langage dans lequel le serveur est écrit.
## Raccourcis clavier
| Touche | Action | | Touche | Action |
|---|---|---|---|---|
| `enter` | inspecter / approfondir | | `/` | filtrer |
| `esc` | retour | | `:` | commande |
| `j` / `k` | déplacer | | `r` | rejouer un appel |
| `g` / `G` | haut / bas | | `c` | capacités |
| `ctrl-f` / `ctrl-b` | page | | `s` | résumé d'outil |
| `p` | pause | | `y` | copier |
| `shift`+`<key>` | trier par colonne | | `e` | exporter |
| `ctrl-d` | supprimer la session | | `f` | suivre |
| `?` | aide | | | |
Appuyez sur `?` dans l'application pour la liste complète.
## Filtrer le flux
Appuyez sur `/` dans une session et combinez des jetons séparés par des espaces, avec ET logique. Le texte brut
correspond à la méthode, à l'outil, à l'identifiant et à la charge utile.
| Jeton | Filtre par | Exemple |
|---|---|---|
| `tool:` | nom de l'outil | `tool:search` |
| `method:` | méthode JSON-RPC | `method:tools/call` |
| `id:` | identifiant de requête, et toute nouvelle tentative qui le poursuit | `id:7` |
| `task:` | identifiant de tâche | `task:01J...` |
| `dir:` | direction (`c2s`, `s2c`) | `dir:s2c` |
| `kind:` | type de trame (`req`, `resp`, `notify`, `stderr`, `invalid`) | `kind:invalid` |
| `status:` | résultat de l'appel (`ok`, `error`, `cancel`, `late`, `cancelled`, `pending`, `bad`, `warn`, `mismatch`, ou un statut HTTP comme `401`) | `status:error` |
Empilez les jetons pour affiner.```text
tool:search status:pending # in-flight calls to one search tool
status:cancel # calls the client gave up on (status:cancelled is a cancelled task)
status:late # results that arrived after the cancellation
method:tools/call status:error # tool calls that failed
dir:s2c kind:req # server-initiated requests (servers before 2026-07-28)
Le dernier ne trouve quelque chose que sur un serveur utilisant la version 2025-11-25 ou antérieure. La révision 2026-07-28 a supprimé les requêtes initiées par le serveur, et un serveur qui a besoin de quelque chose du client répond désormais à la propre requête du client en le demandant, puis le client réessaie. mcpsnoop relie ces nouvelles tentatives à la requête qu'elles continuent, de sorte que l'échange se lit comme un seul appel plutôt que plusieurs.
Exporter des sessions
Transformez n'importe quelle session capturée en un fichier portable.```bash mcpsnoop export -T json|html|text|har|otlp [-o file|-] [session-id|log.jsonl|-]
| Format | Ce que vous obtenez |
|---|---|
| `json` | appels corrélés, compteurs par outil et latence p50/p95/p99, appels les plus lents, capacités et trames brutes |
| `html` | un fichier autonome pour navigateur avec recherche et JSON repliable |
| `text` | un dump en texte brut lisible |
| `har` | une entrée par appel corrélé, ouvrable dans les devtools du navigateur et tout autre outil lisant le HAR |
| `otlp` | OTLP JSON avec une span par appel corrélé ; le contexte de trace W3C relie les traces de l'appelant, sinon une trace est utilisée par session |
MCP n'est pas HTTP, donc l'URL, le code de statut et les durées d'une entrée HAR sont un mapping
volontaire de chaque appel plutôt qu'une transcription sur le fil.
Pour OTLP, le `_meta.traceparent` d'une requête fournit la trace de cet appel et les IDs de span
parente, et `_meta.tracestate` accompagne la span. Lorsque le traceparent est
absent ou invalide, mcpsnoop conserve la trace dérivée de la session et ne transporte aucun état.
mcpsnoop observe plutôt qu'il ne participe, donc il n'ajoute aucune entrée fournisseur propre
et transmet l'état de l'appelant sans modification.```bash
mcpsnoop export -T html -o out.html # an HTML file to open in a browser
mcpsnoop export -T text server.py-48213-7f3a1c9e2b04 # a specific session, as text
mcpsnoop export -T json | jq # the newest session, piped to jq
mcpsnoop export -T har -o session.har # a HAR file to open in browser devtools
mcpsnoop export -T otlp -o trace.json # import into an OTLP-compatible tracing backend
Omettez -o pour écrire sur stdout, et omettez la session pour prendre la plus récente, ou passez - pour lire le JSONL depuis stdin. Dans la TUI, appuyez sur e pour exporter la session sélectionnée en HTML, ou exécutez :export json|html|text|har|otlp [path] depuis le mode commande.
Pour nettoyer une capture existante avant de l'inspecter ou de la partager, passez les mêmes indicateurs de masquage utilisés lors de la capture à export ou open :```bash
mcpsnoop export session.jsonl --redact-secrets --redact-key project_token -o shared.json
mcpsnoop open session.jsonl --redact-path '$.params.arguments.password'
Ces drapeaux réécrivent le fichier exporté ou la vue TUI en mémoire, jamais le JSONL source. `export` refuse une sortie qui nomme le même fichier que son entrée, et écrit via un fichier temporaire renommé en place, si bien qu'une exécution qui échoue laisse le fichier précédent intact.
Les `inputSchema` et `outputSchema` d'un outil, tels qu'annoncés dans un résultat `tools/list`, ne sont pas touchés par `--redact-key` et `--redact-secrets`. Un nom dans un schéma est une déclaration de type plutôt qu'une valeur ; le nom lui-même reste de toute façon dans le journal, et masquer le sous-schéma sous une propriété appelée `token` emporterait avec lui les propres vérifications de l'outil. L'exemption ne vaut que pour cette position, donc un argument qui s'appelle par hasard `inputSchema` est masqué comme n'importe quel autre, et cela s'arrête à `default`, `const`, `examples` et `enum`, qui contiennent des données plutôt que de la structure. Utilisez `--redact-path` pour nommer quelque chose dans un schéma, ou `--redact-value`, qui correspond au texte où qu'il se trouve, sauf dans les deux mots-clés que mcpsnoop analyse, `type` et `x-mcp-header`.
Ce que chaque drapeau atteint diffère, donc vérifiez le résultat plutôt que de supposer. Les quatre masquent les charges utiles JSON-RPC, et `--redact-key`, `--redact-path` et `--redact-secrets` n'atteignent que celles-ci. Seul `--redact-value` masque aussi stderr, les autres textes non JSON et l'intérieur d'une chaîne. Un en-tête `Mcp-Param-*` est masqué en même temps que la valeur du corps qu'il reflète ; les autres métadonnées d'enveloppe, les étiquettes de serveur, `Mcp-Name`, `Mcp-Method` et le statut HTTP restent tels que capturés. Le masquage est effectué dans la mesure du possible, utilisez donc un chemin de sortie séparé et lisez le résultat avant de le partager.
### Diffuser les appels terminés vers un collecteur OTLP
Envoyez des spans pendant que le proxy est en cours d'exécution en le pointant vers un point de terminaison de traces JSON OTLP/HTTP. Répétez `--otlp-header` pour l'authentification du collecteur ou les en-têtes de locataire.```bash
mcpsnoop \
--otlp-endpoint http://localhost:4318/v1/traces \
--otlp-header "Authorization=Bearer $OTLP_TOKEN" \
-- node build/index.js
mcpsnoop http \
--target http://localhost:3000/mcp \
--otlp-endpoint http://localhost:4318/v1/traces
La remise est de type best-effort et ne bloque jamais le trafic MCP proxyé. Si le collecteur est indisponible, mcpsnoop réessaie en arrière-plan et abandonne les nouvelles trames de trace lorsque sa file d'attente bornée est pleine. Le journal de session JSONL normal reste l'enregistrement durable.
Comparer des sessions
Comparez deux sessions enregistrées par identifiant ou chemin JSONL.```bash mcpsnoop diff before-session after-session mcpsnoop diff old.jsonl new.jsonl
Le rapport montre les outils ajoutés ou supprimés, les modifications de description
et de `inputSchema`, les appels d'outils correspondants dont le statut a changé, et
les changements notables de durée. Les appels sont appariés par nom d'outil et par
arguments, donc les appels réordonnés se comparent toujours correctement.
Par défaut, les changements de durée doivent différer d'au moins 100 ms et d'un
facteur 2 ; utilisez `--duration-threshold` et `--duration-ratio` pour ajuster ces
seuils.
Passez `--exit-code` pour faire échouer la CI en cas de régressions : elle se termine
avec un code non nul lorsque la session « après » supprime un outil, modifie la
description d'un outil, le titre, le schéma d'entrée, le schéma de sortie ou les
annotations, contient un appel dont le statut s'est dégradé, ou ralentit. Les
améliorations (outils ajoutés, appels corrigés, accélérations) se terminent toujours
par zéro, de même qu'un changement d'icône, qui modifie l'apparence d'un outil sans
changer ce qu'il fait.
## Vérification des sessions dans la CI
Bloquez une session d'agent enregistrée en cas d'erreurs, de corruption du flux,
d'avertissements de protocole, d'incohérences d'en-tête de routage, d'appels sans
réponse, de trames perdues laissant la capture incomplète, de dérive des définitions
d'outils ou d'utilisation de fonctionnalités de protocole obsolètes.```bash
mcpsnoop check [--format text|junit|sarif] [--fail-on error,invalid,warn,mismatch,pending,late-result,drift,deprecated,incomplete,schema] [session-id|log.jsonl|-]
error, invalid et warn font échouer la vérification à eux seuls. Les autres sont facultatifs.
Passez un sous-ensemble séparé par des virgules pour ne bloquer que ce qui compte pour une tâche, omettez la
session pour vérifier la capture la plus récente, ou utilisez - pour lire le JSONL depuis stdin.
| Signal | Échoue si |
|---|---|
error | un appel ayant reçu une erreur JSON-RPC en réponse, un résultat marqué isError, ou une tâche terminée par un échec |
invalid | une trame sur le canal de protocole qui n'est pas du JSON-RPC valide, généralement un serveur qui journalise sur stdout |
warn | une trame qui enfreint une attente de la spécification MCP ou JSON-RPC |
mismatch | un en-tête de routage en désaccord avec le corps, accompagnant un lot, ou manquant là où la révision l'exige |
pending | une requête encore ouverte à la fin de la capture, laissant l'appelant en attente |
late-result | une réponse arrivée après l'annulation de sa requête |
drift | une définition d'outil annoncée qui change après l'approbation de la référence |
deprecated | une fonctionnalité que la spécification a dépréciée |
incomplete | des trames abandonnées en amont, ce qui fait de chaque autre compteur un plancher plutôt qu'un total |
schema | un schéma annoncé utilisant une construction ou un dialecte qui se propage mal d'un client à l'autre |
Chaque signal est compté, qu'il soit bloquant ou non, de sorte qu'une exécution indique ce qu'elle a trouvé avant que vous décidiez ce qui doit faire échouer la vérification.``` session build-agent: errors=1 invalid=0 warnings=0 mismatches=0 pending=0 late_results=0 deprecated=0 missing_frames=0 schema_findings=1 schema findings: oneOf: search check failed: error
Le nombre de trames perdues voyage aussi avec les artefacts, de sorte qu'une capture qui
se sous-estime le dit partout où elle est ouverte : `missing_frames` dans l'export JSON,
`log.comment` dans HAR, et l'attribut de ressource `mcpsnoop.session.missing_frames`
dans OTLP.```bash
mcpsnoop check build-agent
mcpsnoop check --fail-on error,invalid artifacts/session.jsonl
mcpsnoop check --fail-on mismatch gateway-run.jsonl
Au-delà des compteurs de signaux, vérifiez la forme de l'exécution. Ces vérifications se combinent entre elles et avec --fail-on, et tout échec se solde par une sortie non nulle.
| Option | Échoue quand |
|---|---|
--max-duration <dur> | un ou plusieurs appels d'outil terminés ont dépassé le budget ; indique leur nombre et le pire appel |
--expect-tool <name> | l'outil nommé n'a jamais été appelé (répétable) |
--forbid-tool <name> | l'outil nommé a été appelé (répétable) |
a contract for the run: search must run, delete must not, nothing over 2s
mcpsnoop check --expect-tool search --forbid-tool delete --max-duration 2s run.jsonl
### Signalez-le là où la CI cherche déjà
`--format junit` écrit un `<testcase>` par signal et par session, et ses échecs
suivent la même sélection `--fail-on` que la sortie texte.```yaml
- name: Check captured MCP session
run: |
mkdir -p test-results
mcpsnoop check --format junit artifacts/session.jsonl > test-results/mcpsnoop.xml
- name: Upload mcpsnoop JUnit report
if: always()
uses: actions/upload-artifact@v4
with:
name: mcpsnoop-junit
path: test-results/mcpsnoop.xml
--format sarif écrit un journal SARIF 2.1.0 à la place. Alors que junit rapporte un
agrégat par signal, SARIF rapporte un résultat par constatation, portant la session,
le Seq de la frame et le texte d'avertissement ou de dérive propre à la frame, et pointant vers la
ligne du journal à partir de laquelle la frame a été décodée. Un signal nommé dans --fail-on est
rapporté au niveau error et un signal qui n'y est pas nommé au niveau note, afin que le rapport et
la barrière ne soient jamais en désaccord.
Un résultat pointe vers le journal avec un chemin relatif au répertoire de travail, que
code scanning résout ensuite par rapport à la racine du dépôt. L'alerte s'affiche avec
les lignes environnantes uniquement lorsque ce chemin est un fichier du commit analysé ; ainsi, une
capture générée par le workflow dans artifacts/ ouvre une alerte portant le
message, la règle et le numéro de ligne, mais sans vue de la source. Valider une capture
que l'on souhaite afficher en entier est le seul moyen d'en obtenir une. Un journal lu depuis le répertoire d'état
ou depuis stdin ne reçoit aucun chemin.
Code scanning rejette un fichier dont l'exécution contient plus de 25 000 résultats et
n'affiche que les 5 000 premiers de ceux qu'il accepte, le rapport est donc plafonné à 5 000 :
d'abord les constatations sur lesquelles la barrière a échoué, puis un résultat mcpsnoop/report-truncated
indiquant combien ont été laissés de côté. Les formats text et junit restent complets.
Pour placer les constatations dans l'onglet Security, transmettez le journal SARIF à
upload-sarif. Le job doit disposer de security-events: write, sinon l'upload répond
403. check se termine avec un code non nul en cas de constatation, donc l'étape d'upload a besoin de if: always()
pour s'exécuter sur les exécutions qui ont quelque chose à signaler ; continue-on-error
transmet le verdict au contrôle code scanning, qui échoue sur une alerte de niveau error
et peut être défini comme contrôle requis. Supprimez-la si vous préférez que l'étape de contrôle
elle-même soit ce qui fait passer le job en rouge.```yaml
permissions:
required for all workflows
security-events: write
only required for workflows in private repositories
actions: read contents: read
steps:
- name: Check captured MCP session continue-on-error: true run: mcpsnoop check --format sarif artifacts/session.jsonl > mcpsnoop.sarif
- name: Upload mcpsnoop SARIF report if: always() uses: github/codeql-action/upload-sarif@v4 with: sarif_file: mcpsnoop.sarif category: mcpsnoop
### Détecter un en-tête de routage en désaccord avec le corps
Sur le transport HTTP streamable, une passerelle route en fonction de `Mcp-Method` et `Mcp-Name` pendant que le serveur lit le corps ; un en-tête en désaccord avec le corps signifie donc que les deux regardent deux requêtes différentes. Le signal `mismatch` couvre ce cas, ainsi qu'un en-tête porté par un lot qu'il ne peut pas adresser et un en-tête obligatoire entièrement absent.
Dans la révision 2026-07-28, un en-tête de routage manquant est une erreur de validation, et un serveur conforme rejette la requête avec `400` et `-32020`. mcpsnoop ne le signale que lorsque la session est connue pour parler cette révision ou une révision ultérieure, car les révisions antérieures ne définissent pas du tout ces en-têtes et les omettre y est correct. Le rejet `-32020` émis par un serveur compte comme le même signal.
Un nom ou un URI de ressource qui ne tient pas dans une valeur de champ HTTP transite en Base64 dans une sentinelle `=?base64?…?=`, qui est décodée avant la comparaison, de sorte qu'un client qui encode correctement n'est jamais signalé.
Sur les requêtes HTTP `tools/call`, mcpsnoop affiche également chaque en-tête `Mcp-Param-{Name}` et, lorsque la définition d'outil annoncée correspondante est connue, la compare au chemin d'argument annoté. Les propriétés imbriquées, la sentinelle Base64, les booléens et les entiers sûrs équivalents numériquement sont traités sans faux positifs de comparaison de chaînes. Les en-têtes de paramètres inconnus et les sessions sans définition d'outil correspondante restent observationnels. La rédaction par clé et par valeur s'applique aux valeurs d'en-têtes de paramètres capturées avant qu'elles n'atteignent un puits, et une valeur que mcpsnoop a lui-même masquée n'est jamais signalée comme un désaccord.
### Détecter la dérive des définitions d'outils
Le premier `tools/list` complet observé pour un label de serveur devient sa référence de confiance. Les sessions ultérieures comparent cette référence champ par champ : la description, le titre, les schémas d'entrée et de sortie, les annotations et les icônes, ainsi que les outils ajoutés ou supprimés. Les annotations sont ce qui compte le plus, car un outil approuvé avec `readOnlyHint` qui se déclare ensuite destructif est le piège que ce contrôle existe pour démasquer, et la spécification demande aux clients de traiter les annotations comme non fiables. Le titre et les icônes sont suivis parce que ce sont eux que l'utilisateur voit, et la spécification classe le `title` d'un outil au-dessus de `annotations.title` et de son nom. La table des sessions et le résumé des outils signalent la dérive sans bloquer ni modifier le trafic MCP.
Les annotations sont comparées via leurs valeurs par défaut de la spécification, de sorte qu'un serveur qui commence à expliciter une indication sur laquelle il s'appuyait déjà n'est pas signalé. Une référence enregistrée avant que mcpsnoop ne suive un champ continue de fonctionner pour les champs qu'elle enregistre et indique ceux auxquels elle ne peut pas répondre ; réenregistrez avec `mcpsnoop baseline --accept` une fois que vous faites confiance aux définitions actuelles.
Modifier ce que la rédaction enregistre change ce que la dérive compare. Une référence prise sans `--redact-value` puis vérifiée contre une capture prise avec cette option signale les champs masqués comme modifiés, ce qui est correct, puisque la définition enregistrée a réellement changé. Réenregistrez avec `--accept` après avoir modifié les paramètres de rédaction.
Utilisez un `--label` stable et unique pour chaque serveur dont le nom de commande ou l'hôte cible entrerait sinon en collision. Les références sont stockées dans le répertoire d'état habituel de mcpsnoop, donc `MCPSNOOP_HOME` et `XDG_STATE_HOME` s'appliquent.```bash
mcpsnoop check --fail-on drift session.jsonl
mcpsnoop baseline session.jsonl
mcpsnoop baseline --accept session.jsonl # trust a legitimate definition change
mcpsnoop baseline --reset session.jsonl # trust the next complete tools/list
Dans une CI éphémère, le répertoire d'état démarre vide, donc la première exécution enregistre uniquement la baseline et ne signale aucune dérive. La baseline doit persister entre les exécutions pour que les exécutions ultérieures puissent vérifier par rapport à elle. Pointez --baseline vers un répertoire versionné ou en cache, ou définissez MCPSNOOP_HOME sur un chemin persistant.```bash
mcpsnoop check --fail-on drift --baseline .mcpsnoop/baselines session.jsonl
`drift` est en option pour `check` ; le filtre par défaut `error,invalid,warn` reste inchangé.
### Signaler les fonctionnalités de protocole dépréciées
La révision du 2026-07-28 déprécie Roots, Sampling et Logging. Elles continuent de fonctionner
pendant au moins un an, donc mcpsnoop les signale plutôt que de les traiter comme des erreurs.
Le flux, l'inspecteur de capacités et l'exportation les signalent tous, et chaque marqueur
indique le remplacement.
Deux des trois ne sont désormais accessibles que via une requête multi-allers-retours, où
le nom de la méthode se trouve dans la map `inputRequests` du serveur plutôt que sur la
trame elle-même. Ces deux-là sont également signalés, afin qu'un serveur ayant adopté le
nouveau modèle ne cesse pas silencieusement de signaler.```bash
mcpsnoop check --fail-on deprecated session.jsonl
Comme drift, deprecated est facultatif. Une exécution par défaut rapporte le nombre et reste verte, donc une session utilisant une fonctionnalité dépréciée encore légale ne fait jamais passer le CI au rouge à elle seule.
Signaler les constructions de schéma que les clients gèrent mal
Un serveur peut être parfaitement valide et rester difficile à utiliser pour un agent. Les clients diffèrent dans leur prise en charge réelle de JSON Schema, et un outil que le modèle continue d'appeler à tort est souvent un outil dont le schéma demandait plus que ce que le client fournit.
Le résumé des outils, ouvert avec s, comporte une colonne SCHEMA nommant la particularité la plus notable du schéma de chaque outil annoncé, avec un + final lorsqu'il y en a plusieurs.
| Affiché | Signification |
|---|---|
no root | l'inputSchema est absent, n'est pas un objet JSON, ou a un type racine autre que "object" |
dialect | un $schema nommant un autre dialecte que le 2020-12 utilisé par défaut par la révision |
ext ref | un $ref pointant hors du document, cas pour lequel la spécification avertit également les implémenteurs de ne pas le suivre aveuglément |
oneOf, anyOf, allOf, not | un mot-clé de composition, géré de manière incohérente selon les clients |
ref | un $ref pointant à l'intérieur du même document |
untyped | une propriété qui ne déclare ni type ni autre moyen d'indiquer ce qu'elle accepte |
Sauf le premier, il s'agit d'observations plutôt que de verdicts. Un schéma utilisant oneOf n'est pas faux, seulement susceptible d'être lu différemment par différents clients, et un schéma peut déclarer le dialecte qu'il veut. no root est l'exception : la définition Tool exige inputSchema et fixe son type racine à "object", donc un client qui valide une liste rejette cet outil d'emblée et celui-ci ne devient jamais appelable, sans rien sur le fil pour expliquer pourquoi. no root est en tête de colonne pour cette raison, et un schéma que l'expurgation propre à mcpsnoop a nettoyé n'est jamais signalé, car un schéma illisible n'est pas un schéma incorrect.
Cette distinction détermine le traitement que check leur réserve. no root est un avertissement sur la trame tools/list, donc il fait échouer la porte par défaut error,invalid,warn sans aucun flag, ce qui est le but : un serveur qui fournit un outil inutilisable répond normalement à chaque poignée de main et ne reçoit simplement jamais de tools/call. Les observations sont comptées comme schema_findings et rapportées sous schema findings:, et ne font échouer l'exécution que lorsque vous ajoutez schema à --fail-on. Les deux sont inclus dans --format junit et --format sarif, et export inclut la liste par outil sous summary.definitions.per_tool[].findings.```bash
mcpsnoop check session.jsonl # a non-object root already fails this
mcpsnoop check --fail-on schema session.jsonl # and now so do the observations
La colonne porte la couleur d'avertissement et jamais le rouge de la colonne ERR, et
mcpsnoop ne change toujours rien au trafic qu'il relaie.
Rien n'est résolu ni récupéré. Un `$ref` externe est reconnu par sa seule forme,
et le schéma vers lequel il pointe n'est jamais lu.
### Voyez ce que le serveur vous coûte en contexte
Les définitions d'outils entrent dans le contexte du modèle à chaque conversation, et les
résultats d'outils à chaque appel. Le résumé d'outil (`s`) mesure les deux à partir de la session
que vous avez réellement capturée.
La ligne `definitions` est le coût fixe : ce que pèse le `tools/list` de ce serveur
avant qu'un seul appel ne soit effectué. La colonne `DEF` détaille cela par
outil et `RESULT` correspond à ce que les réponses de chaque outil ont coûté jusqu'à présent. Le tableau reste
trié par erreurs et latence, alors parcourez `DEF` pour trouver les définitions coûteuses ;
l'export les classe des plus lourdes aux plus légères. Une ligne sous le tableau désigne le résultat
le plus lourd, qu'un total masque.
Les chiffres de définition correspondent au JSON dont les espaces blancs insignifiants ont été supprimés, donc un
serveur qui met en forme son `tools/list` de manière lisible n'est pas compté comme plus coûteux qu'un
serveur qui ne le fait pas, et le même serveur mesure la même chose d'une capture à l'autre.
`RESULT` correspond aux octets tels qu'ils sont arrivés : un résultat est une charge utile ponctuelle plutôt
qu'un contrat qui mérite d'être normalisé.```bash
mcpsnoop export -T json | jq '.summary.definitions'
L'export contient les mêmes chiffres, par outil et répartis entre la description et les octets de schéma, de sorte qu'une description volumineuse et un schéma volumineux restent séparables et que chacun puisse être suivi d'une capture à l'autre. mcpsnoop diff vous indique qu'une description ou un schéma a changé entre deux sessions ; l'export est là où réside la taille de ce changement.
Ce sont des octets, pas des tokens. Un nombre de tokens dépend du modèle, donc en mesurer un reviendrait à embarquer un tokeniseur et à choisir lequel. Les octets sont exacts et vous pouvez appliquer votre propre ratio. Un tools/list non terminé rapporte ce qu'il a vu comme un plancher et le dit explicitement, plutôt que de faire passer une somme partielle pour le total.
Détecter un client qui altère l'état du serveur
Dans le modèle à multiples allers-retours, le serveur remet au client un requestState opaque et le client doit le renvoyer tel quel lors de la nouvelle tentative. Le serveur doit le traiter comme une entrée contrôlée par l'attaquant, car un client qui le falsifie peut tenter de modifier le comportement du serveur ou de contourner un contrôle d'autorisation.
Étant dans le flux, mcpsnoop voit la valeur partir et revenir, il peut donc dire quand le contrat a été rompu. Trois façons de le rompre, chacune signalée comme un avertissement de protocole lors de la nouvelle tentative.
| Signalé | Signification |
|---|---|
MRTR retry changed requestState | le client a renvoyé autre chose que ce que le serveur avait émis |
MRTR retry is missing requestState | le serveur en avait émis un et la nouvelle tentative l'a omis |
MRTR retry invented requestState | la nouvelle tentative portait un que le serveur n'a jamais émis |
Ce sont des violations de protocole par le client plutôt que des constats de notre part, elles empruntent donc le signal d'avertissement ordinaire et une exécution check par défaut échoue sur l'une d'elles. C'est délibéré. Un client qui altère l'état du serveur mérite d'arrêter un build.
La valeur elle-même n'est jamais affichée ni journalisée, et rien ne la décode ni ne l'analyse. Il peut s'agir d'un blob chiffré portant un principal et un token, et comparer des octets opaques est tout le contrôle.
Un cas reste hors de portée. Lorsqu'un serveur répond avec un requestState et sans inputRequests, une nouvelle tentative falsifiée ne correspond à rien et ne renvoie aucune clé, de sorte qu'il ne reste rien pour la relier à la requête d'origine et elle se lit comme un appel sans rapport plutôt que comme une violation.
Observer depuis une autre machine
Gardez la capture sur la machine où le trafic se produit et utilisez SSH pour le saut réseau, afin que mcpsnoop n'ait jamais besoin de son propre transport distant.
Vue en direct
Lancez la TUI sur votre poste de travail et renvoyez le socket mcpsnoop de la machine distante vers celui-ci. Le tunnel en direct utilise le transfert de socket Unix via SSH, donc les deux extrémités doivent tourner sous Linux ou macOS. Sur Windows, utilisez la copie du journal post-mortem ci-dessous.```bash
on your workstation, start the TUI
mcpsnoop
create the remote socket directory once
ssh remote-user@remote-host 'mkdir -p ~/.local/state/mcpsnoop'
print the tunnel command, then run the printed ssh -R line
mcpsnoop remote remote-user@remote-host
on the remote host, wrap your server as usual
mcpsnoop -- node build/index.js
Le socket se trouve dans le répertoire d'état du distant, résolu comme `MCPSNOOP_HOME`,
sinon `XDG_STATE_HOME/mcpsnoop`, sinon `~/.local/state/mcpsnoop`. Par défaut, mcpsnoop
suppose le home Linux `/home/<user>` à partir de votre `user@host` et affiche un rappel
sur stderr chaque fois qu'il retombe sur cette supposition. Si le distant se résout ailleurs,
indiquez le seul élément non défini par défaut.```bash
# a non-Linux or custom home, macOS is /Users/<user> and root is /root
mcpsnoop remote --remote-home /Users/remote-user remote-user@remote-host
# an explicit MCPSNOOP_HOME on the remote
mcpsnoop remote --remote-mcpsnoop-home /srv/mcpsnoop remote-user@remote-host
# an explicit XDG_STATE_HOME on the remote
mcpsnoop remote --remote-xdg-state-home /var/lib/state remote-user@remote-host
Post-mortem
Diffusez une session distante directement dans la TUI via SSH, sans avoir besoin de copie locale.```bash ssh remote-user@remote-host 'cat ~/.local/state/mcpsnoop/sessions/session.jsonl' | mcpsnoop open -
Pour conserver plutôt une copie locale, scp les journaux dans votre répertoire de sessions et lancez
la TUI normalement.```bash
# copy the remote logs into your local sessions directory
mkdir -p ~/.local/state/mcpsnoop/sessions
scp remote-user@remote-host:'~/.local/state/mcpsnoop/sessions/*.jsonl' \
~/.local/state/mcpsnoop/sessions/
# open the TUI, it backfills the copied sessions
mcpsnoop
Security
mcpsnoop exécute la commande serveur que vous enveloppez, alors n’enveloppez que des serveurs de confiance et exécutez les serveurs non fiables dans un conteneur. Il n’exécute jamais rien que vous n’ayez pas placé dans la configuration de votre client.
Les trames capturées peuvent contenir des invites, des arguments d’outil, des identifiants et des résultats d’outil. Si les charges utiles peuvent transporter des secrets, activez la rédaction pour nettoyer les copies de trace observées tandis que les octets proxifiés continuent de passer inchangés.
La rédaction par clé remplace les valeurs entières sous les clés d’objet JSON correspondantes, et le même ensemble de clés est appliqué au mieux aux arguments de ligne de commande du serveur enveloppé, de sorte que --api-key=sk-x et --token sk-x sont masqués avec --redact-secrets. Un argument qui transporte un secret sans nom d’option reconnaissable ne peut pas être détecté.
La rédaction par chemin ne remplace que les valeurs sélectionnées par une expression JSONPath, ce qui est utile lorsqu’un nom de clé courant est sensible à un endroit mais sûr à un autre. Répétez --redact-path pour masquer plus d’un emplacement.
La rédaction basée sur les valeurs applique des expressions régulières aux valeurs de chaîne observées, au texte de stderr et aux trames texte non JSON.
Les trois sont au mieux. Les regex peuvent manquer des secrets, correspondre par erreur à du texte inoffensif, ou ne pas détecter les valeurs transformées ou encodées.
La rédaction ne se transforme jamais en accusation. Chaque vérification qui compare une chose observée à une autre — un en-tête de routage au corps, une valeur Mcp-Param à l’argument qu’elle reflète, le schéma d’un outil à ce que la révision exige de lui — sait quand mcpsnoop est le côté qui a réécrit les octets et reste silencieuse plutôt que de signaler un serveur en raison du paramètre de confidentialité de l’utilisateur. La dérive de définition d’outil fait exception, et c’est délibéré, car activer la rédaction modifie ce qui est enregistré et donc ce que contient une référence. Voir Detect tool definition drift.```bash
built-in preset of common secret keys
mcpsnoop --redact-secrets -- node build/index.js
or name your own keys
mcpsnoop --redact-key token,api_key,password -- node build/index.js
scrub one location without redacting every field named password
mcpsnoop --redact-path '$.params.arguments.password' -- node build/index.js
wildcards scrub every matching array element
mcpsnoop --redact-path '$.params.arguments.accounts[*].password' -- node build/index.js
scrub obvious token-shaped values outside known keys
mcpsnoop --redact-value 'sk-[A-Za-z0-9]+' -- node build/index.js
combine the layers in http mode
mcpsnoop http --target http://localhost:3000/mcp --redact-secrets --redact-value 'Bearer\s+\S+'
Pour les workflows à distance, utilisez le tunnel SSH ou le transfert de fichiers SSH afin que l'authentification du transport,
le chiffrement, la vérification de l'hôte, la rotation des clés et la politique d'audit restent dans votre
configuration SSH existante.
## Contribution
Les issues et les pull requests sont les bienvenues. Consultez [CONTRIBUTING.md](https://github.com/kerlenton/mcpsnoop/blob/HEAD/CONTRIBUTING.md) pour
les détails.
## Licence
[MIT](https://github.com/kerlenton/mcpsnoop/blob/HEAD/LICENSE)