Repository navigation
MCP Tools Captures
The captures bundle works with capture files in the file root: listing,
searching, comparing, opening and exporting them, and the evidence that ties a
finding to captured bytes.
Load only these tools with --mcp-tools captures, or combine bundles as
Choosing which tools load describes.
Every tool on this page follows the rules every tool
follows, and each section states only
what its tool does differently. The MCP tool reference lists
the other bundles and finds any tool by name.
| Tool | Parameters | Returns |
|---|---|---|
list_captures |
-- | Capture files and stored vCon containers in --mcp-file-root, with sizes and each one's kind |
find_in_captures |
filter, max_files?, deadline_ms?, wait_seconds?
|
Which capture files hold dialogs matching a filter, without opening any of them; a sweep that outlasts its wait returns a job |
find_in_captures_status |
job_id, wait_seconds?
|
A find_in_captures job's progress, or its result once it finishes |
cancel_find_in_captures |
job_id |
Stops a running find_in_captures job |
compare_captures |
a, b, dimensions?, top_n?
|
Diffs two capture files in --mcp-file-root by aggregate, ranked by how far each bucket MOVED. Neither becomes the loaded capture |
show_evidence |
refs, max_bytes?
|
Follows frame pointers back to the captured bytes: verified, unverified, or unresolvable with a reason |
decode_evidence |
frame_ref, field?
|
Decodes the frame one pointer names: link type, addressing, and each SIP header's byte range inside the message and the frame |
build_evidence_package |
call_ids, filename
|
Write. One directory of escalation artifacts in --mcp-file-root: pcapng, ladder and RTP stats per call, manifest, and the rebuilt-frames README |
save_findings |
summary, call_id?, detail?
|
Write. Records the agent's conclusion to sipnab's log. Needs --mcp-allow-save-findings; no tool reads it back |
export_capture |
filename |
Writes held SIP signaling to a pcap in --mcp-file-root (re-synthesized frames, no RTP) |
open_capture |
filename |
Destructive. Replaces every dialog and stream with another capture from --mcp-file-root. Needs --mcp-allow-open-capture; loads in the background |
Start here when you have a capture and no particular suspect. These answer "what is in it", "how much", and "is the server even seeing traffic".
Capture files and stored vCon containers in the configured root, with their sizes and the instant each one's first packet arrived.
Parameters: none.
It lists what sipnab can open by name, matched case-insensitively:
-
.pcap,.pcapngand.capfiles, their gzip-compressed forms such as*.pcap.gz, and archives of captures (.tar,.tgz,.tar.gz, and.zipin builds with thearchivefeature). Each haskind: "capture". - Stored vCon containers:
*.vcon.json, the name sipnab writes the unsigned, signed and encrypted forms under, and*.vcon.json.gz, the namegzipgives the gzip format. Each haskind: "vcon".open_captureandfind_in_capturesread them as-Idoes. The.<name>.partialfile the fetcher and the forwarder write before they move a container into place is not listed.
Each entry carries encrypted: true for a ZIP
holding an encrypted member, read from the archive's directory alone, so
listing never tries a password. It is false for a vCon container, encrypted
or not. The listing skips directories.
The TUI's file browser lists by the same rule. open_capture
also accepts a capture with no extension at all, which this listing leaves
out. To open one, ask the operator for its name.
Without --mcp-file-root the whole file-tool group is off, and this answers
with a refusal, not an empty list:
Otherwise it answers with captures sorted by filename (upper case first),
plus schema_version. With --mcp-file-root tests/pcap-samples it returns 36
of the directory's 38 entries. The two it leaves out are PROVENANCE.md,
which is not a capture, and SIP_CALL_RTP_G711, a capture with no extension:
// list_captures {}
{
"schema_version": 1,
"captures": [
{ "filename": "Asterisk_ZFONE_XLITE.pcap", "kind": "capture", "bytes": 255581,
"encrypted": false, "first_packet": "2010-09-27T07:12:49+00:00" },
{ "filename": "DTMFsipinfo.pcap", "kind": "capture", "bytes": 25429,
"encrypted": false, "first_packet": "2011-04-27T08:14:29+00:00" },
{ "filename": "SIP_DTMF2.cap", "kind": "capture", "bytes": 420411,
"encrypted": false, "first_packet": "2005-09-09T12:02:25+00:00" },
…
]
}Use first_packet to choose a file before opening it. It is the
timestamp of the file's first record, in whole seconds. Among forty rotated
captures, it narrows the search for a call from 14:02 to the two files that
could hold it, without opening any. open_capture replaces
the loaded capture, and find_in_captures searches files
without doing so.
The key
reads null whenever sipnab got no first packet out of the file — a rotated
capture that never received one, or a file it could not open — so treat null
as "unknown", not as "empty". An archive always reads null: its first packet
belongs to whichever member holds the earliest traffic, and finding out means
unpacking it.
For a vCon container, first_packet is the time of the earliest SIP message it
stores. sipnab reads the whole container to find it, as -I does. It reads
null for a container sipnab cannot open, such as an encrypted one on a run
with no --vcon-key.
The listing carries no dialog count and no last-packet time, because both
need the whole file parsed. To learn which file holds a call, use
find_in_captures.
Which of these files holds the call, asked without giving up the one you are working in.
Use it to find which file in --mcp-file-root holds a call, for example a
Call-ID across rotated captures. open_capture would replace
the loaded capture and void every cursor you hold. find_in_captures reads
each file into a scratch store, applies the filter, and leaves the loaded
capture untouched. An archive in the root counts as one file: the sweep reads
every capture inside it into the one scratch store, and a match names the
archive. The sweep opens the files list_captures lists,
stored vCon containers included: it reads the SIP messages a container stores,
as -I does.
| Name | Type | Legal values | If omitted |
|---|---|---|---|
filter |
string | A filter DSL expression, the same vocabulary every other filtering tool takes. Unparseable fails with invalid_params before the sweep opens anything |
Required |
max_files? |
u32? | Files to open before stopping. Clamped to the operator's --mcp-sweep-max-files ([limits] mcp_sweep_max_files, default 20); 0 means that ceiling |
The ceiling |
deadline_ms? |
u64? | Wall-clock the sweep may spend. Clamped to the operator's --mcp-sweep-deadline-ms ([limits] mcp_sweep_deadline_ms, default 30000); 0 means that ceiling |
The ceiling |
wait_seconds? |
u32? | Seconds this call waits for the sweep to finish before it returns the running job. Clamped to --mcp-max-wait-seconds (default 60); 0 returns at once |
30 |
The examples run against a file root holding three captures from
tests/pcap-samples: SIP_DTMF2.cap, sip-problem-call.pcap and
sip-rtp-g711.pcap:
sipnab --mcp -N -I tests/pcap-samples/sip-rtp-g711.pcap --mcp-file-root /var/spool/sipnab-captures// find_in_captures { "filter": "call_id == \"1-1966@10.0.2.20\"" }
{
"schema_version": 2,
"job_id": "sweep-1",
"status": "done",
"limits": { "max_files": 20, "deadline_ms": 30000 },
"progress": { "files_examined": 3, "files_total": 3, "elapsed_ms": 8,
"cancel_requested": false },
"sweep": {
"matches": [
{ "filename": "sip-rtp-g711.pcap", "dialogs_matched": 1,
"first_call_id": "1-1966@10.0.2.20" }
],
"files_examined": 3,
"files_total": 3,
"unreadable": [],
"complete": true
},
"source_exhausted": true,
"source_stopped_early": false
}A filter that does not parse fails with invalid_params and
data.reason: "invalid_filter" before the sweep opens any file.
The sweep reads its files on its own thread, never on the thread that serves MCP and REST. While it reads, sipnab answers every other MCP call and every REST request as usual.
The call that starts it waits up to wait_seconds for it to finish. When the
sweep finishes in that time, the same call returns status: "done" and the
sweep object. When it does not, the call returns its job_id,
status: "running" and progress, and no sweep:
// find_in_captures { "filter": "call_id == \"1-1966@10.0.2.20\"", "wait_seconds": 0 }
{
"schema_version": 2,
"job_id": "sweep-1",
"status": "running",
"limits": { "max_files": 20, "deadline_ms": 30000 },
"progress": { "files_examined": 0, "files_total": 3, "elapsed_ms": 1,
"cancel_requested": false },
"source_exhausted": true,
"source_stopped_early": false
}Poll it with find_in_captures_status and stop it
with cancel_find_in_captures. All three tools
return this one shape. status is running, done, or canceled, and
sweep is present only once the job has finished.
The job holds no --mcp-max-concurrent slot. A call holds a slot only while it
waits, and --mcp-max-wait-seconds bounds the wait. Operator settings bound
the jobs themselves:
| Bound | Flag | Config key | Default | Accepted |
|---|---|---|---|---|
| Sweeps running at once, per server | --mcp-sweep-max-running |
[limits] mcp_sweep_max_running |
4 | 1 to 64 |
| Finished results waiting for a poll, per server | --mcp-sweep-max-held-results |
[limits] mcp_sweep_max_held_results |
16 | 1 to 256 |
| Seconds a finished result waits for a poll, from the moment it finished | --mcp-sweep-result-retention-secs |
[limits] mcp_sweep_result_retention_secs |
600 | 1 to 43200 |
With the defaults, while four sweeps run, sipnab refuses a fifth with
invalid_request. The message reports the configured bound, names
--mcp-sweep-max-running and [limits] mcp_sweep_max_running, and names the
two tools that end a sweep. One poll hands over a finished result. After that
poll, after the retention passes with no poll, or once the held bound of newer
results wait, the job id is unknown and sipnab refuses a poll of it with
invalid_params. That message reports the configured retention and held bound
and names both flags. Past the held bound, sipnab drops the result of the
earliest-started sweep first. The flag overrides the key.
Each running sweep is one operating system thread with its own scratch
dialog and stream stores, so --mcp-sweep-max-running bounds the threads and
stores agents can hold at once. A held result keeps one entry per file the
sweep matched and one per file it could not read. sipnab refuses 0 and any
value above the accepted range, from the flag and from the key.
A sweep stops before its next packet, not only before its next file, when the
deadline passes, when cancel_find_in_captures stops it, and when sipnab
receives SIGTERM or SIGINT. stopped_because then says deadline, canceled or shutdown, and
the file it was reading is not counted in files_examined.
A sweep that reports no matches has shown only that the call is not in the
files it read. complete is true in exactly one case: every candidate file
examined, and every one of them readable.
It is false when the sweep stopped early — stopped_because says
max-files, deadline, canceled or shutdown — and false when the sweep
could not open some file. unreadable names each such file with its reason.
A reason comes from the operating system and from libpcap. sipnab strips
control characters and keeps the first 200 characters.
With one unreadable file in the root and a Call-ID that is in none of them:
// find_in_captures { "filter": "call_id == \"zzz\"" }
{
…
"sweep": {
"matches": [],
"files_examined": 3,
"files_total": 4,
"unreadable": [
{ "filename": "broken.pcap",
"reason": "Failed to open pcap file '/var/spool/sipnab-captures/broken.pcap': libpcap error: unknown file format" }
],
"complete": false
}
}Check complete after a match too. On a rotated spool a call that spans a
rotation is in two files, and a sweep that stopped early may have found only
one of them.
limits is what this sweep ran under: the caller's max_files and
deadline_ms after sipnab clamped them to the operator's ceilings.
The operator sets both ceilings, and a caller can only ask for less:
| Ceiling | Flag | Config key | Default | Accepted |
|---|---|---|---|---|
| Files per sweep | --mcp-sweep-max-files |
[limits] mcp_sweep_max_files |
20 | 1 to 4294967295 |
| Milliseconds per sweep | --mcp-sweep-deadline-ms |
[limits] mcp_sweep_deadline_ms |
30000 | 1 to 43200000 |
A spool that rotates forty files needs --mcp-sweep-max-files 40 before one
sweep can reach the oldest file. With the default, the sweep stops after 20
files with stopped_because: "max-files" and complete: false. A call with
"max_files": 1 shows the same thing on a smaller root:
// find_in_captures { "filter": "call_id == \"zzz\"", "max_files": 1 }
{
…
"limits": { "max_files": 1, "deadline_ms": 30000 },
"sweep": {
"matches": [],
"files_examined": 1,
"files_total": 4,
"unreadable": [],
"complete": false,
"stopped_because": "max-files"
}
}The flag overrides the key. sipnab refuses 0 and any value above the
accepted range, from the flag and from the key.
max_files bounds how many files a sweep opens. deadline_ms bounds how long
it runs, which is what limits a sweep over large files: twenty small files and
twenty 2 GB files are the same max_files. The sweep checks the deadline
before each file and before each packet, so it overruns the deadline by at
most one packet.
Polls a find_in_captures job. It waits up to
wait_seconds for the job to finish and answers with the same shape
find_in_captures returns: status: "running" with progress, or done or
canceled with the sweep result. It hands the result over once, and sipnab
refuses the next poll of that id.
| Name | Type | Legal values | If omitted |
|---|---|---|---|
job_id |
string | The job_id a find_in_captures call returned. An id the server does not hold fails with invalid_params, naming the id |
Required |
wait_seconds? |
u32? | Seconds to wait for the job to finish before answering. Clamped to --mcp-max-wait-seconds; 0 answers at once |
30 |
The wait does not block the server: sipnab answers other calls and REST requests while it lasts.
Polling the job that the "wait_seconds": 0 call above started:
// find_in_captures_status { "job_id": "sweep-1" }
{
"schema_version": 2,
"job_id": "sweep-1",
"status": "done",
"limits": { "max_files": 20, "deadline_ms": 30000 },
"progress": { "files_examined": 3, "files_total": 3, "elapsed_ms": 3,
"cancel_requested": false },
"sweep": {
"matches": [
{ "filename": "sip-rtp-g711.pcap", "dialogs_matched": 1,
"first_call_id": "1-1966@10.0.2.20" }
],
"files_examined": 3,
"files_total": 3,
"unreadable": [],
"complete": true
},
"source_exhausted": true,
"source_stopped_early": false
}A second poll of the same id fails, because the first poll took the result:
{ "code": -32602,
"message": "unknown sweep job 'sweep-1': it was never started, its result was already handed over, it finished more than 600 seconds ago (--mcp-sweep-result-retention-secs), or 16 newer results were waiting (--mcp-sweep-max-held-results)",
"data": { "param": "job_id", "reason": "not_found" } }Stops a running find_in_captures job before its next
packet. It returns at once, with progress.cancel_requested: true. A
following find_in_captures_status poll reports
status: "canceled", with stopped_because: "canceled", complete: false,
and the files read to the end before the cancel. For a job that had already
finished, this call returns its result and hands it over.
| Name | Type | Legal values | If omitted |
|---|---|---|---|
job_id |
string | The job_id a find_in_captures call returned. An id the server does not hold fails with invalid_params, naming the id |
Required |
The example cancels a sweep over 63 files, started with "wait_seconds": 0
on a server run with --mcp-sweep-max-files 100:
// cancel_find_in_captures { "job_id": "sweep-1" }
{
"schema_version": 2,
"job_id": "sweep-1",
"status": "running",
"limits": { "max_files": 100, "deadline_ms": 30000 },
"progress": { "files_examined": 13, "files_total": 63, "elapsed_ms": 303,
"cancel_requested": true },
"source_exhausted": true,
"source_stopped_early": false
}The next find_in_captures_status poll returns "status": "canceled" with:
"sweep": {
"matches": [],
"files_examined": 13,
"files_total": 63,
"unreadable": [],
"complete": false,
"stopped_because": "canceled"
}The tool is readOnlyHint: true: it stops a read that find_in_captures
started and changes no capture, store or file.
You know something is wrong but not which call. These cut a capture down to the dialogs worth reading: by symptom, by text, by time, or by relationship to a call you already have.
Is today worse than yesterday, and where.
Use it to compare a capture against a baseline, for example today's traffic
against yesterday's. Two captures share no Call-IDs, so the tool compares
aggregates, not dialogs: it reads both files, groups each one the way
aggregate_dialogs groups, and ranks
the buckets by how far they moved.
| Name | Type | Legal values | If omitted |
|---|---|---|---|
a |
string | The baseline: a bare filename inside --mcp-file-root. |
Required — the call fails. |
b |
string | The capture to hold against it, in the same root. | Required — the call fails. |
dimensions? |
string[]? | From the aggregate_dialogs vocabulary: state, response_code, method, from.user, to.user, ua, src.ip, dst.ip, rtp.codec. A repeated name de-duplicates rather than failing. Anything else fails naming the nine. |
["state", "response_code"] — how many calls reached each state, and which codes they ended on. |
top_n? |
integer? | Rows per dimension. Everything past it sums into other. Ceiling is --mcp-max-rows. |
50. |
The example diffs two files in tests/pcap-samples, with
--mcp-file-root tests/pcap-samples:
// compare_captures { "a": "sip-rtp-g711.pcap", "b": "sip-auth-failure.pcapng",
// "dimensions": ["state"], "top_n": 4 }
{
"schema_version": 1,
"a": { "filename": "sip-rtp-g711.pcap", "packets": 852, "dialogs": 2,
"streams": 2, "dialogs_dropped": 0, "read_error": null },
"b": { "filename": "sip-auth-failure.pcapng", "packets": 8, "dialogs": 2,
"streams": 0, "dialogs_dropped": 0, "read_error": null },
"dimensions": [
{
"dimension": "state",
"buckets": [
{ "value": "Completed", "a": 1, "b": 0, "delta": -1 },
{ "value": "Failed", "a": 0, "b": 1, "delta": 1 },
{ "value": "InCall", "a": 1, "b": 0, "delta": -1 },
{ "value": "Terminated", "a": 0, "b": 1, "delta": 1 }
],
"other": { "value": "(other)", "a": 0, "b": 0, "delta": 0 },
"distinct_values": 4
}
],
"summary": "'sip-rtp-g711.pcap' (2 dialogs) is the baseline; 'sip-auth-failure.pcapng' (2 dialogs) is held against it, so delta is b minus a. Neither is the capture this server holds."
}Buckets rank by absolute movement, not by size, so the first row is the
largest change. delta is b - a throughout. A value absent from one side
counts as zero there, so a response code that appears only in b shows as a
positive delta.
Neither file becomes the loaded capture. The tool reads both into private stores that the call discards, so answers about the loaded capture do not change. The read runs on a separate thread, so sipnab keeps answering other MCP and REST requests while it runs.
The tool is readOnlyHint: false for one reason: reading the files adds to
the process-wide undecodable-frame counts and the ICMP evidence that
get_capture_report reports.
open_capture does the same.
Read the a and b blocks before the deltas. dialogs_dropped above
zero means that side reached the per-file dialog ceiling (100000 dialogs, the
default of --limit, which this tool does not read), so its counts are
lower bounds, not totals. read_error names a read that stopped early, as on
the newest file of a rotating capture that sipnab or another tool still
writes to. The tool reports a partial read instead of refusing it, and
summary states both conditions in words.
The tool refuses four requests:
| Situation | Answer |
|---|---|
No --mcp-file-root
|
file tools are disabled: start sipnab with --mcp-file-root <DIR> to enable them |
| A path rather than a bare name | '../../etc/passwd' is not a bare filename. These tools take a name, not a path, ... |
a and b resolving to one file |
'x.pcap' and 'x.pcap' are the same file; a capture compared with itself differs from itself nowhere |
| A dimension outside the vocabulary | cannot compare on 'hour'; one of: state, response_code, ... |
The dimension check runs before the tool opens either file. The same-file check
runs after both names are fully resolved, so it also catches two spellings of
one file. A capture that yielded no dialogs and reported a read error fails
with internal_error instead of producing a diff.
The dimensions are the nine
aggregate_dialogs accepts.
group_dialogs's to_domain,
hour and next_hop are not accepted.
Every finding a tool reports points back at captured bytes. These retrieve those bytes, prove which frame they came from, and package them so someone else can check the work.
Follows a frame pointer back to the bytes it names, turning one from a string into something a reader can check without reopening the capture.
| Name | Type | Legal values | If omitted |
|---|---|---|---|
refs |
string[] | At least one pointer in <source>#<ordinal>@<digest> form. An empty array fails with invalid_params (refs must name at least one frame pointer; …). |
Required — the call fails. |
max_bytes? |
u32? | 1 to 4096 bytes of hex per frame. Higher clamps to 4096, and to the frame length when the frame is shorter. 0 means zero bytes here, not the default — this is the one parameter on the surface that does not read 0 as "unset". |
256 bytes, enough for a SIP start line and its headers. |
A pointer whose @digest half is missing still resolves, and comes back
unverified rather than verified. Batching is the normal use — one bad
pointer never discards the rest, so each entry reports its own status.
Where pointers come from. Not every tool returns one, and two key names are in use:
-
frame_ref— the findingslint_dialogandvalidate_messagereturn. A finding also cites a message index; the pointer identifies the frame without that list. -
frame—list_dialogs,find_problems,tail_dialogs,get_dialog(its dialog and its messages),get_message, the JSONget_dialog_report, and the streams inrtp_stats. -
No pointer at all —
search_messages,search_by_time,find_correlated,triage_call,check_codec_negotiation,diagnose_registration,compare_dialogs,get_sdp_timeline, the RTCP remote reports, and the capture-level counters.
A fact with no pointer omits the key entirely. It is never "" or frame 0.
The example follows the frame pointer that
list_dialogs returned for the first call in
tests/pcap-samples/sip-rtp-g711.pcap,
on a server started with --mcp-file-root tests/pcap-samples, with
max_bytes cut to 64 to keep the hex short:
// show_evidence { "refs": ["tests/pcap-samples/sip-rtp-g711.pcap#0@db88659b94678546"],
// "max_bytes": 64 }
{
"schema_version": 1,
"requested": 1,
"resolved": 1,
"verified": 1,
"summary": "1 of 1 pointer(s) resolved; 1 verified against a recorded digest",
"frames": [
{
"pointer": "tests/pcap-samples/sip-rtp-g711.pcap#0@db88659b94678546",
"status": "verified",
"source": "sip-rtp-g711.pcap",
"ordinal": 0,
"frame_bytes": 500,
"hex_bytes_shown": 64,
"truncated": true,
"hex": "00 00 00 00 00 00 00 00 00 00 00 00 08 00 45 00 01 e6 fe 17 40 00 40 11 22 cd 0a 00 02 14 0a 00 02 0f 13 c4 13 c4 01 d2 1a 06 49 4e 56 49 54 45 20 73 69 70 3a 74 65 73 74 40 31 30 2e 30 2e 32"
}
]
}source reports the bare filename, not the path the pointer carried — the tool
keeps only the final component and pushes it through the file-root guard. A
pointer that resolves to nothing keeps its entry and gains a reason instead of
the byte fields:
// show_evidence { "refs": ["bogus#1@deadbeef"] }
{
"schema_version": 1,
"requested": 1,
"resolved": 0,
"verified": 0,
"summary": "0 of 1 pointer(s) resolved; 0 verified against a recorded digest",
"frames": [
{
"pointer": "bogus#1@deadbeef",
"status": "unresolvable",
"reason": "cannot open '<file-root>/bogus': Failed to open pcap file '<file-root>/bogus': libpcap error: <file-root>/bogus: No such file or directory"
}
]
}status has three values:
| Status | Means |
|---|---|
verified |
The frame is there and its bytes hash to what the pointer recorded. The capture has not changed under the claim. |
unverified |
The frame is there, the pointer carried no @digest, so this checked nothing. The bytes could come from a rotated capture. |
unresolvable |
No bytes. reason says why — a malformed pointer, a source outside the file root, a frame past the end, or a digest mismatch. |
A digest mismatch is unresolvable: the tool returns no bytes from a capture
that changed after a run recorded the pointer.
The file root confines every source. The tool takes only the final path
component of the pointer's source and resolves it inside --mcp-file-root
under the same rule the file tools use, so copy the capture into the file
root before following its pointers.
A pointer naming a live device or a HEP listener is unresolvable: sipnab
retains parsed messages, not frames, so there is nothing on disk to seek to.
One bad pointer never discards the rest of a batch — each gets its own entry, so
a caller can tell which one failed. max_bytes caps the hex per frame (default
256, maximum 4096) and truncated says when a frame was longer.
Follows one frame pointer down to the byte range of a single header.
show_evidence returns a frame's bytes as hex. This tool
follows the same pointer and reports what the frame contains: the link type,
the innermost addressing, and one row per SIP header with the byte range where
that header sits. Use it to quote the exact header a finding is about.
| Name | Type | Legal values | If omitted |
|---|---|---|---|
frame_ref |
string | ONE pointer in <source>#<ordinal>@<digest> form — the frame key on a dialog, message or stream, or the frame_ref key on a lint_dialog or validate_message finding. Both names carry the same text. A blank string fails with invalid_params. |
Required — the call fails. |
field? |
string? | Keep only headers with this name, matched case-insensitively against the canonical long form (Contact, never m). |
Every header in the message. |
It takes one pointer per call, unlike show_evidence. Pass field to return
only the header you need.
The example follows the pointer
list_dialogs returns for the first call in
tests/pcap-samples/sip-rtp-g711.pcap:
// decode_evidence { "frame_ref": "tests/pcap-samples/sip-rtp-g711.pcap#0@db88659b94678546",
// "field": "Contact" }
{
"frame_bytes": 500,
"link_type": 1,
"network": { "src_addr": "10.0.2.20", "src_port": 5060,
"dst_addr": "10.0.2.15", "dst_port": 5060, "transport": "UDP" },
"ordinal": 0,
"payload_bytes": 458,
"payload_offset": 42,
"pointer": "tests/pcap-samples/sip-rtp-g711.pcap#0@db88659b94678546",
"schema_version": 1,
"sip": {
"header_count": 9,
"headers_returned": 1,
"headers": [
{ "name": "⟦untrusted-capture-data⟧Contact⟦/untrusted-capture-data⟧",
"value": "⟦untrusted-capture-data⟧sip:sipp@10.0.2.20:5060⟦/untrusted-capture-data⟧",
"index": 5,
"message_byte_start": 227, "message_byte_end": 259,
"frame_byte_start": 269, "frame_byte_end": 301 }
],
"is_request": true,
"method": "INVITE",
"start_line": "⟦untrusted-capture-data⟧INVITE sip:test@10.0.2.15:5060 SIP/2.0⟦/untrusted-capture-data⟧"
},
"source": "sip-rtp-g711.pcap",
"status": "verified"
}sipnab fences the header names, values and start line, because the sender wrote
them. header_count counts the headers in the message and headers_returned
counts this answer. index is the header ordinal a lint finding cites. The
message_byte_* pair locates the header inside the SIP message, and the
frame_byte_* pair locates it inside the whole frame, so
frame_byte_start to frame_byte_end in show_evidence's hex is the header
itself.
The ranges come from the parser, which records each header's byte range as it reads the message. A folded header's range spans every continuation line the parser unfolded into it, so the range covers bytes the unfolded value does not reproduce verbatim. A line the parser drops (no colon, not UTF-8, too long, or past the per-message header cap) does not affect the other headers' ranges.
A header that came from no bytes carries no range, and ranges_unavailable
counts them. That happens to a header synthesized or rewritten after the
parse:
{ "sip": { "header_count": 7, "headers_returned": 7, "headers": [ /* one row with no byte keys */ ],
"ranges_unavailable": "1 header(s) carry no byte range, because they came from no bytes in this message -- synthesized, or rewritten after the parse. A plausible range for one of those would point at bytes that never said it." } }The frame-relative pair is also omitted when the transport payload does not
sit at exactly one place in the frame, as with a decapsulated or reassembled
payload. Omitted keys are absent, never 0 or "".
status carries the same three values show_evidence uses, with the same
meanings. An unresolvable pointer returns only pointer, status, reason
and schema_version:
// decode_evidence { "frame_ref": "uprobe:opensips/1234#7" }
{
"schema_version": 1,
"pointer": "uprobe:opensips/1234#7",
"status": "unresolvable",
"reason": "read 7 came from the TLS library inside opensips (pid 1234); it was never a frame on any wire, so there is no capture to seek into and no bytes to verify it against. This is not a missing file"
}Two more keys report a frame that resolved but did not decode.
decode_unavailable appears when the capture does not reopen for its
link-layer type, and when the frame carries no SIP-bearing transport.
not_sip appears when the transport payload does not parse as SIP.
The file root confines every source. As with show_evidence, the tool
never opens the path in the pointer: it keeps the final component and resolves
it inside --mcp-file-root, which is why source above reads
sip-rtp-g711.pcap. A pointer naming a file outside the root comes back
unresolvable with the confined path in its reason.
The answer carries no timestamp. For the message's time, call
get_message, which returns it beside
the same pointer. A frame carrying two SIP messages in one TCP segment decodes
as the first one, because a pointer names a frame, not a message.
One directory holding everything an escalation needs for a set of calls.
Use it to hand a set of calls to someone else, such as a carrier or another team. It writes one directory: a pcapng of the signaling, a ladder and an RTP-stats file per call, a manifest, and a README stating that the pcapng frames were rebuilt from parsed messages. Forward the whole directory, so the recipient gets the README with the files.
| Name | Type | Legal values | If omitted |
|---|---|---|---|
call_ids |
string[] | At least one Call-ID the store holds, in the order they should appear. A repeat packages once. More than --mcp-max-rows of them fails, asking for batches. |
Required — the call fails. |
filename |
string | A bare directory name inside --mcp-file-root, and it must not already exist. |
Required — the call fails. |
// build_evidence_package { "call_ids": ["1-1966@10.0.2.20", "1-1968@10.0.2.20"],
// "filename": "escalation-4417" }
// against tests/pcap-samples/sip-rtp-g711.pcap
{
"schema_version": 1,
"path": "/var/spool/sipnab-exports/escalation-4417",
"calls": 2,
"messages": 10,
"files": [
"README.md", "manifest.json",
"call-01-ladder.md", "call-01-rtp.json",
"call-02-ladder.md", "call-02-rtp.json",
"signaling.pcapng"
],
"summary": "2 call(s) packaged in /var/spool/sipnab-exports/escalation-4417. README.md inside it states that the frames in signaling.pcapng were rebuilt from parsed messages rather than copied, which is the claim the recipient has to see.",
"source_exhausted": true,
"source_stopped_early": false
}manifest.json maps each ordinal back to its Call-ID and repeats the
disclaimer as a field:
{
"schema_version": 1,
"sipnab_version": "0.5.222",
"package": "escalation-4417",
"signaling": "signaling.pcapng",
"signaling_frames_rebuilt": true,
"calls": [
{ "index": 1, "call_id": "1-1966@10.0.2.20",
"ladder": "call-01-ladder.md", "rtp_stats": "call-01-rtp.json" },
{ "index": 2, "call_id": "1-1968@10.0.2.20",
"ladder": "call-02-ladder.md", "rtp_stats": "call-02-rtp.json" }
]
}File names use an ordinal, never the Call-ID. manifest.json maps each
ordinal to its Call-ID.
The pcapng holds every named call's messages in one chronological run, not
grouped by leg, so a multi-leg call reads as one timeline. Its frames come from
export_capture's writer, with the same limits: the SIP
layer is faithful, everything under it is rebuilt from recorded addresses and
ports, sipnab writes a SIP-over-TCP message as UDP, and RTP, RTCP, DNS and ICMP
are absent.
The RTP measurements in call-NN-rtp.json come from the original capture,
and nobody can reproduce them from the pcapng alone. The README says so.
The ladder and the stats are the output of
render_ladder and
rtp_stats for the same calls.
The tool creates nothing until every Call-ID checks out. An unknown id anywhere
in the list fails with invalid_params (call_id 'nope' not found) and
leaves no directory behind. A write that fails afterwards removes the
part-built directory.
The shared file rule applies in full — bare names only, no symlink out of the root, and the tool declines a name already taken, including by a directory:
{ "code": -32602,
"message": "the requested filename '/var/spool/sipnab-exports/escalation-4417' already exists. sipnab will not write over it, because that file may be the only copy of a capture — choose a name that is not taken." }An empty call_ids array fails with call_ids must name at least one call: an empty package is a directory of disclaimers.
Writes one line to sipnab's log and nothing else. Requires
--mcp-allow-save-findings, which is off by default.
| Name | Type | Legal values | If omitted |
|---|---|---|---|
summary |
string | A one-line conclusion. sipnab clips it at 500 characters and reports the original length. | Required — the call fails. |
call_id? |
string? | Any string. Not checked against the store, so a finding can name a call the store no longer holds. | The finding records no call. |
detail? |
string? | Supporting text, clipped at 4096 characters. | The finding carries a summary alone. |
// save_findings { "summary": "audit probe", "call_id": "1-1966@10.0.2.20", "detail": "x" }
{
"schema_version": 1,
"seq": 0,
"written_at": "2026-10-16T11:57:35.132789476+00:00",
"summary_chars_submitted": 11,
"detail_chars_submitted": 1,
"truncated": false,
"recorded_total": 1,
"remaining": 999,
"readable_over_mcp": false,
"delivered_to": "sipnab log (tracing/journald/stderr)",
"capture_identity": {
"node": "capture-01",
"instance": "134b7818df009db9a44c86-2",
"dialog_generation": 13,
"stream_generation": 2
},
"source_exhausted": true,
"source_stopped_early": false
}The finding goes to sipnab's log and nowhere else. No tool reads it back,
it appears in no query result, and no analysis uses it. readable_over_mcp is
always false. Read findings in the log: journalctl -u sipnab, syslog, or
stderr. sipnab logs the line at info as the agent's claim, so --quiet hides
it:
INFO serve_inner: sipnab::mcp::findings: MCP agent recorded a finding: audit probe seq=0 written_at=2026-10-16T11:57:59.714713194+00:00 call_id="1-1966@10.0.2.20" capture_instance="1363cc18df00a3adb2f107-2" dialog_generation=13 stream_generation=2 detail="x"
On a server started without --mcp-allow-save-findings the call fails with
saving findings is disabled: start sipnab with --mcp-allow-save-findings to permit it. A stock server accepts no writes at all.
Two bounds apply, and the response reports both:
- sipnab clips
summaryat 500 characters anddetailat 4096, setstruncated, and reports the original lengths in*_chars_submitted. - One process accepts 1000 findings by default (
--mcp-max-findings,[limits] mcp_max_findings). Past that, sipnab refuses writes with an error naming the limit.remainingcounts down to it.
Getting the result out of sipnab and into the next tool, the next team, or a bug report someone else can reproduce.
Writes the SIP signaling sipnab is holding to a pcap. Use it to preserve signaling before stopping a live capture — otherwise the messages end with the process.
The file is not a copy of the capture. sipnab keeps parsed messages, not the frames that arrived, so the export rebuilds one Ethernet/IP/UDP frame around each message. The SIP layer is faithful. Everything under it is reconstructed from the addresses and ports sipnab recorded.
The file holds no RTP, no RTCP and no non-SIP traffic, and writes a SIP-over-TCP message as UDP. Exporting
tests/pcap-samples/sip-rtp-g711.pcap, which holds 852 packets, writes 10.Nothing inside the file says that the frames were rebuilt. When you forward it, say so.
build_evidence_packageincludes a README that does.
Parameters: filename (string, required) — a bare filename inside
--mcp-file-root. A path component of any kind fails with '../escape.pcap' is not a bare filename. These tools take a name, not a path, and write only inside the configured --mcp-file-root. sipnab does not require a .pcap extension and does
not add one, so notes.txt writes a pcap under that name, which
list_captures then never lists. No optional parameters.
Returns path (the resolved absolute path, so you learn where the bytes went),
messages, bytes and schema_version. Against
tests/pcap-samples/sip-rtp-g711.pcap:
// export_capture { "filename": "sig.pcap" }
{
"schema_version": 1,
"path": "/var/spool/sipnab-exports/sig.pcap",
"messages": 10,
"bytes": 5673,
"source_exhausted": true,
"source_stopped_early": false
}The tools that change server state rather than read it. Every one stays off until you turn it on server-side.
Destructive. Requires --mcp-allow-open-capture, which is off by default.
It loads another capture from --mcp-file-root and throws away every dialog and
stream the server holds.
Use it on a long-lived HTTP server working through several captures, where
restarting the server would end the clients' sessions. Over stdio, start
sipnab again with a different -I instead, which leaves a clean store.
Parameters: filename (string, required) — a bare filename inside
--mcp-file-root, under the rule every file tool applies. Any capture format
libpcap reads is fine, and an archive (.tar, .tgz, .tar.gz) loads as the
set of captures it holds, in first-packet order, into the one store. No optional
parameters, and no way to ask for a merge: this replaces the stores rather than
adding to them.
No password argument, on this tool or any other. sipnab refuses a call
carrying any argument named like a password before the tool runs, and its
audit record shows the value as [REDACTED]:
argument `password` refused: sipnab never takes an archive password in an MCP
tool call, because an argument passes through the model's context, the
client's transcript and the provider's logs. Treat the password you sent as
exposed. The operator configures one when starting sipnab:
--archive-password-file, --archive-password-command, --archive-password-stdin,
the systemd credential archive-password, or SIPNAB_ARCHIVE_PASSWORD.
The file-opening tools
(open_capture, find_in_captures, compare_captures, show_evidence) also
refuse any argument they do not take, rather than ignoring it. An encrypted
ZIP opens with the passwords the operator configured when starting sipnab, as
described in Archives. When members stay locked,
capture_status reports load.archive_passwords: how many, and that the
operator must supply the password. It never invites the agent to send one.
Returns status ("loading"), filename, path, the new
capture_identity, discarded_dialogs, note and schema_version.
The example starts from tests/pcap-samples/sip-rtp-g711.pcap, with
--mcp-allow-open-capture and a file root holding sip-problem-call.pcap:
// open_capture { "filename": "sip-problem-call.pcap" }
{
"schema_version": 1,
"status": "loading",
"filename": "sip-problem-call.pcap",
"path": "/var/spool/sipnab-captures/sip-problem-call.pcap",
"capture_identity": {
"node": "capture-01",
"instance": "13650418df00a4d0f42050-4",
"dialog_generation": 14,
"stream_generation": 3
},
"discarded_dialogs": 2,
"note": "the previous capture is gone; poll capture_status until load.done is true, and treat every answer carrying a different capture_identity.instance as a different capture",
"source_exhausted": false,
"source_stopped_early": false
}The call returns as soon as the background read starts, not when it finishes,
and sipnab keeps answering other MCP and REST requests while the read runs. Poll
capture_status and watch load.packets climb until load.done turns true.
A second later the same session's capture_status shows:
"load": {
"filename": "sip-problem-call.pcap",
"instance": "13650418df00a4d0f42050-4",
"packets": 23,
"elapsed_sec": 1,
"done": true,
"error": null
},
"dialog_count": 5,
"source_exhausted": trueelapsed_sec stops counting when done turns true: it is how long the read
took, and every later capture_status reports the same value.
Every answer afterwards carries a new capture_identity.instance. Treat any
cursor, message index or Call-ID from before the swap as void: they addressed a
different capture. discarded_dialogs says how many dialogs the swap
discarded.
The tool refuses in four situations, and each refusal names what to do instead:
| Refusal | Why |
|---|---|
--mcp-allow-open-capture missing |
The operator did not enable it. The tool is still listed, so an agent can tell "not permitted here" from "this build cannot" |
| The source is a live interface | A live capture never finishes writing to the stores. There is no opt-out |
| The source has not drained | The original reader is still filling the stores. Poll capture_status until source_exhausted is true |
| A load is already running | Poll capture_status until load.done is true |
Without the flag, the refusal reads opening a capture is disabled: start sipnab with --mcp-allow-open-capture to permit it. A stock server holds the capture it was started on. Called before sipnab has read the startup capture
to the end, it reads refusing to open 'sip-problem-call.pcap': the current source has not finished reading. Poll capture_status until source_exhausted is true, then call again.
A fifth, where the client can carry the question. If your client declared
MCP's elicitation capability, a swap that would discard dialogs asks the
person driving it to confirm first, and a decline refuses the call:
refusing to open 'sip-problem-call.pcap': the confirmation was declined;
nothing was done. The loaded capture is untouched and its 2 dialog(s) are
still addressable.
The refusal is invalid_params (-32602) with data.reason: "declined".
sipnab checks the four refusals above before the question, so it asks nobody
to approve a swap it would refuse. A swap that would discard nothing does
not ask. A client that declared no elicitation capability is not asked and is
not treated as having refused. --mcp-allow-open-capture is then the only
guard. See
the protocol page.
A filename must be a bare name inside the root, under the same rule every file tool applies.
Website · Repository · Issues · Generated from docs/ — edit there, not here.
Getting started
- Installation
- First CLI Triage
- Glossary
- Cookbook
- Runnable Examples
- Real World Captures
- Troubleshooting
- Tuning Capture
- Encapsulations
- Rtpengine Relays
- Backers
Using the TUI
CLI & automation
- CLI Reference
- Error Messages
- Filter DSL
- Output Formats
- SIP Response Codes
- SIP Methods
- SIP Header Fields
- SIP Parameters
- SIP Lint Rules
- MOS and Codecs
Configuration
Integrations (API & MCP)
- REST API
- Prometheus Metrics
- Authentication
- MCP
- MCP Deployment
- MCP Across an Estate
- MCP Tools
- MCP Tools Core
- MCP Tools Signaling
- MCP Tools Captures
- MCP Tools Security
- MCP Tools Media
- MCP Tools Relay
- MCP Tools TFPS
- MCP Tools Server
- MCP Tools vCon
- MCP Tools TLS
- MCP Protocol
- Uprobe Walkthrough
- Capture SIP over TLS
- WASM Plugins
- vCon Export
- vCon Capture Stack
- G729 Decoding
Your SIP server
- Use OpenSIPS as the SIP Server
- Run sipnab beside OpenSIPS
- Use Kamailio as the SIP Server
- Run sipnab beside Kamailio
- Use Asterisk as the PBX
- Run sipnab beside Asterisk
- Use FreeSWITCH in the Voice Stack
- Run sipnab beside FreeSWITCH
Add to your voice stack
- Add a vCon Server
- Send vCons to a vCon Server
- Send vCons to vcon store
- Add TFPS
- Control TFPS from sipnab
- Ban SIP Scanners with fail2ban
- Feed fail2ban from sipnab
- Add rtpengine
- Name rtpengine Media with sipnab
- Add rtpproxy
- Name rtpproxy Media with sipnab
- Add Homer
- Connect sipnab to Homer
- Add Prometheus and Grafana
- Add sipnab Metrics to Prometheus
Development & internals
- Internals Index
- Internals Subsystem Guide
- Internals Invariants
- Internals Testing
- Internals Walkthroughs
- Internals Build CI Release
- Internals Profiling
- Internals Domain Primer
- Library API
- Benchmarks
- Fault Model
- Threat Model
- Assurance Case
- Architecture
- Internals Threading
- Internals TUI Testing
- Internals Zero Copy Payloads
- Internals Uprobe Capture
- Internals Rtpengine Control Plane
- Internals Relay Control Decoding
- Internals vCon Exporter
- Internals Operator Guide Notes
- Internals Postmortems
- Internals Code Map
- Internals Lint Rules
- Internals Encapsulation Decoding
- Internals MCP Design Notes
- Internals Metrics And Output Notes
{ "code": -32602, "message": "file tools are disabled: start sipnab with --mcp-file-root <DIR>", "data": { "reason": "not_enabled" } }