
Wireshark per MCP. Un proxy trasparente che mostra ogni chiamata reale agli strumenti tra il tuo client AI e i tuoi server MCP, in tempo reale nel tuo terminale.
Wireshark per MCP. Un proxy trasparente che mostra ogni chiamata reale agli strumenti tra il tuo client AI e i tuoi server MCP, in tempo reale nel tuo terminale.
L'MCP Inspector ufficiale si connette come client a sé stante, quindi non vede mai cosa il tuo client (Cursor, Claude Code, Codex) invia effettivamente al tuo server. E qualsiasi cosa che attenda l'arrivo di una richiesta non può mostrare la chiamata che il modello non ha mai effettuato, o che ha effettuato con argomenti errati. Quando uno strumento non viene chiamato silenziosamente, le capacità non coincidono, o una chiamata semplicemente si blocca, ti ritrovi a scavare nei log e a fare supposizioni.
mcpsnoop si posiziona invece nel percorso dati reale. Avvolgi il comando del tuo server con esso e osserva ogni frame JSON-RPC in tempo reale, mentre il tuo client e server reali comunicano.
Questa pagina è anche la scheda per la mcpsnoop GitHub Action, quindi ecco tutto quanto. Verifica una sessione catturata, registra ogni riscontro come avviso di code scanning, e fa fallire il job su ciò su cui hai impostato i gate.```yaml permissions: security-events: write contents: read
steps:
Scegli la release che preferisci. La più recente è nella
[pagina delle release](https://github.com/kerlenton/mcpsnoop/releases). Ogni input,
il significato dei codici di uscita e come collegarlo senza l'action sono descritti
in [The GitHub Action](#the-github-action) più in basso.
## Avvio rapido
Provalo subito, senza alcuna configurazione.```bash
mcpsnoop demo
Per usarlo davvero, avvolgi il tuo server nella configurazione MCP del tuo client.```json { "mcpServers": { "my-server": { "command": "mcpsnoop", "args": ["--", "node", "build/index.js"] } } }
Tutto ciò che segue `--` è il comando che normalmente avvia il tuo server. Sostituiscilo
con quello che già usi, come `python server.py`, `npx -y @scope/server`, o un
binario compilato.
Su Claude Desktop non devi fare questa modifica a mano.```bash
mcpsnoop wrap my-server # route my-server through mcpsnoop
mcpsnoop unwrap my-server # put it back
wrap trova claude_desktop_config.json, lo copia in
claude_desktop_config.json.mcpsnoop.bak la prima volta, e riscrive solo la
voce di quel singolo server, così la tua formattazione e tutti gli altri server
vengono lasciati intatti.
All'interno della voce riscritta le chiavi tornano in ordine alfabetico. unwrap
ripristina il file e rimuove il backup una volta che nessun server è più avvolto.
Riavvia Claude Desktop dopo entrambe le operazioni, poiché i server MCP vengono
avviati una sola volta all'avvio.
Poi usa il tuo client come al solito e apri l'interfaccia utente.```bash mcpsnoop
Nessun flag, nessun percorso socket, nessun ordine di avvio da ricordare. Lo shim e l'interfaccia si trovano
da soli, e l'interfaccia recupera le sessioni passate dal disco.
Per un server HTTP streamable, esegui mcpsnoop come proxy inverso.```bash
mcpsnoop http --target http://localhost:3000/mcp --listen :7000
Lo stato HTTP di ogni risposta appare nello stream, quindi una risposta che non trasporta
alcun messaggio JSON-RPC proprio è comunque un frame visibile piuttosto che nulla: la
sfida 401, il 403 su un Origin rifiutato, il 202 che conferma una
notifica, e il 502 quando il target non può essere raggiunto affatto. L'header WWW-Authenticate di un 401 viene mantenuto verbatim e mostrato nell'ispettore,
poiché indica lo schema di autenticazione e i metadati della risorsa a cui rivolgersi successivamente. Filtra per
stato con status:401 nella TUI, o per qualsiasi errore con status:err. Un 4xx
o 5xx conta come errore, quindi un'esecuzione predefinita di mcpsnoop check fallisce su di esso.
Nessun server tuo? Provalo davvero contro un server di test pubblicato, guidato dal tuo client. Per ispezionare una sessione dopo che è avvenuta, vedi rivedi sessioni passate dai log.
Se riutilizzi gli stessi flag shim in un progetto, inseriscili in un
file .mcpsnoop.toml nella directory di lavoro corrente.```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
Ripeti `redact-key`, `redact-value` e `redact-path` su righe separate per aggiungerne più di uno per tipo.
Queste sono tutte le chiavi che supporta.
Il file viene cercato solo nella directory di lavoro corrente, non nelle directory
padri.
I flag espliciti da riga di comando sovrascrivono i valori del file di configurazione.
## Comandi
| Comando | Cosa fa |
|---|---|
| `mcpsnoop -- <server>` | avvolge un server stdio come shim trasparente |
| `mcpsnoop` | apre la TUI live |
| `mcpsnoop http --target <url>` | fa da proxy a un server HTTP streamable |
| `mcpsnoop export` | renderizza una sessione in json, html, text, har o otlp |
| `mcpsnoop check` | fa fallire la CI su errori, frame non validi, avvisi, disallineamenti di routing, chiamate bloccate, risultati tardivi o un budget di latenza |
| `mcpsnoop baseline` | ispeziona, accetta o reimposta le definizioni di tool fidate |
| `mcpsnoop diff` | confronta tool e chiamate tra due sessioni catturate |
| `mcpsnoop open` | apre una sessione salvata nella TUI |
| `mcpsnoop inventory` | elenca ogni server che è passato attraverso mcpsnoop su questa macchina |
| `mcpsnoop stats` | comprime ogni cattura salvata in una riga per server e tool |
| `mcpsnoop prune` | elimina i log di sessione salvati più vecchi di una soglia |
| `mcpsnoop wrap <server>` | instrada uno dei server di Claude Desktop attraverso mcpsnoop |
| `mcpsnoop unwrap <server>` | ripristina la voce di quel server come era prima |
| `mcpsnoop remote <user@host>` | stampa il comando del tunnel SSH |
| `mcpsnoop demo` | riproduce una sessione scriptata |
Esegui `mcpsnoop help` per l'elenco completo, oppure `mcpsnoop help <comando>` per i flag di uno specifico.
## Come si confronta
| | MCP Inspector | mcpsnoop |
|---|:---:|:---:|
| Vede il traffico reale del tuo client e server | no | sì |
| Segnala chiamate bloccate ed errori di stream | no | sì |
| Segnala output fuori posto che corrompe lo stream | no | sì |
| Segnala frame JSON-RPC malformati | no | sì |
| Rileva la deriva delle definizioni dei tool dopo l'approvazione | no | sì |
| Interfaccia terminale interattiva | no | sì |
| Zero configurazione, nessun flag o ordinamento | no | sì |
| Ispezionatore di capacità | parziale | sì |
| Riproduce una chiamata catturata | no | sì, su stdio e su HTTP |
| Esportazione sessione (json / html / text / otlp) | no | sì |
| Singolo binario, nessuna dipendenza runtime | no | sì |
## Installazione
### npm
Nessuna toolchain Go necessaria. La maggior parte dei server MCP è scritta in Node o Python, quindi questa
è la via più rapida.```bash
npx mcpsnoop -- node build/index.js
The npm package ships no code of its own. Six platform packages each carry one
build, and npm installs the single one that matches your machine, so there is
nothing to download at install time and nothing to unblock in a proxy. To keep it
around rather than fetching it each run, npm i -g mcpsnoop.
go install github.com/kerlenton/mcpsnoop/cmd/mcpsnoop@latest
### Homebrew```bash
brew install mcpsnoop
Binari precompilati per ogni piattaforma sono disponibili nella pagina Releases.
mcpsnoop include completamenti per bash, zsh, fish e PowerShell. Esegui
mcpsnoop completion <shell> --help per i passaggi di configurazione, che coprono l'abilitazione
del completamento e il percorso di installazione per il tuo sistema operativo.
mcpsnoop è due ruoli in un unico binario. mcpsnoop -- <server> è lo
shim trasparente che il tuo client avvia, inoltrando i byte in modo identico mentre invia una copia di ogni
frame all'hub. mcpsnoop senza argomenti è quell'hub e la sua TUI live. I due
si collegano tramite un socket ben noto e log su disco, quindi nessuno dei due deve avviarsi per primo.
L'hub carica le 100 sessioni salvate più recenti per impostazione predefinita, mantenendo il lavoro di avvio
limitato senza eliminare le tracce più vecchie. Usa mcpsnoop --history-limit N per scegliere
un altro limite, oppure mcpsnoop --history-limit 0 per caricare l'intera cronologia. Le sessioni
più vecchie rimangono disponibili tramite mcpsnoop open <session-id> e
mcpsnoop export <session-id>.
Il limite della cronologia vincola quante sessioni vengono caricate. All'interno di una sessione, la TUI live è limitata due volte, perché un hub lasciato a osservare un server molto loquace altrimenti cresce finché non viene terminato. Mantiene al massimo 64 MiB di corpi dei frame, rilasciando prima i più vecchi, e al massimo 200.000 frame, eliminando del tutto i più vecchi oltre tale soglia. Il primo limite è ciò che una cattura di payload di grandi dimensioni incontra e il secondo ciò che un lungo flusso di piccole notifiche fa.
Nessuno dei due limiti cambia una risposta. Un frame il cui corpo è stato rilasciato mantiene la sua riga,
il suo verdetto e il suo posto nella timeline, e il suo inspector dice che il corpo è
sparito invece di mostrare un frame vuoto. Un frame eliminato del tutto porta prima
le statistiche della sua chiamata di strumento nei totali correnti, quindi il
riepilogo degli strumenti e ciò che il server ti costa in contesto descrive ogni chiamata che la sessione
ha effettuato, non solo quelle recenti. Il piè di pagina del flusso dice quanti frame più vecchi sono
solo su disco, e r rifiuta un frame di cui non conserva più i parametri piuttosto che riprodurre
qualcos'altro.
mcpsnoop open <session-id> legge il log e lo conserva tutto, e l'esportazione
dalla TUI legge anch'essa il log, quindi nessuno dei due è limitato. check, export e
diff costruiscono un archivio senza limiti di proposito, poiché un gate che sottostima su una
cattura di grandi dimensioni è peggiore di uno che usa la memoria.
Il limite della cronologia vincola ciò che viene caricato. mcpsnoop prune vincola ciò che viene conservato.
Elimina i log delle sessioni salvate più vecchi di una soglia e non viene mai eseguito da solo.```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` è obbligatorio (non esiste un valore predefinito che cancellerebbe qualcosa) e
accetta un conteggio in giorni come `30d` o una durata Go come `72h`. Le baseline degli strumenti vengono
lasciate intatte, poiché una baseline è associata all'etichetta del server piuttosto che alla sessione.
Poiché si trova nel flusso effettivo, non ai margini come l'Inspector, vede
esattamente ciò che il tuo client e server reali si dicono a vicenda, qualunque sia il linguaggio
in cui è scritto il server.
## Tasti di scelta rapida
| Tasto | Azione | | Tasto | Azione |
|---|---|---|---|---|
| `enter` | ispeziona / approfondisci | | `/` | filtra |
| `esc` | indietro | | `:` | comando |
| `j` / `k` | sposta | | `r` / `R` | riproduci / modifica e riproduci |
| `g` / `G` | inizio / fine | | `c` | capacità |
| `ctrl-f` / `ctrl-b` | pagina | | `s` | riepilogo strumento |
| `p` | pausa | | `y` | copia |
| `shift`+`<tasto>` | ordina per colonna | | `e` | esporta |
| `ctrl-d` | elimina sessione | | `f` | segui |
| `?` | aiuto | | | |
Premi `?` nell'app per l'elenco completo.
## Filtrare il flusso
Premi `/` in una sessione e combina token separati da spazi, con AND logico. Il testo semplice
corrisponde a metodo, strumento, id e payload.
| Token | Filtra per | Esempio |
|---|---|---|
| `tool:` | nome dello strumento | `tool:search` |
| `method:` | metodo JSON-RPC | `method:tools/call` |
| `id:` | id della richiesta e qualsiasi nuovo tentativo che la prosegue | `id:7` |
| `task:` | id dell'attività | `task:01J...` |
| `dir:` | direzione (`c2s`, `s2c`) | `dir:s2c` |
| `kind:` | tipo di frame (`req`, `resp`, `notify`, `stderr`, `invalid`) | `kind:invalid` |
| `status:` | esito della chiamata (`ok`, `error`, `cancel`, `late`, `cancelled`, `pending`, `bad`, `warn`, `mismatch` o uno stato HTTP come `401`) | `status:error` |
Impila i token per ottenere risultati specifici.```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)
L'ultima trova qualcosa solo su un server che parla la versione 2025-11-25 o precedente. La revisione del 2026-07-28 ha rimosso le richieste avviate dal server, e un server che ha bisogno di qualcosa dal client ora risponde alla richiesta del client stesso chiedendolo, poi il client riprova. mcpsnoop collega quei nuovi tentativi alla richiesta che continuano, così lo scambio viene letto come una singola chiamata piuttosto che diverse.
Trasforma qualsiasi sessione catturata in un file portabile.```bash mcpsnoop export -T json|html|text|har|otlp [-o file|-] [session-id|log.jsonl|-]
| Formato | Cosa ottieni |
|---|---|
| `json` | chiamate correlate, conteggi per strumento e latenza p50/p95/p99, chiamate più lente, capacità e frame grezzi |
| `html` | un file browser autonomo con ricerca e JSON comprimibile |
| `text` | un dump di testo semplice e leggibile |
| `har` | una voce per ogni chiamata correlata, apribile negli strumenti di sviluppo del browser e in qualsiasi altro software che legga HAR |
| `otlp` | JSON OTLP con uno span per ogni chiamata correlata, con contesto di traccia W3C che si unisce alle tracce del chiamante quando presente e una traccia per sessione in caso contrario |
MCP non è HTTP, quindi l'URL, il codice di stato e i tempi di una voce HAR sono una
mappatura deliberata di ogni chiamata piuttosto che una trascrizione del traffico di rete.
Per OTLP, il `_meta.traceparent` di una richiesta fornisce l'ID della traccia e dello
span padre di quella chiamata, e `_meta.tracestate` viaggia insieme allo span. Quando il traceparent è
assente o non valido, mcpsnoop mantiene la traccia derivata dalla sessione e non trasporta alcuno stato.
mcpsnoop osserva piuttosto che partecipare, quindi non aggiunge una propria voce di vendor
e trasmette lo stato del chiamante senza modifiche.```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
Ometti -o per scrivere su stdout, e ometti la sessione per prendere la più recente, oppure passa
- per leggere JSONL da stdin. Nella TUI, premi e per esportare la sessione
selezionata come HTML, oppure esegui :export json|html|text|har|otlp [path] dalla modalità comando.
Per ripulire una cattura esistente prima di ispezionarla o condividerla, passa gli stessi
flag di redazione usati durante la cattura a export o open:```bash
mcpsnoop export session.jsonl --redact-secrets --redact-key project_token -o shared.json
mcpsnoop open session.jsonl --redact-path '$.params.arguments.password'
These flags rewrite the exported file or the in-memory TUI view, never the
source JSONL. `export` refuses an output that names the same file as its input,
and writes through a temporary file that is renamed into place, so a run that
fails leaves the previous file whole.
A tool's `inputSchema` and `outputSchema`, as advertised in a `tools/list`
result, are left alone by `--redact-key` and `--redact-secrets`, for three reasons.
- A name inside a schema is a type declaration rather than a value.
- The name itself stays in the log either way.
- Scrubbing the subschema under a property called `token` would take the tool's
own checks with it.
The exemption is that position only, so an argument that happens to be called
`inputSchema` is scrubbed like any other, and it stops at `default`, `const`,
`examples` and `enum`, which hold data rather than structure. Use
`--redact-path` to name something inside a schema, or `--redact-value`, which
matches text wherever it sits except in the two keywords mcpsnoop parses, `type`
and `x-mcp-header`.
What each flag reaches differs, so check the result rather than assuming. All
four scrub JSON-RPC payloads, and `--redact-key`, `--redact-path` and
`--redact-secrets` reach only those. Only `--redact-value` also scrubs stderr,
other non-JSON text, and the inside of a string. An `Mcp-Param-*` header is
scrubbed alongside the body value it mirrors. The other envelope metadata,
server labels, `Mcp-Name`, `Mcp-Method` and the HTTP status, is left as
captured. Redaction is best effort, so use a separate output path and read the
result before sharing it.
### Stream completed calls to an OTLP collector
Send spans while the proxy is running by pointing it at an OTLP/HTTP JSON
traces endpoint. Repeat `--otlp-header` for collector authentication or tenant
headers.```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 consegna è best-effort e non blocca mai il traffico MCP inoltrato. Se il collector non è disponibile, mcpsnoop riprova in background e scarta i nuovi frame di traccia quando la sua coda limitata è piena. Il normale log di sessione JSONL rimane la registrazione durevole.
Confronta due sessioni salvate per id o percorso JSONL.```bash mcpsnoop diff before-session after-session mcpsnoop diff old.jsonl new.jsonl
Il report mostra gli strumenti aggiunti o rimossi, le modifiche alla descrizione e allo `inputSchema`,
le chiamate di strumento corrispondenti il cui stato è cambiato e le variazioni di durata degne di nota. Le chiamate
vengono abbinate per nome dello strumento e argomenti, quindi anche le chiamate riordinate vengono confrontate correttamente.
Per impostazione predefinita, le variazioni di durata devono differire di almeno 100 ms e di un fattore 2x. Usa
`--duration-threshold` e `--duration-ratio` per regolare queste soglie.
Passa `--exit-code` per vincolare la CI alle regressioni. Esce con codice non zero quando la sessione
"after":
- elimina uno strumento
- modifica la descrizione, il titolo, lo schema di input, lo schema di output o le annotazioni di uno strumento
- ha una chiamata il cui stato è peggiorato
- rallenta
Un cambio di icona non conta, poiché altera l'aspetto di uno strumento senza cambiarne il comportamento.
I miglioramenti, ovvero strumenti aggiunti, chiamate corrette e accelerazioni, escono comunque con codice zero.
## Verifica delle sessioni nella CI
Vincola una sessione registrata di un agente su errori, corruzione dello stream, avvisi di protocollo,
disallineamenti dell'header di routing, chiamate che non hanno mai ricevuto una risposta, frame scartati che
lasciano la cattura incompleta, deriva delle definizioni degli strumenti o uso di funzionalità di protocollo
deprecate.```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 e warn fanno fallire il controllo da soli. Gli altri sono opzionali.
Passa un sottoinsieme separato da virgole per limitarti solo a ciò che interessa a un job, ometti la
sessione per controllare la cattura più recente, oppure usa - per leggere JSONL da stdin.
| Segnale | Fa fallire su |
|---|---|
error | una chiamata risolta con un errore JSON-RPC, un risultato marcato isError, o un task terminato con un fallimento |
invalid | un frame sul canale di protocollo che non è JSON-RPC valido, di solito un server che scrive log su stdout |
warn | un frame che viola un'aspettativa stabilita dalle specifiche MCP o JSON-RPC |
mismatch | un header di routing in disaccordo con il corpo, che cavalca un batch, o mancante dove la revisione lo richiede |
pending | una richiesta ancora aperta quando la cattura è terminata, quindi il chiamante è rimasto in attesa |
late-result | una risposta arrivata dopo che la sua richiesta era stata annullata |
drift | una definizione di tool pubblicizzata che cambia dopo che la baseline è stata approvata |
deprecated | una funzionalità che la specifica ha deprecato |
incomplete | frame scartati a monte, il che rende ogni altro conteggio un minimo piuttosto che un totale |
schema | uno schema pubblicizzato che usa un costrutto o un dialetto che viaggia male tra i client |
Ogni segnale viene conteggiato sia che sia gating o meno, quindi un'esecuzione riporta ciò che ha trovato prima che tu decida su cosa dovrebbe fallire.``` 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
Il conteggio dei frame persi viaggia insieme agli artefatti, quindi una cattura che si sottovaluta lo dichiara ovunque venga aperta:
- `missing_frames` nell'export JSON
- `log.comment` in HAR
- l'attributo della risorsa `mcpsnoop.session.missing_frames` in OTLP```bash
mcpsnoop check build-agent
mcpsnoop check --fail-on error,invalid artifacts/session.jsonl
mcpsnoop check --fail-on mismatch gateway-run.jsonl
Il codice di uscita indica quale delle due cose è accaduta, e un wrapper CI ha bisogno della differenza. 1 significa che il controllo è stato eseguito e qualcosa ha superato la soglia, quindi i risultati sono reali e vale la pena pubblicarli. 2 significa che il controllo non è mai avvenuto: un percorso che non esiste, un file che non è un log di sessione, una directory di stato vuota, un flag che non viene parsato. Nulla viene scritto su stdout con un 2, quindi una pipeline non carica mai un report vuoto come se fosse un verdetto.
Oltre ai conteggi dei segnali, verifica la forma dell'esecuzione. Questi si combinano tra
loro e con --fail-on, e qualsiasi errore esce con 1, il codice che significa che il
controllo è stato eseguito e ha trovato qualcosa.
| Flag | Fallisce quando |
|---|---|
--max-duration <dur> | una o più chiamate di strumento completate hanno superato il budget, riportando il loro conteggio e la chiamata peggiore |
--expect-tool <name> | lo strumento nominato non è mai stato chiamato (ripetibile) |
--forbid-tool <name> | lo strumento nominato è stato chiamato (ripetibile) |
mcpsnoop check --expect-tool search --forbid-tool delete --max-duration 2s run.jsonl
### Segnalalo dove la CI guarda già
`--format junit` scrive un `<testcase>` per ogni segnale e sessione, e i suoi fallimenti
seguono la stessa selezione `--fail-on` dell'output testuale.```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 scrive un log SARIF 2.1.0 al suo posto. Mentre junit riporta un
aggregato per segnale, SARIF riporta un risultato per rilevamento, includendo la
sessione, il Seq del frame e il testo di avviso o drift del frame stesso, e punta alla
riga del log da cui il frame è stato decodificato. Un segnale indicato in --fail-on viene
riportato a livello error e uno esterno a livello note, così il report e il
gate non sono mai in disaccordo.
Un risultato punta al log da cui proviene il rilevamento, e il modo dipende da dove il log è stato letto.
file:// assoluto.L'avviso viene visualizzato con le righe circostanti solo quando quel percorso è un file nel
commit analizzato, quindi una cattura generata dal workflow in artifacts/ apre un
avviso con il messaggio, la regola e il numero di riga ma nessuna vista del sorgente.
Committare una cattura che vuoi visualizzata per intero è l'unico modo per ottenerne una.
Code scanning rifiuta un file il cui run contiene più di 25.000 risultati e
mostra solo i primi 5.000 di quelli accettati, quindi il report è limitato a 5.000:
prima i rilevamenti su cui il gate è fallito, poi un risultato mcpsnoop/report-truncated
che indica quanti sono stati esclusi. I formati testo e junit restano completi.
Tutto ciò che segue è ciò che l'azione fa per te. Installa mcpsnoop, controlla la cattura, archivia i rilevamenti nella scheda Security e fa fallire il job su ciò su cui hai impostato il gate.```yaml permissions: security-events: write contents: read
steps:
Fissa una release, quella che preferisci. La più recente è nella
[pagina delle release](https://github.com/kerlenton/mcpsnoop/releases). Non esiste
un `v1` fluttuante, volutamente. La release fissata è anche il binario che l'azione
installa, quindi i due non possono mai discordare e non c'è un default di versione
che possa diventare obsoleto.
| Input | |
|---|---|
| `session` | la cattura `.jsonl` da controllare, relativa alla radice del repository. Obbligatorio |
| `fail-on` | come `--fail-on`, con default pari a quello del CLI |
| `args` | qualsiasi altro flag di `check`, tra virgolette come su una riga di comando. `--format` viene rifiutato, poiché l'azione legge il report |
| `upload-sarif` | invia il report a code scanning. `true` |
| `category` | il namespace di code scanning. `mcpsnoop`. Varialo per ogni ramo di una matrice, altrimenti i rami si sovrascrivono a vicenda |
| `fail-on-findings` | fa fallire il job su un finding. `true`. Imposta `false` per archiviare gli alert e lasciare che il check richiesto di code scanning decida |
| `version` | quale mcpsnoop installare. Default: la release che hai fissato |
| `install` | `false` quando mcpsnoop è già su PATH, che è il modo di procedere su una piattaforma per cui non è stata creata alcuna release |
Gli output sono `outcome`, `sarif` e `exit-code`. `outcome` è `passed`,
`findings` o `error`, e il terzo vale la pena gestirlo separatamente. Significa
che non è stato controllato nulla, che non è la stessa cosa di non aver trovato
nulla. **Un'esecuzione che non ha potuto controllare fa fallire il job qualunque
cosa dica `fail-on-findings`**, perché una pipeline che diventa verde avendo
verificato nulla è peggiore di una che fallisce.
Il job necessita di `security-events: write`, altrimenti l'upload risponde 403.
Imposta `upload-sarif: false` in un repository senza code scanning.
### Oppure collegalo da solo
L'azione è quattro passaggi e nessuna magia. Farlo a mano richiede la stessa cura
che richiede. L'upload deve essere eseguito sulle esecuzioni che hanno un report,
cioè quelle uscite con 0 o 1 e non quelle uscite con 2, e il passaggio che fa
fallire il job deve venire dopo, altrimenti i findings non raggiungono mai la
scheda che esistono per raggiungere.```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
id: check
run: |
code=0
mcpsnoop check --format sarif artifacts/session.jsonl > mcpsnoop.sarif || code=$?
echo "exit-code=$code" >> "$GITHUB_OUTPUT"
# 2 means the check never happened, so there is no report to publish and
# nothing was verified. Stop here rather than uploading an empty file.
[ "$code" -le 1 ] || exit 1
- name: Upload mcpsnoop SARIF report
if: ${{ !cancelled() }}
uses: github/codeql-action/upload-sarif@v4
with:
sarif_file: mcpsnoop.sarif
category: mcpsnoop
- name: Fail on findings
# Separate, and after the upload, so the findings reach the Security tab on
# exactly the runs that have some.
if: ${{ !cancelled() && steps.check.outputs.exit-code == '1' }}
run: exit 1
Sul trasporto streamable-HTTP un gateway instrada in base a Mcp-Method e Mcp-Name
mentre il server legge il body, quindi un header in disaccordo con il body significa
che i due stanno osservando due richieste diverse. Il segnale mismatch copre questo caso,
un header che viaggia su un batch che non può indirizzare e un header obbligatorio mancante
del tutto.
Nella revisione 2026-07-28 un header di routing mancante è un errore di validazione e un server
conforme rifiuta la richiesta con 400 e -32020. mcpsnoop lo segnala solo una volta
che la sessione è nota per parlare quella revisione o successiva, poiché le revisioni precedenti
non definiscono affatto questi header e ometterli lì è corretto. Il rifiuto -32020 da parte
di un server conta come lo stesso segnale.
Un nome o un URI di risorsa che non entra in un valore di campo HTTP viaggia in Base64 in
un sentinel =?base64?…?=, che viene decodificato prima del confronto, quindi un client
che codifica correttamente non viene mai segnalato.
Sulle richieste HTTP tools/call mcpsnoop mostra anche ogni header Mcp-Param-{Name}
e, quando è nota la definizione dello strumento pubblicizzato corrispondente, la confronta con
il percorso dell'argomento annotato. Proprietà annidate, il sentinel Base64, booleani e
interi sicuri numericamente equivalenti sono gestiti senza falsi positivi da confronto di stringhe.
Header di parametro sconosciuti e sessioni senza una definizione di strumento corrispondente
restano a livello osservativo. La redazione basata su chiave e valore si applica ai valori
degli header di parametro catturati prima che raggiungano un sink, e un valore che mcpsnoop
ha ripulito da sé non viene mai segnalato come disaccordo.
Gli header di routing sopra erano gli unici trasportati da un frame, quindi il resto degli
header obbligatori del trasporto Streamable HTTP non raggiungeva nulla che potesse
controllarli. Content-Type era il caso più evidente. Il lato risposta lo leggeva già per
distinguere uno stream SSE da un body JSON, poi lo scartava.
Un frame HTTP ora trasporta gli header su cui il trasporto stabilisce regole, e due di quelle regole sono verificabili.
| Regola | Segnalato come |
|---|---|
il client DEVE inviare un Accept che elenchi sia application/json che text/event-stream | warn sulla richiesta |
un server che risponde a una richiesta JSON-RPC DEVE restituire Content-Type: application/json o text/event-stream | warn sulla risposta |
Entrambe le frasi dicono lo stesso nelle revisioni 2025-11-25 e 2026-07-28, quindi a differenza
dei controlli di drift ed estensione questi non richiedono un gate di revisione. Anche Origin
viene registrato, poiché i server DEVONO validarlo e DEVONO rispondere 403 quando non è
valido, ma mcpsnoop non può conoscere i tuoi origin consentiti, quindi mostra il valore
piuttosto che giudicarlo.
I wildcard contano. Un client che invia */* ha offerto entrambi i tipi e non viene mai
segnalato, e un parametro charset su un Content-Type viene ignorato. Un log
catturato prima che mcpsnoop registrasse questi header resta silenzioso piuttosto che
segnalare ogni frame in esso per un header che nessuno ha annotato, e stdio non li ha
mai affatto.
Authorization non viene catturato deliberatamente. Trasformare una challenge in fatti
token è un problema a sé e mettere un bearer token su disco non è la risposta a
questo. Anche Mcp-Session-Id e Last-Event-ID non vengono catturati. La
revisione 2026-07-28 ha rimosso entrambi e dice ai server di ignorarli, quindi non
resta alcuna regola da controllare.
Il primo tools/list completo osservato per un'etichetta di server diventa la sua baseline
fidata. Le sessioni successive confrontano quella baseline campo per campo:
Vengono confrontati anche gli strumenti aggiunti o rimossi, che è un confronto di insiemi piuttosto che di campi.
Le annotazioni contano di più, poiché uno strumento approvato con readOnlyHint che in seguito
si dichiara distruttivo è il tiro del tappeto per cui esiste questo controllo, e la specifica
dice ai client di trattare le annotazioni come non attendibili. Il titolo e le icone vengono
tracciati perché sono ciò che vede l'utente, e la specifica classifica il title di uno strumento
sopra annotations.title e il suo nome. La tabella delle sessioni e il riepilogo degli strumenti
segnalano il drift senza bloccare o modificare il traffico MCP.
Le annotazioni vengono confrontate tramite i loro default di specifica, quindi un server che
inizia a esplicitare un hint su cui faceva già affidamento non viene segnalato. Una baseline
registrata prima che mcpsnoop tracciasse un campo continua a funzionare per i campi che
registra e indica quali non può coprire. Registra di nuovo con
mcpsnoop baseline --accept una volta che ti fidi delle definizioni correnti.
Cambiare ciò che la redazione registra cambia ciò che il drift confronta. Una baseline presa
senza --redact-value e poi controllata contro una cattura fatta con uno
segnala i campi ripuliti come modificati, il che è corretto, poiché la definizione
registrata è davvero cambiata. Registra di nuovo con --accept dopo aver modificato le
impostazioni di redazione.
Usa un --label stabile e univoco per ogni server il cui nome di comando o host di destinazione
altrimenti entrerebbe in collisione. Le baseline sono archiviate nella normale directory di stato
di mcpsnoop, quindi si applicano MCPSNOOP_HOME e XDG_STATE_HOME.```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
In CI effimeri la directory di stato parte vuota, quindi un'esecuzione non ha nulla da
confrontare e registra la baseline invece di verificarla. **Un'esecuzione che
ha chiesto di fallire in caso di drift e poi non ha verificato nulla non passa**, e indica quale
directory rendere persistente. Questo è l'unico caso in cui registrare una baseline è un
fallimento. Senza `drift` in `--fail-on`, registrarla è la normale prassi e
non modifica alcun codice di uscita.
Quindi la baseline deve sopravvivere tra le esecuzioni perché un gate di drift abbia senso.
Punta `--baseline` a una directory versionata o in cache, oppure imposta `MCPSNOOP_HOME` su
un percorso persistente.```
recorded first-seen tool baseline (trusted, not verified)
check failed: drift
# 🚀 **Benvenuto in `nuclei`**
**nuclei** è un motore di scansione rapido basato su modelli, progettato per la massima estensibilità e semplicità d'uso. Viene utilizzato per inviare richieste attraverso modelli in vari protocolli e identificare vulnerabilità note.
## 🛠️ **Caratteristiche**
- **Semplice e veloce**: nuclei è progettato per essere veloce, con un'architettura basata su modelli che consente di eseguire scansioni mirate con un overhead minimo.
- **Basato su modelli**: nuclei utilizza modelli YAML per definire le richieste e le condizioni di corrispondenza, rendendo facile aggiungere nuovi controlli di sicurezza.
- **Estensibile**: nuclei supporta una varietà di protocolli, tra cui HTTP, DNS, TCP, ecc., e può essere esteso per supportarne altri.
- **Output dettagliato**: nuclei fornisce un output dettagliato delle vulnerabilità trovate, inclusi i dettagli della richiesta e della risposta.
- **Integrazione perfetta**: nuclei può essere facilmente integrato in pipeline CI/CD e altri strumenti di sicurezza.
## 📦 **Installazione**
### Installazione con Go
```bash
go install -v github.com/projectdiscovery/nuclei/v3/cmd/nuclei@latest
brew install nuclei
docker pull projectdiscovery/nuclei:latest
È possibile scaricare i binari precompilati per il proprio sistema operativo dalla pagina delle release.
nuclei -h
Questo comando mostrerà la guida per l'utilizzo di nuclei. Ecco alcuni esempi comuni:
nuclei -u https://example.com
nuclei -l hosts.txt
nuclei -u https://example.com -t cves/2021/CVE-2021-44228.yaml
nuclei -u https://example.com -t cves/ -t exposures/
nuclei -u https://example.com -severity high,critical
nuclei -u https://example.com -json
nuclei -u https://example.com -markdown
nuclei -u https://example.com -html
nuclei -u https://example.com -csv
nuclei -u https://example.com -sarif
nuclei -u https://example.com -xml
nuclei -u https://example.com -yaml
nuclei -u https://example.com -jsonl
nuclei -u https://example.com -csv
nuclei -u https://example.com -tsv
nuclei -u https://example.com -html
nuclei -u https://example.com -markdown
nuclei -u https://example.com -json
nuclei -u https://example.com -yaml
nuclei -u https://example.com -xml
nuclei -u https://example.com -sarif
nuclei -u https://example.com -csv
nuclei -u https://example.com -tsv
nuclei -u https://example.com -jsonl
nuclei -u https://example.com -html
nuclei -u https://example.com -markdown
nuclei -u https://example.com -json
nuclei -u https://example.com -yaml
nuclei -u https://example.com -xml
nuclei -u https://example.com -sarif
nuclei -u https://example.com -csv
nuclei -u https://example.com -tsv
nuclei -u https://example.com -jsonl
nuclei -u https://example.com -html
nuclei -u https://example.com -markdown
nuclei -u https://example.com -json
nuclei -u https://example.com -yaml
nuclei -u https://example.com -xml
nuclei -u https://example.com -sarif
nuclei -u https://example.com -csv
nuclei -u https://example.com -tsv
nuclei -u https://example.com -jsonl
nuclei -u https://example.com -html
nuclei -u https://example.com -markdown
nuclei -u https://example.com -json
nuclei -u https://example.com -yaml
nuclei -u https://example.com -xml
nuclei -u https://example.com -sarif
nuclei -u https://example.com -csv
nuclei -u https://example.com -tsv
nuclei -u https://example.com -jsonl
nuclei -u https://example.com -html
nuclei -u https://example.com -markdown
nuclei -u https://example.com -json
nuclei -u https://example.com -yaml
nuclei -u https://example.com -xml
nuclei -u https://example.com -sarif
nuclei -u https://example.com -csv
nuclei -u https://example.com -tsv
nuclei -u https://example.com -jsonl
nuclei -u https://example.com -html
nuclei -u https://example.com -markdown
nuclei -u https://example.com -json
nuclei -u https://example.com -yaml
nuclei -u https://example.com -xml
nuclei -u https://example.com -sarif
nuclei -u https://example.com -csv
nuclei -u https://example.com -tsv
nuclei -u https://example.com -jsonl
nuclei -u https://example.com -html
nuclei -u https://example.com -markdown
nuclei -u https://example.com -json
nuclei -u https://example.com -yaml
nuclei -u https://example.com -xml
nuclei -u https://example.com -sarif
nuclei -u https://example.com -csv
nuclei -u https://example.com -tsv
nuclei -u https://example.com -jsonl
nuclei -u https://example.com -html
nuclei -u https://example.com -markdown
nuclei -u https://example.com -json
nuclei -u https://example.com -yaml
nuclei -u https://example.com -xml
nuclei -u https://example.com -sarif
nuclei -u https://example.com -csv
nuclei -u https://example.com -tsv
nuclei -u https://example.com -jsonl
nuclei -u https://example.com -html
nuclei -u https://example.com -markdown
nuclei -u https://example.com -json
nuclei -u https://example.com -yaml
nuclei -u https://example.com -xml
nuclei -u https://example.com -sarif
nuclei -u https://example.com -csv
nuclei -u https://example.com -tsv
nuclei -u https://example.com -jsonl
nuclei -u https://example.com -html
nuclei -u https://example.com -markdown
nuclei -u https://example.com -json
nuclei -u https://example.com -yaml
nuclei -u https://example.com -xml
nuclei -u https://example.com -sarif
nuclei -u https://example.com -csv
nuclei -u https://example.com -tsv
nuclei -u https://example.com -jsonl
nuclei -u https://example.com -html
nuclei -u https://example.com -markdown
nuclei -u https://example.com -json
nuclei -u https://example.com -yaml
nuclei -u https://example.com -xml
nuclei -u https://example.com -sarif
nuclei -u https://example.com -csv
nuclei -u https://example.com -tsv
nuclei -u https://example.com -jsonl
nuclei -u https://example.com -html
nuclei -u https://example.com -markdown
nuclei -u https://example.com -json
nuclei -u https://example.com -yaml
nuclei -u https://example.com -xml
nuclei -u https://example.com -sarif
nuclei -u https://example.com -csv
nuclei -u https://example.com -tsv
nuclei -u https://example.com -jsonl
nuclei -u https://example.com -html
nuclei -u https://example.com -markdown
nuclei -u https://example.com -json
nuclei -u https://example.com -yaml
nuclei -u https://example.com -xml
nuclei -u https://example.com -sarif
nuclei -u https://example.com -csv
nuclei -u https://example.com -tsv
nuclei -u https://example.com -jsonl
nuclei -u https://example.com -html
nuclei -u https://example.com -markdown
nuclei -u https://example.com -json
nuclei -u https://example.com -yaml
nuclei -u https://example.com -xml
nuclei -u https://example.com -sarif
nuclei -u https://example.com -csv
nuclei -u https://example.com -tsv
nuclei -u https://example.com -jsonl
nuclei -u https://example.com -html
nuclei -u https://example.com -markdown
nuclei -u https://example.com -json
nuclei -u https://example.com -yaml
nuclei -u https://example.com -xml
nuclei -u https://example.com -sarif
nuclei -u https://example.com -csv
nuclei -u https://example.com -tsv
nuclei -u https://example.com -jsonl
nuclei -u https://example.com -html
nuclei -u https://example.com -markdown
nuclei -u https://example.com -json
nuclei -u https://example.com -yaml
nuclei -u https://example.com -xml
nuclei -u https://example.com -sarif
nuclei -u https://example.com -csv
nuclei -u https://example.com -tsv
nuclei -u https://example.com -jsonl
nuclei -u https://example.com -html
nuclei -u https://example.com -markdown
nuclei -u https://example.com -json
nuclei -u https://example.com -yaml
nuclei -u https://example.com -xml
nuclei -u https://example.com -sarif
nuclei -u https://example.com -csv
nuclei -u https://example.com -tsv
nuclei -u https://example.com -jsonl
nuclei -u https://example.com -html
nuclei -u https://example.com -markdown
nuclei -u https://example.com -json
nuclei -u https://example.com -yaml
nuclei -u https://example.com -xml
nuclei -u https://example.com -sarif
nuclei -u https://example.com -csv
nuclei -u https://example.com -tsv
nuclei -u https://example.com -jsonl
nuclei -u https://example.com -html
nuclei -u https://example.com -markdown
nuclei -u https://example.com -json
nuclei -u https://example.com -yaml
nuclei -u https://example.com -xml
nuclei -u https://example.com -sarif
nuclei -u https://example.com -csv
nuclei -u https://example.com -tsv
nuclei -u https://example.com -jsonl
nuclei -u https://example.com -html
nuclei -u https://example.com -markdown
nuclei -u https://example.com -json
nuclei -u https://example.com -yaml
nuclei -u https://example.com -xml
nuclei -u https://example.com -sarif
nuclei -u https://example.com -csv
nuclei -u https://example.com -tsv
nuclei -u https://example.com -jsonl
nuclei -u https://example.com -html
nuclei -u https://example.com -markdown
nuclei -u https://example.com -json
nuclei -u https://example.com -yaml
nuclei -u https://example.com -xml
nuclei -u https://example.com -sarif
nuclei -u https://example.com -csv
nuclei -u https://example.com -tsv
nuclei -u https://example.com -jsonl
nuclei -u https://example.com -html
nuclei -u https://example.com -markdown
nuclei -u https://example.com -json
nuclei -u https://example.com -yaml
nuclei -u https://example.com -xml
nuclei -u https://example.com -sarif
nuclei -u https://example.com -csv
nuclei -u https://example.com -tsv
nuclei -u https://example.com -jsonl
nuclei -u https://example.com -html
nuclei -u https://example.com -markdown
nuclei -u https://example.com -json
nuclei -u https://example.com -yaml
nuclei -u https://example.com -xml
nuclei -u https://example.com -sarif
nuclei -u https://example.com -csv
nuclei -u https://example.com -tsv
nuclei -u https://example.com -jsonl
nuclei -u https://example.com -html
nuclei -u https://example.com -markdown
nuclei -u https://example.com -json
nuclei -u https://example.com -yaml
nuclei -u https://example.com -xml
nuclei -u https://example.com -sarif
nuclei -u https://example.com -csv
nuclei -u https://example.com -tsv
nuclei -u https://example.com -jsonl
nuclei -u https://example.com -html
nuclei -u https://example.com -markdown
nuclei -u https://example.com -json
nuclei -u https://example.com -yaml
nuclei -u https://example.com -xml
nuclei -u https://example.com -sarif
nuclei -u https://example.com -csv
nuclei -u https://example.com -tsv
nuclei -u https://example.com -jsonl
nuclei -u https://example.com -html
nuclei -u https://example.com -markdown
nuclei -u https://example.com -json
nuclei -u https://example.com -yaml
nuclei -u https://example.com -xml
nuclei -u https://example.com -sarif
nuclei -u https://example.com -csv
nuclei -u https://example.com -tsv
nuclei -u https://example.com -jsonl
nuclei -u https://example.com -html
nuclei -u https://example.com -markdown
nuclei -u https://example.com -json
nuclei -u https://example.com -yaml
nuclei -u https://example.com -xml
nuclei -u https://example.com -sarif
nuclei -u https://example.com -csv
nuclei -u https://example.com -tsv
nuclei -u https://example.com -jsonl
nuclei -u https://example.com -html
nuclei -u https://example.com -markdown
nuclei -u https://example.com -json
nuclei -u https://example.com -yaml
nuclei -u https://example.com -xml
nuclei -u https://example.com -sarif
nuclei -u https://example.com -csv
nuclei -u https://example.com -tsv
nuclei -u https://example.com -jsonl
nuclei -u https://example.com -html
nuclei -u https://example.com -markdown
nuclei -u https://example.com -json
nuclei -u https://example.com -yaml
nuclei -u https://example.com -xml
nuclei -u https://example.com -sarif
nuclei -u https://example.com -csv
nuclei -u https://example.com -tsv
nuclei -u https://example.com -jsonl
nuclei -u https://example.com -html
nuclei -u https://example.com -markdown
nuclei -u https://example.com -json
nuclei -u https://example.com -yaml
nuclei -u https://example.com -xml
nuclei -u https://example.com -sarif
nuclei -u https://example.com -csv
nuclei -u https://example.com -tsv
nuclei -u https://example.com -jsonl
mcpsnoop check --fail-on drift --baseline .mcpsnoop/baselines session.jsonl
`drift` è opzionale per `check`. Il gate predefinito `error,invalid,warn` rimane invariato.
### Rilevare una funzionalità che nessuna delle due parti ha negoziato
SEP-2133 ha spostato le funzionalità opzionali fuori dal protocollo core e nelle estensioni,
pubblicizzate nella mappa `extensions` delle capacità di ciascuna parte. Tasks è una di
queste, quindi a partire dal 2026-07-28 una `tasks/get`, una `notifications/tasks` o una `tools/call`
che risponde con un handle di task ha significato solo quando l'altra parte ha dichiarato
di supportare Tasks.
Quando non lo ha fatto, la specifica è esplicita: la parte che supporta la funzionalità DEVE
ripiegare sul comportamento core o rifiutare la richiesta. Farlo comunque è il motivo per cui
una funzionalità sembra collegata e poi non fa nulla in silenzio, e ciò che un lettore ottiene
invece è un `-32601` o un `-32021` diversi frame dopo, oppure un task che non avanza mai. mcpsnoop
avvisa sul frame che ha raggiunto l'estensione e indica quale parte non l'ha mai pubblicizzata.```
tool "slow" answered with a task handle uses the io.modelcontextprotocol/tasks
extension, which the client never advertised
È un warn, quindi un check predefinito fallisce su di esso. Resta silenzioso ogni volta che la cattura non può mostrare cosa è stato negoziato, ovvero una cattura che inizia dopo l'handshake o una le cui capacità sono state ripulite dalla tua stessa redazione, e sulle revisioni precedenti al 2026-07-28, dove tasks/* sono protocollo core e usarli è corretto.
La revisione del 2026-07-28 depreca Roots, Sampling e Logging. Continuano a funzionare per almeno un anno, quindi mcpsnoop le contrassegna invece di trattarle come errori. Lo stream, l'ispettore delle capacità e l'export le segnalano tutti, e ogni marcatore indica la sostituzione.
Due delle tre sono ora raggiungibili solo tramite una richiesta multi round-trip, dove il nome del metodo si trova nella mappa inputRequests del server piuttosto che sul frame stesso. Anche quelle vengono segnalate, così un server che è passato al nuovo pattern non smette silenziosamente di riportarle.```bash
mcpsnoop check --fail-on deprecated session.jsonl
Come `drift`, anche `deprecated` è opt-in. Un'esecuzione predefinita riporta il conteggio e resta verde, quindi una sessione che usa una funzionalità deprecata ma ancora legale non fa mai diventare rossa la CI da sola.
### Costrutti dello schema di flag che i client gestiscono male
Un server può essere perfettamente valido e comunque difficile da usare per un agente. I client differiscono su quanto di JSON Schema supportano davvero, e uno strumento che il modello continua a chiamare in modo errato è spesso uno strumento il cui schema richiede più di quanto il client fornisca.
Il riepilogo degli strumenti, aperto con `s`, ha una colonna SCHEMA che indica la cosa più notevole dello schema di ogni strumento pubblicizzato, con un `+` finale quando ce n'è più di un tipo.
| Mostrato | Significato |
|---|---|
| `no root` | `inputSchema` è assente, non è un oggetto JSON, o ha un tipo radice diverso da `"object"` |
| `dialect` | un `$schema` che indica un dialetto diverso dal 2020-12 a cui la revisione fa default |
| `ext ref` | un `$ref` che punta fuori dal documento, che è anche il caso in cui la specifica avverte gli implementatori di non seguirlo ciecamente |
| `oneOf`, `anyOf`, `allOf`, `not` | una parola chiave di composizione, gestita in modo incoerente tra i client |
| `ref` | un `$ref` che punta all'interno dello stesso documento |
| `untyped` | una proprietà che non dichiara alcun tipo né altro modo di indicare cosa accetta |
Tutti tranne il primo sono osservazioni piuttosto che verdetti. Uno schema che usa `oneOf` non è sbagliato, solo probabile che venga letto in modo diverso da client diversi, e uno schema può dichiarare qualunque dialetto preferisca. `no root` è l'eccezione: la definizione di `Tool` richiede `inputSchema` e fissa il suo tipo radice a `"object"`, quindi un client che valida un elenco rifiuta del tutto quello strumento e non diventa mai chiamabile, senza nulla sul filo che spieghi il perché. `no root` guida la colonna per questo motivo, e uno schema che la redazione di mcpsnoop ha ripulito non viene mai segnalato, poiché uno schema illeggibile non è uno schema sbagliato.
Questa suddivisione decide cosa fa `check` con loro. `no root` è un avviso sul frame `tools/list`, quindi fallisce il gate predefinito `error,invalid,warn` senza alcun flag, che è il punto: un server che pubblica uno strumento inutilizzabile risponde normalmente a ogni handshake e semplicemente non riceve mai un `tools/call`. Le osservazioni vengono conteggiate come `schema_findings` e riportate sotto `schema findings:`, e fanno fallire l'esecuzione solo quando aggiungi `schema` a `--fail-on`. Entrambe raggiungono `--format junit` e `--format sarif`, e `export` trasporta l'elenco per strumento sotto `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 colonna porta il colore di avviso e mai il rosso della colonna ERR, e mcpsnoop continua a non modificare nulla riguardo al traffico che inoltra.
Nulla viene risolto o recuperato. Un $ref esterno viene riconosciuto dalla sua
sola forma, e lo schema a cui punta non viene mai letto.
r riemette una chiamata catturata contro un server live. Per una cattura stdio il
comando è nel log, quindi mcpsnoop avvia una copia isolata e invia la richiesta a
quella. Una cattura HTTP non ha alcun comando da avviare, e l'endpoint che
registra viene privato delle sue userinfo e di ogni valore di query, quindi nomina il server
senza essere un indirizzo da comporre.
Quindi tu dici dove va una ripetizione, e mcpsnoop non compone mai un endpoint di produzione
solo perché qualcuno ha premuto un tasto.```bash
mcpsnoop open --replay-target https://api.example.com/mcp session.jsonl
mcpsnoop open --replay-target https://api.example.com/mcp
--replay-header 'Authorization: Bearer sk-…' session.jsonl
Senza `--replay-target` una sessione HTTP lo dichiara piuttosto che offrire una chiave che
non può funzionare. Con essa, `r` chiede comunque prima del primo invio di una sessione,
allo stesso modo in cui un comando registrato riceve conferma prima di essere eseguito.
Una credenziale raggiunge il server tramite `--replay-header` e da nessun'altra parte.
mcpsnoop non registra alcuna intestazione `Authorization` e non ne riproduce alcuna, quindi
non c'è nulla di catturato che una riproduzione possa far trapelare.
La POST riprodotta trasporta ciò che il trasporto rende obbligatorio, cosa che una POST del
corpo catturato nudo non fa: `MCP-Protocol-Version`, un `Accept` che elenca sia
`application/json` che `text/event-stream`, `Mcp-Method`, `Mcp-Name` dove la
specifica lo richiede, e ogni `Mcp-Param-*` catturato. Questi vengono re-inviati parola per parola
dalla cattura, sentinella base64 inclusa, quindi non possono essere in disaccordo con il corpo
come potrebbe esserlo una ri-derivazione. L'unica intestazione che non viene copiata è la versione
del protocollo, perché il corpo riprodotto dichiara la revisione che mcpsnoop parla e l'
intestazione deve corrispondere al corpo.
`Mcp-Name` è derivato dal corpo che viene inviato piuttosto che copiato, perché la
specifica lo fa derivare da `params.name` o `params.uri` e richiede che un server
rifiuti un'intestazione in disaccordo con il corpo, quindi una modifica che rinomina lo strumento
altrimenti invierebbe il vecchio nome. Le intestazioni `Mcp-Param-*` rispecchiano gli
argomenti catturati, quindi una riproduzione modificata non ne invia nessuna piuttosto che asserire
qualcosa su un corpo che qualcuno ha riscritto. Una cattura può impostare intestazioni solo in quella
singola famiglia. Un log è un file che le persone si passano, e lasciargli nominare qualsiasi intestazione
gli permetterebbe di sovrascrivere quelle obbligatorie o aggiungere una credenziale che nessuno ha passato.
Un `Mcp-Param-*` che una regola di redazione ha ripulito ferma la riproduzione con un motivo. Inviare
il segnaposto metterebbe i byte di mcpsnoop su un server live come se un utente
li avesse digitati.
Un reindirizzamento viene rifiutato piuttosto che seguito. L'indirizzo è quello che hai nominato e
confermato, e seguire un 307 consegnerebbe quella scelta all'estremità remota, re-inviando
il corpo e, su un salto che cambia solo la porta, anche la credenziale. mcpsnoop
riporta dove il server voleva inviarlo e ti lascia decidere se nominare
quello invece.
Una risposta che arriva come un singolo oggetto JSON e una che arriva come un flusso di eventi
vengono entrambe lette, e un errore viene nominato piuttosto che numerato:
- un 401 riporta lo schema che il server ha richiesto
- un `-32020` riporta ciò a cui ha obiettato
- un 400 o 404 non JSON-RPC dice che l'indirizzo non è un endpoint HTTP Streamable
di questa revisione
### Distinguere la latenza del server da quella dell'utente
Sotto richieste multi round-trip una singola chiamata di strumento è diverse richieste, e i
secondi che una persona ha impiegato per rispondere a un'elicitazione stanno dentro l'intervallo. Questo è
deliberato, poiché quell'intervallo è di solito quello che vuoi vedere di più, ma significa
che un singolo numero non può rispondere a entrambe le domande.
Su una catena `book_flight` dove il server ha lavorato 1,2 secondi mentre l'utente ne ha impiegati
37, `check --max-duration 5s` incolpa lo strumento per 38,2 secondi. Lo fa ancora,
perché cambiare cosa significa quel flag allenterebbe ogni pipeline che già
lo imposta. Due fratelli nominano ciò che misurano invece.```bash
mcpsnoop check --max-server-duration 1s session.jsonl # the server's share alone
mcpsnoop check --max-round-trips 2 session.jsonl # how chatty a tool is
## 🛠️ Caratteristiche
- **Rilevamento in tempo reale**: Monitora continuamente le connessioni di rete e i processi per attività sospette.
- **Analisi del traffico**: Analizza i pacchetti di rete per identificare modelli di attacco noti.
- **Risposta automatizzata**: Isola automaticamente i sistemi compromessi e blocca gli IP dannosi.
- **Dashboard intuitiva**: Visualizza avvisi e metriche in tempo reale tramite un'interfaccia web.
- **Regole personalizzabili**: Definisci regole di rilevamento personalizzate per adattarle al tuo ambiente.
- **Registrazione completa**: Conserva log dettagliati per analisi forensi e conformità.
assertion failed: 1 tool call exceeded the 1s server budget (worst: tool "book_flight" held for 1.2s) assertion failed: 1 tool call exceeded the 2 round trip budget (worst: tool "book_flight" took 3)
Entrambi sono disattivati per impostazione predefinita, quindi un'esecuzione `check` predefinita non è influenzata, ed entrambi vengono letti dai timestamp dei frame e da un link che mcpsnoop ha già dedotto, quindi nessuno dei due fa supposizioni sull'intento.
Premi `i` nella TUI per la ripartizione, oppure leggi `interactions` negli export json, text e html. Ogni voce è un'operazione logica con il suo conteggio di round trip, il suo totale, la quota per cui il server l'ha trattenuta e la quota per cui ha atteso il client, più una riga per hop che indica cosa ha chiesto ogni risposta. Il riepilogo per strumento guadagna una colonna `TRIPS`, così uno strumento prolisso è visibile senza aprire nulla.
`export --format har` inserisce la quota del server in `wait` e il resto in `blocked`, che è lo scopo di quel campo, così un visualizzatore smette di disegnare un'attesa del server di 38 secondi che non è mai avvenuta.
I conteggi e le due quote vengono accumulati man mano che arrivano i frame, piuttosto che derivati quando li richiedi, perché lo store live rilascia i frame vecchi per restare nel suo budget e una risposta derivata sarebbe silenziosamente una finestra invece di una catena. La ripartizione per hop viene letta dai frame ancora trattenuti e lo dichiara quando è solo una parte di uno. `ServerTime + ClientTurnaround` equivale al totale per costruzione, non per un'aritmetica di cui qualcuno deve fidarsi.
`--max-round-trips` giudica una catena ancora in esecuzione, perché ogni richiesta già effettuata è conteggiabile e un server che chiede ancora e ancora produce esattamente l'operazione che nessuno porta mai a termine. `--max-server-duration` attende una conclusione, che è la regola che `--max-duration` applica già, poiché un'operazione ancora aperta non ha latenza da giudicare.
Un'operazione che mcpsnoop non è riuscito a collegare resta una voce autonoma a hop singolo. `matchRetry` rifiuta di proposito un collegamento ambiguo, e questa vista non colma quella lacuna.
Un'operazione che ha richiesto una sola richiesta non riporta una ripartizione per hop, perché un singolo hop ripete parola per parola i totali sopra. Una catena riporta un hop per richiesta e lo dichiara quando lo store non trattiene più ogni frame o quando il lavoro si è risolto al di fuori della coppia richiesta e risposta di cui è fatto un hop, come fa un task handle.
### Scopri cosa un server ha chiesto al tuo utente
L'elicitazione è l'unico percorso in MCP in cui una persona digita dati in un server, e con MRTR la domanda e la risposta non sono più due metà di un unico scambio. La domanda è sepolta in un `InputRequiredResult`, la risposta torna dentro `inputResponses` su un retry con un id diverso, e l'unica cosa che le lega è il link che mcpsnoop già deduce.
Senza quell'abbinamento, una richiesta di password rifiutata appare come un semplice errore di strumento.```
tools/call login_legacy [form] creds: decline after 3s
password string
Premi l nella TUI, oppure leggi elicitations negli export json, text e html.
Ogni riga indica l'operazione che la domanda ha interrotto, la modalità, il messaggio,
cosa è stato chiesto, cosa ha fatto l'utente e quanto tempo ha impiegato. Una domanda a cui
nessun retry ha mai risposto risulta come pending, che MRTR considera un esito ordinario
piuttosto che un errore, poiché la specifica dice ai server di non presupporre che un client
ritenti affatto.
Le righe dei form elencano i nomi delle proprietà requestedSchema e i loro tipi dichiarati. Una
proprietà il cui subschema è stato sostituito da una regola di redazione mostra un tipo unknown
piuttosto che il segnaposto, perché un segnaposto non è qualcosa che il server ha dichiarato.
Le righe URL riportano l'indirizzo per intero, che la specifica impone a un client di mostrare
prima del consenso, e nominano l'host da solo, che dice di evidenziare contro lo
spoofing dei sottodomini.
Il ledger non contiene mai un valore inviato. Ciò che l'utente ha digitato resta nella capture per chi ne ha bisogno, e lasciarlo fuori da una superficie di riepilogo costruita per essere esportata e incollata in giro è ciò che tiene tutto questo completamente fuori dalla storia della redazione. Conta soprattutto in modalità url, dove la specifica inserisce le credenziali di proposito.
Un retry risponde al round da cui è stato emesso e a nessun altro. MRTR dice a un server che quando un client omette parte di ciò che è stato chiesto, deve chiedere di nuovo in un nuovo round, quindi un round precedente che contiene una chiave senza risposta accanto a una con risposta è traffico ordinario, e la metà senza risposta resta pending piuttosto che prendere in prestito la risposta del round successivo.
Una domanda registrata è limitata. Il messaggio, l'url e l'elenco dei campi vengono conservati per la durata della sessione, al di fuori del budget di frame che rilascia i body, quindi un server non può renderne uno arbitrariamente costoso. I limiti sono molto al di sopra di qualsiasi domanda reale e un messaggio troncato indica che è stato troncato.
Niente qui avvisa e niente qui cambia un codice di uscita di check. Un ledger
registra ciò che è accaduto. Non lo giudica.
check legge una sessione e diff ne legge esattamente due, quindi uno strumento che fallisce
occasionalmente resta invisibile finché qualcuno non apre le capture a mano. Su
sedici capture di un server il cui run_query risponde isError circa un quarto
delle volte, check riporta la più recente, onestamente, come pulita.```bash
mcpsnoop stats
mcpsnoop stats --since 7d --label prod
mcpsnoop stats --limit 20 --format json
git clone https://github.com/example/repo.git
cd repo
pip install -r requirements.txt
.env.example in .envpython manage.py migratepython manage.py runserverfrom security_tool import Scanner
scanner = Scanner(config_path="config.yaml")
results = scanner.scan_target("https://example.com")
print(results)
La documentazione completa è disponibile nella cartella docs/. Include:
I contributi sono benvenuti! Per favore, consulta le linee guida per i contributi prima di inviare una pull request.
Questo progetto è concesso in licenza sotto la MIT License.
Per domande o supporto, apri un problema su GitHub Issues o contattaci via email all'indirizzo [email protected].
Disclaimer: Questo strumento è destinato esclusivamente a test di sicurezza autorizzati. L'uso non autorizzato è vietato e potrebbe violare leggi locali e internazionali. Utilizzalo a tuo rischio e pericolo.
read 16 logs of 16 in ~/.local/state/mcpsnoop/sessions
SERVER TOOL CALLS ERR PROTO FAIL% SESS p50 p95 p99 DEF
flaky-demo run_query 13 3 0 23.1% 3/13 434ms 519ms 519ms 195B
docs-mirror run_query 3 1 0 33.3% 1/3 357ms 434ms 434ms 195B
docs-mirror search_docs 12 0 0 0.0% 0/3 377ms 386ms 386ms 200B
flaky-demo search_docs 52 0 0 0.0% 0/13 42ms 58ms 59ms 200B
```
`ERR` e `PROTO` sono colonne separate perché la specifica le considera
cose separate. Uno strumento che risponde con `isError` segnala qualcosa su cui un
modello può agire e riprovare. Un errore JSON-RPC indica che la richiesta o il server
sono sbagliati.
`SESS` è il conteggio delle sessioni che hanno riscontrato un errore rispetto alle sessioni che hanno
chiamato lo strumento, ovvero la domanda "una esecuzione su dieci" che un tasso basato sulle chiamate non può
rispondere.
Le righe sono identificate dalla combinazione di server ed etichetta. Il server è il
comando registrato e la directory di lavoro per stdio e l'endpoint per HTTP, la stessa
identità che usa `inventory`. Una delle due metà da sola raggruppa qualcosa che non dovrebbe: la sola
etichetta unisce due server che derivano da un unico nome, cosa che accade ogni volta che due
checkout di un progetto eseguono lo stesso punto di ingresso, e la sola identità unisce
un singolo comando eseguito deliberatamente come `prod` e poi come `staging`. Entrambi gli errori
fondono due distribuzioni pulite in una che non descrive nessuna delle due.
Quando due righe condividono effettivamente un'etichetta, la cella `SERVER` riporta la directory di lavoro
o l'endpoint che le distingue, e il JSON riporta `command`, `cwd` e
`endpoint` su ogni riga. Un nome che non è mai stato ambiguo viene lasciato invariato, quindi la
tabella ordinaria rimane invariata.
Ogni sessione in un log viene aggregata, non solo la prima, quindi un file creato
concatenando catture conta tutte le sessioni.
I percentili sono aggregati sulle durate grezze. Una mediana delle mediane è una mediana
di nulla. Un'operazione multi round-trip è una singola chiamata con una singola durata, indipendentemente da quante
richieste abbia richiesto, e una chiamata ancora aperta conta verso `CALLS` pur
non contribuendo alcuna latenza.
Una cattura è residente alla volta. Un log viene caricato, aggregato nei contatori
correnti e rilasciato prima che il successivo venga aperto, quindi una directory con centinaia di file
costa quanto la singola cattura più grande, non la loro somma.
`--limit` predefinito è di cento dei log più recenti e l'intestazione indica quanti
di quanti sono stati letti, quindi una risposta limitata non passa mai per una completa.
`stats` riporta e non blocca: non scrive nulla, non tocca alcuna baseline, non apre
alcun socket ed esce con 0 ogni volta che la scansione ha avuto successo.
### Vedere quali server sono stati effettivamente eseguiti qui
La scoperta che la gente continua a ripetere su Shadow MCP è che le organizzazioni
scoprono più volte più server MCP in esecuzione di quanti ne siano stati approvati, perché un
server è spesso solo una dipendenza che qualcuno ha aggiunto a un plugin IDE. La stessa cosa
accade in miniatura su un singolo laptop, e mcpsnoop ha registrato la
risposta per tutto il tempo senza mai mostrarla.```bash
mcpsnoop inventory
mcpsnoop inventory --tools # also count what each server last advertised
mcpsnoop inventory --format json # for something else to read
```
Una riga per server, non per sessione. La chiave della riga è il comando registrato
e la directory di lavoro, mai l'etichetta, perché l'etichetta deriva
dall'ultimo elemento del percorso del comando e `node ~/one/build/index.js` e
`node ~/two/build/index.js` derivano entrambi `index.js`. Una sessione HTTP si basa invece
sull'endpoint che ha fatto da proxy, poiché mcpsnoop non ha avviato nulla lì.
La lettura è un envelope per log, il frame meta che il proxy scrive per primo, quindi
resta economica su una directory di capture di grandi dimensioni. `--tools` è l'eccezione e
legge un log per server, l'esecuzione più recente di ciascuno, motivo per cui è un flag
piuttosto che una colonna. Anche in quel caso la lettura è limitata, perché un inventario degli strumenti è
stato di sessione che lo store incorpora man mano, quindi una capture di cento megabyte viene
letta attraverso una finestra fissa piuttosto che tenuta intera per produrre un singolo intero.
Quando non c'è un conteggio, la riga indica quale di tre cose è accaduta, perché
un log che non ha potuto essere letto non è un server che non ha pubblicizzato nulla, e
una sola frase per entrambi farebbe affermare a mcpsnoop qualcosa di falso.
Un comando riscritto da una regola `--redact` viene stampato come registrato e contrassegnato, piuttosto
che spacciato per il comando effettivamente eseguito. Due esecuzioni di un server, una ripulita
e una no, sono due righe. mcpsnoop non può sapere cosa abbia sostituito il segnaposto,
e unirle significherebbe supporre che le metà nascoste corrispondessero. Un server eseguito
sotto due valori `--label` è una riga che riporta entrambi i nomi, poiché la chiave è il
comando piuttosto che il nome.
Nessun elemento in una riga è scritto da mcpsnoop. Un comando proviene da chi ha installato
il server, una directory di lavoro viene dal filesystem e un'etichetta derivata
viene dal comando. Un valore contenente un carattere di controllo viene citato piuttosto
che stampato grezzo, così una directory il cui nome contiene un newline non può chiudere
il campo in cui è stampata e far sì che le righe successive vengano lette come server mai
eseguiti. Anche un argomento contenente uno spazio viene citato, perché `node "~/My Project/
build/index.js"` è altrimenti indistinguibile da due argomenti.
Qualsiasi cosa che la scansione non abbia potuto incorporare viene nominata nell'intestazione piuttosto che eliminata.
I log vuoti vengono contati separatamente da quelli danneggiati, poiché un log di zero byte è il
residuo ordinario di un'esecuzione il cui exec è fallito o di un proxy HTTP che nessuno ha chiamato.
L'output è ordinato per nome piuttosto che per data di esecuzione, così due esecuzioni su una directory
producono gli stessi byte, il che lo rende utilizzabile come baseline da confrontare
in seguito.
Due lacune esistono per costruzione, non per svista. Un'esecuzione con
`--trace-file` ha scritto fuori dalla directory delle sessioni e non apparirà, e
`prune` elimina i log, quindi "prima vista" è vecchia solo quanto ciò che è ancora su
disco. mcpsnoop riporta cosa è stato eseguito su questa macchina tramite esso. Non scansiona alcuna rete,
non legge alcuna configurazione client a cui non sia stato puntato e non giudica nulla.
### Distinguere un server guasto da uno strumento che dice no
Uno strumento che risponde con `result.isError` funziona. Ha guardato e non ha trovato nulla, oppure ha
rifiutato l'input. Un server che risponde con un errore JSON-RPC è guasto. Entrambi erano
un singolo numero nel riepilogo degli strumenti, il che significava che uno strumento ben educato che segnala
errori di dominio sembrava esattamente un server guasto e veniva ordinato sopra di esso.
La colonna `ERR` li separa. Il rosso è il lato server, ovvero un errore JSON-RPC
o un'attività terminata come fallita senza dire perché. Il colore di avviso è l'`isError`
dello strumento stesso. Uno strumento con entrambi mostra i conteggi uniti, rosso per primo, e una
riga sotto la tabella indica i due totali ogni volta che c'è un numero di avvisi da
spiegare. L'export riporta la stessa suddivisione come `protocol_errors` e
`tool_errors` accanto al totale `errors` a cui sommano sempre.
`check --fail-on error` è invariato e continua a scattare su entrambi, poiché un gate
che ne ignorasse uno sarebbe un gate che un server potrebbe disattivare restituendo
l'altro.```bash
mcpsnoop export -T json | jq '.summary.tools[] | {name, errors, protocol_errors, tool_errors}'
```
### Scopri quanto ti costa il server in termini di contesto
Le definizioni degli strumenti entrano nel contesto del modello a ogni conversazione, e i
risultati degli strumenti a ogni chiamata. Il riepilogo degli strumenti (`s`) misura entrambi
dalla sessione che hai effettivamente catturato.
La riga `definitions` è il costo fisso: quanto pesa il `tools/list` di questo server
prima che venga effettuata una singola chiamata. La colonna `DEF` suddivide quel costo per
strumento e `RESULT` è quanto sono costate finora le risposte di ciascuno strumento. La tabella
rimane ordinata per errori e latenza, quindi scorri `DEF` per trovare le definizioni costose.
L'export le elenca dalla più pesante alla più leggera. Una riga sotto la tabella indica il singolo
risultato più pesante, che un totale nasconde.
Le cifre delle definizioni sono il JSON con rimossi gli spazi bianchi insignificanti, quindi un
server che stampa in modo leggibile il proprio `tools/list` non viene conteggiato come più costoso di
uno che non lo fa, e lo stesso server misura lo stesso valore tra catture diverse.
`RESULT` è il numero di byte così come sono arrivati: un risultato è un payload una tantum piuttosto
che un contratto che vale la pena normalizzare.```bash
mcpsnoop export -T json | jq '.summary.definitions'
```
L'esportazione riporta le stesse cifre, per strumento e suddivise in byte di descrizione e byte di schema, così una descrizione corposa e uno schema corposo restano separabili e ciascuno può essere tracciato tra le catture. `mcpsnoop diff` ti dice se una descrizione o uno schema è cambiato tra due sessioni. L'esportazione è il punto in cui vive la dimensione di quel cambiamento.
**Questi sono byte, non token.** Il conteggio dei token dipende dal modello, quindi misurarli significherebbe includere un tokenizer e scegliere quale. I byte sono esatti e puoi applicare il tuo rapporto. Un `tools/list` non completato riporta ciò che ha visto come minimo e lo dichiara, invece di spacciare una somma parziale per il totale.
### Rilevare un client che altera lo stato del server
Nel pattern multi round-trip il server consegna al client un `requestState` opaco e il client deve rispedirlo identico al retry. Al server viene detto di trattarlo come input controllato dall'attaccante, perché un client che lo manomette può tentare di alterare il comportamento del server o aggirare un controllo di autorizzazione.
Standing nel mezzo del flusso, mcpsnoop vede il valore uscire e tornare, quindi può dire quando il contratto è stato violato. Ci sono tre modi in cui può rompersi, ciascuno segnalato come avviso di protocollo sul retry.
| Segnalato | Significato |
|---|---|
| `MRTR retry changed requestState` | il client ha rispedito qualcosa di diverso da ciò che il server ha emesso |
| `MRTR retry is missing requestState` | il server ne ha emesso uno e il retry lo ha omesso |
| `MRTR retry invented requestState` | il retry ne portava uno che il server non ha mai emesso |
Queste sono violazioni di protocollo da parte del client, non osservazioni nostre, quindi viaggiano sul normale segnale di avviso e **un'esecuzione `check` predefinita fallisce su una di esse**. Questo è intenzionale. Un client che altera lo stato del server merita di bloccare una build.
Il valore stesso non viene mai mostrato né registrato, e nulla lo decodifica o lo analizza. Può essere un blob cifrato che trasporta un principal e un token, e confrontare byte opachi è l'intero controllo.
Un caso è fuori portata. Quando un server risponde con un `requestState` e nessun `inputRequests`, un retry manomesso non corrisponde a nulla e non risponde a nessuna chiave, quindi non resta nulla per collegarlo alla richiesta originale e viene letto come una chiamata non correlata piuttosto che come una violazione.
Uno scambio abbandonato non disturba il successivo e non viene nemmeno conservato per sempre. Sessantaquattro scambi aperti sono molti più di quanti un client ne abbia mai in una volta, quindi una sessione che ne contiene di più ne sta trattenendo alcuni che nessuno completerà, e i più vecchi vengono ritirati perché la specifica dice ai server di dare a quello stato una scadenza breve e di rifiutarlo in seguito. Il ritiro viene conteggiato, non silenzioso. Il footer del flusso mostra `N unlinked` e l'esportazione porta `session.retired_exchanges`, perché un retry che arriva per un'operazione ritirata viene letto come una chiamata a sé stante, e un lettore che confronta i conteggi merita di esserne informato.
Ritirarne uno consente anche allo store live di rilasciarlo. Un'operazione in pausa resta in attesa di proposito, quindi la sua durata copre l'intero scambio, e lo store rifiuta di dimenticare una chiamata in attesa perché una risposta potrebbe ancora arrivare. Una volta che il limite ha ritirato un'operazione, nulla può più rispondere, quindi trattenerla mantiene viva una chiamata che nessun lettore può raggiungere. Ciò che la sessione riporta non cambia. È ancora conteggiata come in attesa e ancora conteggiata in `N unlinked`, perché quanta memoria occupa un record e cosa dice il record sono domande diverse.
Uno scambio abbandonato non disturba il successivo. MRTR dice ai server che non devono presumere che un client ritenterà mai, quindi un utente che rifiuta una sollecitazione lascia un'operazione che nessun frame successivo risolverà mai. mcpsnoop cerca prima tra le operazioni la cui presenza di `requestState` concorda con quella del retry, cosa che la specifica rende una regola in entrambe le direzioni, quindi un retry conforme trova comunque l'unica operazione che continua anche quando uno scambio abbandonato sullo stesso strumento è lì accanto. Il controllo che segnala le tre violazioni sopra viene eseguito solo quando nulla concorda, quindi un retry genuinamente non conforme viene comunque identificato.
## Osservare da un'altra macchina
Mantieni la cattura locale alla macchina dove avviene il traffico e usa SSH per il salto di rete, così mcpsnoop non ha mai bisogno di un trasporto remoto proprio.
### Vista live
Esegui la TUI sulla tua workstation e inoltra indietro il socket mcpsnoop della macchina remota. Il tunnel live usa l'inoltro di socket Unix di SSH, quindi entrambe le estremità devono eseguire Linux o macOS. Su Windows, usa la copia del log post-mortem qui sotto.```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
```
Il socket risiede nella directory di stato del remoto, risolta come `MCPSNOOP_HOME`,
altrimenti `XDG_STATE_HOME/mcpsnoop`, altrimenti `~/.local/state/mcpsnoop`. Di default mcpsnoop
presume la home Linux `/home/<user>` dal tuo `user@host` e stampa un promemoria
su stderr ogni volta che ripiega su quella ipotesi. Se il remoto risolve altrove,
indica l'unico elemento non predefinito.```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
Trasmetti una sessione remota direttamente nella TUI via SSH, senza bisogno di copie locali.```bash
ssh remote-user@remote-host 'cat ~/.local/state/mcpsnoop/sessions/session.jsonl' | mcpsnoop open -
```
Per mantenere una copia locale, copia invece i log nella tua directory delle sessioni con scp ed esegui la TUI come di consueto.```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
```
## Sicurezza
mcpsnoop esegue il comando server che avvolgi, quindi avvolgi solo server di cui ti fidi ed esegui quelli non affidabili in un container. Non esegue mai nulla che tu non abbia inserito nella configurazione del tuo client.
Per i flussi di lavoro remoti, usa il tunneling SSH o il trasferimento file SSH, così autenticazione del trasporto, crittografia, verifica dell'host, rotazione delle chiavi e policy di audit restano nella tua configurazione SSH esistente.
### Oscurare ciò che catturi
I frame catturati possono includere prompt, argomenti degli strumenti, credenziali e risultati degli strumenti. Se i payload possono trasportare segreti, attiva l'oscuramento per ripulire le copie del trace osservato mentre i byte inoltrati passano comunque invariati.
L'oscuramento basato su chiavi sostituisce interi valori sotto chiavi JSON corrispondenti, e lo stesso set di chiavi viene applicato al meglio degli sforzi agli argomenti della riga di comando del server avvolto, così `--api-key=sk-x` e `--token sk-x` vengono ripuliti con `--redact-secrets`. Un argomento che trasporta un segreto senza un nome di flag riconoscibile non può essere rilevato.
L'endpoint HTTP non fa parte di tutto ciò, perché non è un payload che hai scelto di inviare. `--target` è un flag che devi passare per eseguire il proxy, quindi il suo URL raggiungerebbe il log della sessione qualunque siano le tue impostazioni di oscuramento. mcpsnoop lo registra con userinfo, ogni valore di query e il frammento già rimossi, sempre, per costruzione piuttosto che per pattern. Le chiavi di query sopravvivono, poiché sono ciò che distingue due endpoint di un singolo host, e il frammento viene scartato perché non ha mai raggiunto il server in primo luogo. Ciò che viene registrato identifica il server e non è un indirizzo da contattare.
L'oscuramento basato su percorsi sostituisce solo i valori selezionati da un'espressione JSONPath, utile quando un nome di chiave comune è sensibile in una posizione ma sicuro in un'altra. Ripeti `--redact-path` per ripulire più di una posizione.
L'oscuramento basato su valori applica espressioni regolari ai valori stringa osservati, al testo di stderr e ai frame di testo non JSON.
Tutti e tre sono al meglio degli sforzi. Le regex possono non rilevare segreti, corrispondere eccessivamente a testo innocuo o non vedere valori trasformati o codificati.
L'oscuramento non si trasforma mai in un'accusa. Ogni controllo che confronta una cosa osservata con un'altra, un'intestazione di routing con il corpo, un valore `Mcp-Param` con l'argomento che rispecchia, lo schema di uno strumento con ciò che la revisione richiede, sa quando mcpsnoop è stato il lato che ha riscritto i byte e resta in silenzio piuttosto che segnalare un server per l'impostazione di privacy dell'utente stesso. La deriva della definizione degli strumenti è l'eccezione, e deliberatamente, poiché attivare l'oscuramento cambia ciò che viene registrato e quindi ciò che una baseline contiene. Vedi [Rilevare la deriva della definizione degli strumenti](#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+'
```
## Contributing
Issue e pull request sono benvenuti. Consulta [CONTRIBUTING.md](https://github.com/kerlenton/mcpsnoop/blob/main/CONTRIBUTING.md) per
i dettagli.
## Licenza
[MIT](https://github.com/kerlenton/mcpsnoop/blob/main/LICENSE)