MCP Tools - NormB/sipnab GitHub Wiki
Every tool the MCP server exposes, grouped by bundle. This page does four things:
- it says what a bundle is,
- it lists the bundles,
- it finds any tool by name,
- it states the rules every tool follows.
Each bundle's page documents what its tools answer and what they return.
This is lookup material, not reading material.
- MCP server — what MCP is, and a first working example.
- MCP deployment — the deployment shapes.
- MCP protocol — the wire contract, security model and error semantics.
No tool edits the analysis in place, and every response carries a row ceiling.
That ceiling defaults to 1000 rows and is an operator setting:
- Set it with
--mcp-max-rows N, or[limits] mcp_max_rowsin the config file. The flag wins. - Lower it for a model with a small context window; raise it for a batch client that writes answers to a file.
- It is a different limit from
dialog_limit(--limit, default 100000), which bounds the dialogs tracked over the whole run.
One tool replaces the analysis outright: open_capture, off unless you enable
it. It mints a new capture identity. So the replacement cannot reach a
consumer as an ordinary update.
A bundle is a named set of tools defined in sipnab's code.
- Every tool is in exactly one bundle. So the ten bundles together are every tool the server has.
-
--mcp-toolsloads bundles by name, as Choosing which tools load describes. - Each bundle has a page that documents its tools.
| Bundle | Tools | What its tools answer |
|---|---|---|
core |
9 | The smallest set that answers what happened on one call, and whether the fault was in signaling or media, from listing the calls to the bytes that prove it. |
signaling |
20 | Follow-up questions about SIP signaling: single messages, call flows, SDP negotiation, related call legs, RFC conformance, time windows, and reproducing a call. |
captures |
11 | Capture files in the file root: listing, searching, comparing, opening and exporting them, and the evidence that ties a finding to captured bytes. |
security |
6 | Who sent what: scanner and fraud findings, the busiest sources, one endpoint's activity, registration problems, pass/fail expectations, and fail2ban rules. |
media |
4 | Where a call's media endpoints came from, why a stream has no dialog, the facts under a quality score, and audio export. |
relay |
4 | Questions put to the configured media relay, and decoding the relay control messages sipnab captured. |
tfps |
7 | The toll-fraud prevention peer (TFPS): what it blocks and logs, and the operator's ban, unban and revert actions. |
server |
5 | The sipnab process and its capture path: capture health, HEP senders, resource use, build capabilities, and shutdown. |
vcon |
7 | vCon conversation containers, exported, validated and analyzed, one at a time or a directory at once, read from the operator's store, the vCon export spool's counts, and the SIPREC recording metadata a call carried. |
tls |
3 | Reading SIP inside TLS without keys through kernel uprobes, and which TLS libraries this host runs. |
Every tool the server registers sends its name, description and schemas to the
client on tools/list. The client keeps them in the model's context for
the whole session. The client pays that cost before the agent asks anything. So
give a client that needs a few tools only those few:
sipnab -N -I capture.pcap --mcp --mcp-tools core,relay,get_sdp_timeline--mcp-tools (or [mcp] tools in the config file)
takes a comma-separated list of:
-
bundles, from the bundle table. Every tool is in exactly
one, so the bundles together are
full. - single tool names, for one tool from a bundle you do not want whole.
-
your own bundles, defined under
[mcp.bundles]in the config file. -
full, every tool. This is the default.
sipnab refuses to start, exit 2, when a name is unknown or empty, and lists
the valid bundles. Names are case-sensitive. With --mcp-tools core,bogus:
--mcp-tools: unknown tool or bundle 'bogus'; bundles: core, signaling, captures, security, media, relay, tfps, server, vcon, tls, or full; tool names are listed in the MCP tool reference
The handshake's instructions string names the tools sipnab loaded and the
bundles it left out, so an agent that misses a tool can tell the operator
which bundle to add. Under --mcp-tools core it ends:
Tools loaded: core. Other bundles, which the operator can add with --mcp-tools or [mcp] tools: signaling, captures, security, media, relay, tfps, server, vcon, tls.
Output schemas are off by default.
- MCP makes them optional, and they are more than half the bytes.
- Every response carries the same JSON with or without them, as a text block
and as
structuredContent. - Only a client that validates responses against a declared shape needs them.
- Turn them on with
--mcp-output-schemasor[mcp] output_schemas = true.
You can read one tool's response shape without turning output schemas on for
every tool. Read the resource sipnab://schema/<tool>. It returns one of
these:
- the tool's output JSON Schema and an example payload generated from that schema,
- a statement that the tool declares no output schema.
Tool response schemas describes the resource.
| Bundle | Tools | Bytes (default) | Bytes with output schemas |
|---|---|---|---|
| core | 9 | 12,316 | 54,624 |
| signaling | 20 | 22,474 | 58,619 |
| captures | 11 | 12,723 | 30,211 |
| security | 6 | 9,954 | 9,954 |
| media | 4 | 3,480 | 16,048 |
| relay | 4 | 4,199 | 19,988 |
| tfps | 7 | 5,563 | 15,947 |
| server | 5 | 5,394 | 27,452 |
| vcon | 7 | 10,714 | 33,024 |
| tls | 3 | 2,850 | 4,943 |
| full | 76 | 89,667 | 270,810 |
Bytes are the compact JSON of each tool's tools/list entry, summed over the
bundle.
Measured on sipnab 0.5.222 on a build with every feature.
- Sizes, tool counts and bundle membership may change in later releases.
- A build without a feature does not register that feature's tools, so its bundles are smaller.
Every tool in alphabetical order, with the bundle that holds it. The tool name links to its section on the bundle's page.
| Tool | Bundle | Returns |
|---|---|---|
actions_revert |
tfps | Write, off by default. Backs out one ban sipnab placed, or every one it still holds |
aggregate_dialogs |
core | Counts dialogs grouped by ONE field, in the store rather than in the model |
analyze_vcon |
vcon | Rebuilds the SIP messages a stored container holds and analyzes them apart from the loaded capture: per dialog the --json-dialogs object, the signaling diagnosis and the ladder, plus the capture analysis and what a container cannot reproduce |
await_condition |
signaling | Waits until a filter matches or a deadline passes, whichever is first, so a live capture costs one call instead of a polling loop |
build_evidence_package |
captures |
Write. One directory of escalation artifacts in --mcp-file-root: pcapng, ladder and RTP stats per call, manifest, and the rebuilt-frames README |
cancel_find_in_captures |
captures | Stops a running find_in_captures job |
capture_health |
server | Capture-path counters read twice: run totals, deltas across the window, undecoded_fraction, and undecodable frames by reason |
capture_status |
core | What this server captures: live or file, uptime, and whether stopping loses unsaved packets |
check_codec_negotiation |
signaling | Codecs offered vs answered and whether they intersect — for 488s |
compare_captures |
captures | Diffs two capture files in --mcp-file-root by aggregate, ranked by how far each bucket MOVED. Neither becomes the loaded capture |
compare_dialogs |
signaling | Two calls side by side, with the differences named |
decode_evidence |
captures | Decodes the frame one pointer names: link type, addressing, and each SIP header's byte range inside the message and the frame |
decode_ng |
relay | Decodes the relay control message one pointer names: the command, the call, whether it carries SDP, and which delivery path carried it |
describe_endpoint |
security | Everything one participant did: dialogs by method and state, INVITE outcomes, REGISTER state, banners, streams, findings |
diagnose_registration |
security | Whether an endpoint registered, hit a rejection, is looping on auth, or got a short expiry |
evaluate_expectations |
security | A pass/fail verdict per rule plus an exit code for a build. A rule whose population is empty fails rather than passing quietly |
explain_attribution |
media | Where each of a call's media endpoints came from and how much that path is worth — asked directly, HMAC-verified, plain secret, port-gated only, or the parties' own SDP |
explain_response_code |
signaling | IANA registry meaning and class for a SIP status code |
explain_rule |
signaling | The catalog entry behind one rule identifier: citation, basis, scope, selectors |
export_audio |
media | Writes a call's RTP audio to a WAV in --mcp-file-root; needs the server started with --retain-audio
|
export_capture |
captures | Writes held SIP signaling to a pcap in --mcp-file-root (re-synthesized frames, no RTP) |
export_vcon |
vcon | Dialogs as vCon conversation containers, structured JSON, each with its SHA-256. One call_id or a whole filtered set, paged with cursor. Unsigned, retained audio inline unless media is omit, and every omission stated in the response |
fetch_vcon |
vcon |
Connects, off by default. One container from the operator's [vcon_fetch] store by uuid, returned inline with the checks it passed. Needs --mcp-allow-vcon-fetch; the store and its keys come from operator configuration only |
find_correlated |
signaling | The other legs of the same call across a B2BUA, each with a score AND the strategy that matched it |
find_in_captures |
captures | 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 |
captures | A find_in_captures job's progress, or its result once it finishes |
find_problems |
core | A page of dialogs matching one or more diagnostic alias names |
generate_fail2ban_rule |
security | A fail2ban filter and jail derived from ONE recorded finding, with that finding attached as the evidence |
generate_repro |
signaling | A SIPp scenario replaying one call, with the hypothesis as an input: pin holds the suspected cause fixed, vary regenerates identity |
generate_wireshark_filter |
signaling | A Wireshark display filter selecting one call's signaling and its RTP by SSRC, plus the tshark line that applies it |
get_call_tree |
signaling | Every leg of one call as a tree, each edge carrying its parent, depth, score, strategy and whether the walk went through it |
get_capture_report |
core | Whole-capture analysis: findings, orphaned media, STUN/ICMP evidence, what the caps shed |
get_dialog |
core | Paginated dialog with full SIP messages |
get_dialog_report |
signaling | Structured per-call report (JSON / Markdown / text) |
get_message |
signaling | Single SIP message at a given index |
get_sdp_timeline |
signaling | SDP offer/answer exchanges in order: codecs, ptime, direction |
group_dialogs |
signaling | Carrier metrics per group — ASR, NER, ACD, post-dial delay, MOS, retransmissions — each beside the population it rests on |
hep_senders |
server | Who feeds this run's HEP listener: each sender, its count, when it was last heard and whether it went silent, and every address the listener refused, by reason |
lint_dialog |
signaling | Conformance findings for one call, declaration against observation included, each with its RFC and section |
list_captures |
captures | Capture files and stored vCon containers in --mcp-file-root, with sizes and each one's kind |
list_dialogs |
core | A page of dialog summaries, with the total behind it |
list_tls_libraries |
tls | which TLS libraries this host runs, and whether sipnab could read their plaintext without keys |
media_diagnostics |
media | The facts under the MOS: QoS marking, jitter grounding, delay provenance, silence, and what the far end reported |
open_capture |
captures |
Destructive. Replaces every dialog and stream with another capture from --mcp-file-root. Needs --mcp-allow-open-capture; loads in the background |
query_relay |
relay |
Transmits. Asks the configured relay what it holds right now. Needs --mcp-allow-relay-query and a live source; the destination comes from operator configuration only |
reconcile_orphans |
media | Why each RTP stream with no dialog lacks one: a relay named the endpoint but no signaling arrived, SDP named it but no dialog claims it, or nothing named it at all |
relay_compare |
relay |
Transmits. Sets the relay's per-call RTP count beside sipnab's measured count, both tiers named and a word verdict, never a sum. Same gate as query_relay
|
relay_stats |
relay |
Transmits. Asks the configured relay for its own counters (global, per-call, or the names it knows), tiered relay_reported. Same opt-in and live-source gate as query_relay
|
render_ladder |
signaling | Call-flow ladder (Markdown / text), or an annotated Mermaid diagram |
rtp_stats |
core | One call's RTP quality and diagnosis, or a capture-wide stream sweep |
runtime_stats |
server | What sipnab is doing and what it costs the host: its own memory, threads, descriptors and CPU; the host's totals and the basis they came from; its share of them and whether that share is load-bearing; per-interface counters; store occupancy against the caps |
save_findings |
captures |
Write. Records the agent's conclusion to sipnab's log. Needs --mcp-allow-save-findings; no tool reads it back |
search_by_time |
signaling | Dialogs whose first message falls in an RFC 3339 window |
search_messages |
core | A page of substring matches across method/From/To/UA/body, with the total behind it |
security_findings |
security | Recent scanner / fraud / digest / reg_flood findings, plus the detectors this server runs |
server_capabilities |
server | sipnab version, the optional features this binary carries, and the libpcap it runs |
show_evidence |
captures | Follows frame pointers back to the captured bytes: verified, unverified, or unresolvable with a reason |
shutdown_server |
server |
Destructive. Stops the process. Needs --mcp-allow-shutdown; dry-run by default |
siprec_metadata |
vcon | The SIPREC (RFC 7866) recording metadata a call carried: session id, mode, participants with AOR and display name, and the recorded streams, each naming its m= line label and the participant that sends it |
start_tls_capture |
tls | installs kernel uprobes and reads SIP plaintext with no key; needs --mcp-allow-tls-capture
|
stop_tls_capture |
tls | stops that capture and removes its kernel probes |
tail_dialogs |
signaling | Cursor-based incremental dialog fetch |
tfps_ban |
tfps | Write, off by default. Relays an OPERATOR's decision to condemn a source, recorded in the journal and rate limited, and reports the TFPS answer as given, refusal included |
tfps_banned |
tfps | Every source TFPS holds condemned right now, with the rule, what it saw, and when the ban began and lapses |
tfps_dropped |
tfps | Per condemned source, what the enforcement dropped and the last request line the source sent |
tfps_labels |
tfps | The TFPS verdict log -- blocked, would-block, exempt -- the export the label corpus harness scores the scanner detector against |
tfps_status |
tfps | Whether the toll-fraud prevention peer (TFPS) runs on this host, and what it reports: enforcement, mode, interface, sources blocked now, database, version |
tfps_unban |
tfps | Write, off by default. Releases a source sipnab banned |
timeline |
signaling | Call volume per fixed-width interval, so a gap or a spike is visible without reading every dialog |
top_talkers |
security | The busiest IPs, user agents or dialled prefixes, ranked, each share stated against the population behind it |
triage_call |
core | First-pass verdict: signaling problem, media problem, both, or none, with evidence |
validate_filter |
signaling | Whether a filter DSL expression parses, the parser's message when it does not, and how many dialogs it selects — with no rows |
validate_message |
signaling | Conformance findings for one message, read alone |
validate_vcon |
vcon | Checks a container against the working group's draft-ietf-vcon-vcon-core-04 schema and names every finding |
vcon_directory_report |
vcon | Every stored container in a directory of the file root: per container its status, form, schema finding count, calls, dispositions and duration, or why sipnab skipped it, then the totals. The report -I <dir> --report prints |
vcon_spool_status |
vcon | Counts the --export-vcon-dir spool: containers pending, delivered and failed, with the store's answer from each failed container's record. A snapshot of files; sends, moves and deletes nothing |
Seven tools select calls, and each answers a different question.
A filter is one of these:
- an expression in sipnab's filter language (the DSL), such as
state == Failed. A value may be bare or quoted:method == INVITEandmethod == 'INVITE'are the same filter, - the name of a built-in alias such as
problems.
| The question | The tool | Why this one |
|---|---|---|
| Which calls match this condition? |
list_dialogs with filter
|
Takes an alias or any DSL expression and pages through every match, with total_matched and a cursor |
| Which calls show a known kind of problem? | find_problems |
Takes diagnostic alias names (problems, slow-setup, one-way and the rest) instead of an expression, and can narrow them with a DSL filter |
| Which calls contain this text: a number, a User-Agent, a header value? | search_messages |
Searches the whole SIP message, every header, the request line and the body, case-insensitively, and returns the message that matched |
| Which calls started in this time window? | search_by_time |
Selects by the time of the first message, in an RFC 3339 window, optionally narrowed by a filter |
| What did this one IP address or user do? | describe_endpoint |
Selects by ip or user and returns counts over every match plus a page of that participant's dialogs |
| Which capture file holds these calls? | find_in_captures |
Sweeps the files in --mcp-file-root with a filter and names the files that match, without replacing the loaded capture |
| Does my filter parse, and how many calls does it select? | validate_filter |
Compiles the expression and returns the count with no rows, so a wrong filter costs one small answer |
A prompt is a named list of steps. It tells a model which tools to call and
in what order. A client lists them with prompts/list and fetches one with
prompts/get. No prompt takes arguments, and every tool stays callable
without one.
| Prompt | When to use it | Tools it calls, in order | Bundles it needs |
|---|---|---|---|
triage-outage |
Something is failing now |
capture_status, find_problems, triage_call, show_evidence
|
core, captures
|
carrier-escalation |
Building a case to send to a carrier |
capture_status, group_dialogs, get_call_tree, build_evidence_package
|
core, signaling, captures
|
codec-interop-audit |
Two endpoints disagree about media |
capture_status, check_codec_negotiation, group_dialogs, media_diagnostics
|
core, signaling, media
|
post-change-verification |
A change went in and needs checking against a baseline |
capture_status, compare_captures, evaluate_expectations, timeline
|
core, signaling, captures, security
|
vcon-handoff |
You are exporting calls to a vCon store |
capture_status, validate_filter, export_vcon, validate_vcon
|
core, signaling, vcon
|
Each prompt's steps:
-
triage-outage:- read
unanalysed_sip_messagesfromcapture_statusfirst; - get the shape of the failure from
find_problems; - run
triage_callon one Call-ID from that result; - follow its
framepointer withshow_evidencebefore stating that a message caused the failure.
- read
-
carrier-escalation:-
capture_status; -
group_dialogsbynext_hopwith theasrandnermetrics, reading thepopulationblock; -
get_call_treeon an affected Call-ID, because carrier faults usually span several legs; -
build_evidence_packagefor the calls the escalation cites.
-
-
codec-interop-audit:-
capture_status; -
check_codec_negotiationfor offers with no common codec; -
group_dialogsbyrtp.codecwithmos_p10; -
media_diagnosticson a call that negotiated a codec and still sounded wrong.
-
-
post-change-verification:-
capture_status; -
compare_capturesagainst the baseline from before the change; -
evaluate_expectationswhen the repository has a rules file; -
timelineto check the failures did not move to a different hour.
-
-
vcon-handoff:-
capture_status; -
validate_filteron the filter that selects the calls; -
export_vconwith thatfilter; -
validate_vconon each exported container, passed ascontainer; - then record each container's
digest(its SHA-256) and Call-ID.
Passing
call_idtovalidate_vconinstead exports a second container with a different creation time. So its digest differs from the one sent. -
sipnab serves a prompt only when the server registers every tool it calls.
-
Under
--mcp-tools core,prompts/listreturns no prompt. Each one calls at least one tool outsidecore. -
prompts/getrefuses a prompt it does not serve with error -32602. The message names the tools the server does not load and the bundles that hold them:{"code":-32602,"message":"prompt 'triage-outage' is not served: it calls show_evidence, which this server does not load. Add the bundle(s) captures with --mcp-tools or [mcp] tools. Served: ","data":{"param":"name","reason":"not_enabled"}} -
A tool the build does not carry also hides the prompts that call it.
Seven rules hold across the whole surface. Each tool section on a bundle page states only what its tool does differently. So read these once and skip them afterwards.
A required parameter has no default.
- Leave one out, or send the wrong JSON type, and the tool does not run. The
call fails with
invalid_params(-32602),data.reason"invalid_arguments", and a message that names the field, for examplefailed to deserialize parameters: missing field `call_id`. - A value of the right type that the tool cannot use (an unknown choice, a
range it does not accept) fails with
invalid_params(-32602), anddata.reasonsays which; see Error data. - Optional parameters carry a
?in every parameter table. - Each row says what the tool does when you omit that one.
A limit of 0 means the default, not "no rows".
-
limitdefaults to 50 everywhere it appears. - The server clamps anything larger to
--mcp-max-rows(1000 unless the operator lowered it). It returns the clamped page without an error. - Both edges bite the same caller:
- A client that computes
limitfrom "rows I still want" asks for 50 when it meant to ask for none. - A client that asks for 5000 to "get everything" receives 1000 and no complaint.
- A client that computes
- Read
returnedrather than the length you asked for.
Pass a cursor back exactly as it arrived. next_cursor pairs a timestamp
with an identity:
-
<RFC 3339>|<Call-ID>for dialogs, -
<RFC 3339>|<Call-ID>#<message index>forsearch_messageshits andget_dialogmessages, -
<RFC 3339>|0xSSRC@src>dstfor thertp_statssweep andreconcile_orphans.
A hand-rebuilt bare timestamp still parses on the dialog tools. So rebuilding
one costs rows instead of raising an error. next_cursor: null marks the
final page.
A page ends at the first bound it reaches, and says which. Two bounds apply to every paged tool:
- the row limit (
limit, clamped to--mcp-max-rows), - the byte budget (
max_response_bytes, clamped to--mcp-max-response-bytes, 1048576 bytes unless the operator changed it).
Either one does three things:
- sets
truncated: true, - names itself in
truncated_by(rowsorbytes), - leaves a
next_cursorthat resumes at the first row the page left out.
truncated_by is absent when neither bound cut the page.
The byte budget works like this:
- It drops whole rows from the end of the page and never part of one.
- A page always keeps at least one row. So following the cursor always moves forward, even when one row alone is larger than the budget.
- It counts the bytes the
sipnab_mcp_tool_response_bytes_totalmetric counts: the JSON page, the provenance note and the two completeness booleans below. - The JSON-RPC framing and the
structuredContentcopy of the page are not counted. - Every paged tool takes it:
list_dialogs,find_problems,search_by_time,search_messages,tail_dialogs,get_dialog,reconcile_orphansand thertp_statssweep.
Every list-style tool answers with a page object, never a bare array.
- The rows sit under a named key (
dialogs,hits,streams,findings) besidereturnedandtotal_matched. - So counting the rows is never necessary and never right.
-
search_messagesandsecurity_findingscarry the page fields too. -
tail_dialogsis the one page object with no total, because a tail cannot have one. Response bounding tabulates it.
Every answer says how much of the capture is behind it. Two booleans ride on every response object:
-
source_exhausted, true once sipnab has read the source to its end, -
source_stopped_early, true when a read ended before the source did. Causes are a truncated dump file, a file that would not open, or a read error part-way through.
The answer is the WHOLE answer only when source_exhausted is true and
source_stopped_early is false. One response tells you that much. So nothing
has to call capture_status to interpret it.
Until then the response omits truncated: false entirely.
- That value claims outright that the page holds every match.
- A caller reading only that field deserves no claim rather than a wrong one.
JSON absence and
falseare different values. - The response still says
truncated: truewhenever the row cap really did keep matches out. That fact holds whatever the load state. -
complete, where a tool has one, likewise never readstrueover a partial read.
A file source loads on a background thread, so a call made right after the
server starts can answer from part of the file. This sends list_dialogs with
no pause, against a sample capture that holds 1334 dialogs:
printf '%s\n' \
'{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"shell","version":"0"}}}' \
'{"jsonrpc":"2.0","method":"notifications/initialized"}' \
'{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"list_dialogs","arguments":{}}}' \
| sipnab --mcp -N -I tests/pcap-samples/sipp-branch-scenario.pcapng --quiet \
| jq -c 'select(.id == 2) | .result.content[0].text | fromjson | {returned, total_matched, truncated_by, source_exhausted}'{"returned":33,"total_matched":33,"truncated_by":null,"source_exhausted":false}jq prints null for truncated_by because the response leaves that field
out. The counts vary from run to run with how far the load had got. The same
call
sent two seconds after notifications/initialized:
{"returned":50,"total_matched":1334,"truncated_by":"rows","source_exhausted":true}When source_exhausted is false, total_matched counts only what had
loaded. Repeat the call, or poll capture_status until source_exhausted
reads true, before treating a count as final.
Twenty-one tools are the exception, because their answer cannot move with the load. How much of the capture sipnab has read says nothing about what their sources hold:
- Twelve read something other than the capture store:
-
explain_response_code,explain_rule,decode_evidence,decode_ng,show_evidence,list_captures,list_tls_libraries,server_capabilities; -
vcon_spool_status, which counts the vCon export spool that the forwarder drains; -
compare_captures, which reads two files and never the loaded capture; -
analyze_vcon, which analyzes a container the caller hands over; -
vcon_directory_report, which reads the containers in a directory of the file root.
-
- The other nine ask another process:
-
query_relayandrelay_statsput the question to the media relay; -
tfps_status,tfps_banned,tfps_dropped,tfps_labels,tfps_banandtfps_unbanput it to the toll-fraud prevention peer; -
fetch_vconputs it to the operator's vCon store.
-
The tools that answer with a rendered document have no object to put a field
in. These are render_ladder, and get_capture_report / get_dialog_report
in markdown and text. So they say it in prose instead.
- A document drawn over a capture that is still loading ends with the same
INCOMPLETE RUNblock--reportappends, naming each reason. - The same holds over a capture whose read stopped before its end.
- A document drawn over a capture read in full says nothing extra. A caveat on every answer is a caveat nobody reads.
Every other tool answers with a JSON object, and the two booleans sit in the
object beside the tool's own fields.
timeline, for example, returns its
buckets inside an object that carries both.
Capture text arrives fenced, and identifiers do not.
- Free text an endpoint wrote comes wrapped in
⟦untrusted-capture-data⟧…⟦/untrusted-capture-data⟧. That covers display names,User-Agent, SDP and whole messages. - The tools that emit it append a provenance note as the LAST content block.
- Call-IDs, cursors, addresses and timestamps stay verbatim. So they pass straight into the next call.
Untrusted capture text gives the per-tool breakdown. Every JSON sample on the bundle pages shows the markers where the server really emits them.
list_dialogs, find_problems, tail_dialogs and search_by_time take an
optional fields array. Each row then carries only the fields you name, which
makes a large page smaller. limit, --mcp-max-rows and cursors bound the
number of rows, and fields bounds what each row carries.
Ask for two fields on each dialog in the paging sample capture from Reproduce the examples below:
{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"list_dialogs","arguments":{"limit":2,"fields":["state","final_status_code"]}}}The dialogs array in the answer:
[{"call_id":"[email protected]","final_status_code":403,"state":"Registered"},{"call_id":"[email protected]","final_status_code":403,"state":"Registered"}]The rules:
-
call_idis always kept, listed or not, so you can pass every row to a follow-up tool. -
An unknown name fails with
invalid_params(-32602). The message anddata.valid_valueslist the fields the row carries:{"code":-32602,"message":"unknown field 'stat'; this row carries: call_id, created_at, duration_sec, final_status_code, frame, from_user, input_origin, method, msg_count, state, timing, to_user, updated_at","data":{"param":"fields","reason":"invalid_choice","valid_values":["call_id","created_at","duration_sec","final_status_code","frame","from_user","input_origin","method","msg_count","state","timing","to_user","updated_at"]}} -
The page fields are always kept.
total_matched,truncated,next_cursorandcapture_identitystay on the response whateverfieldsnames. -
A field a row does not carry stays absent. It does not appear as
null.
These tools all require --mcp-file-root <DIR> and refuse to run without it:
-
list_captures,export_capture,export_audio, -
compare_captures, -
build_evidence_package.
They take a bare filename, never a path.
-
generate_reprojoins them the moment you give it afilename. -
decode_evidencepushes the source half of a frame pointer through the same guard.
sipnab refuses ../x, /etc/passwd and sub/dir.pcap before any filesystem
call, with invalid_params and data.reason path_refused:
{"code":-32602,"message":"'../x' is not a bare filename. These tools take a name, not a path, and write only inside the configured --mcp-file-root.","data":{"reason":"path_refused"}}sipnab also refuses a symbolic link inside the root that resolves outside the root. It checks where the name points, not how the caller wrote it:
{"code":-32602,"message":"'link' resolves outside the configured --mcp-file-root. The name is a link to '/tmp', and these tools write only inside the root.","data":{"reason":"path_refused"}}Each tool returns the resolved path of the file it wrote. Without
--mcp-file-root, every file tool answers not_enabled:
{"code":-32602,"message":"file tools are disabled: start sipnab with --mcp-file-root <DIR> to enable them","data":{"reason":"not_enabled"}}The boundary stops an escape, and an overwrite. Inside the root, these refuse a filename that already exists:
export_capture,export_audio,build_evidence_package,generate_repro'sfilename,shutdown_server'ssave_to.They name the file and ask for one that is free (
data.reasonalready_exists). Choose another name, or calllist_capturesto see which names a directory already uses.The guard covers every file in the root, not only the capture the run is reading: the captures staged there for
open_captureand every earlier export keep their contents too.
find_problems.kinds — diagnostic alias names, OR-ed together.
Defaults to ["problems"]. The full vocabulary:
problems · slow-setup · short-calls · one-way · nat-issues ·
codec-asym · ptime-asym · payload-asym · duration-asym ·
late-media
An unknown alias returns invalid_params (-32602) naming the alias, with
the vocabulary in data.valid_values:
{"code":-32602,"message":"unknown alias 'bogus'","data":{"param":"kinds","reason":"invalid_choice","valid_values":["problems","slow-setup","short-calls","one-way","nat-issues","codec-asym","ptime-asym","payload-asym","duration-asym","late-media"]}}The same names work as the list_dialogs filter aliases (and as
sipnab --filter aliases on the CLI).
security_findings.kinds matches the rule names sipnab records findings
under: scanner, fraud, digest, reg_flood.
-
Write
reg_floodwith an underscore. The--alertrule grammar spells itreg-flood, but sipnab records and filters findings asreg_flood. -
Omitted or empty
kindsreturns findings of every kind. -
sipnab refuses an unknown name and lists the four names. For
reg-floodthe refusal also suggests the underscore spelling:{"code":-32602,"message":"unknown kind 'reg-flood', expected one of: digest, fraud, reg_flood, scanner (did you mean 'reg_flood'?)","data":{"reason":"invalid_value"}}
All tools return MCP errors via the JSON-RPC error object. The codes
sipnab uses:
| Code | Meaning |
|---|---|
-32602 (invalid_params) |
Unknown Call-ID, out-of-range index, malformed filter, unknown format, unknown alias, a path where a bare filename belongs, and every "this tool is not enabled" refusal. The common case by far. |
-32603 (internal_error) |
A read or write that reached the filesystem or the decoder and failed there — export_audio with no retained payload, a capture file sipnab cannot open. Not only for bugs. |
| -32000 (server error) | Capacity, not correctness: --mcp-max-concurrent or --mcp-rate-limit-per-peer turned this call away, and the same call succeeds once the server has room. The only code here worth a retry — treat -32602 as a bug in the request. |
Tools never panic. An unknown Call-ID always produces a structured error rather than an empty result.
Every -32602 and -32000 error also carries a data object. So a client can
act on the error without parsing message.
- The
messagetext stays as it was. - People read
message, and its wording may improve between releases. - The
datacodes below stay stable.
| Member | Present | Meaning |
|---|---|---|
reason |
always | A stable lowercase code from the table below. |
param |
when one argument caused the refusal | The argument's name, as the tool's input schema spells it. For a resource read, the URI template variable (tool, alias, rule_id) or uri. |
valid_values |
when the refusal is "not one of these" | The values the server accepts for that argument. |
retry_after_ms |
on a rate-limit refusal | Milliseconds until the current one-second window ends and the same call can succeed. |
sipnab omits a member that does not apply, rather than sending it as null.
reason |
Code | Meaning |
|---|---|---|
not_found |
-32602 | A named thing (a Call-ID, a rule, a finding, a sweep job, a subscription) is not held by this server. |
invalid_choice |
-32602 | The value is not in the allowed set; valid_values lists the set. |
invalid_value |
-32602 | The value is empty, out of range, or not in the expected format. |
invalid_filter |
-32602 | A filter expression did not compile. |
missing_param |
-32602 | A required argument, or one of a set of alternatives, was not given. |
conflicting_params |
-32602 | The call named two arguments that cannot appear together. |
not_enabled |
-32602 | The operator did not enable this on the server: an opt-in flag such as --mcp-allow-shutdown, --mcp-file-root, or --allow-action. |
path_refused |
-32602 | sipnab refused a filename: not a bare name, outside --mcp-file-root, or one of the capture's own inputs. |
already_exists |
-32602 | The filename asked for already exists, and sipnab does not overwrite. |
not_ready |
-32602 | The server's current state refuses the request (a capture is still loading, a live capture is running, the source has not finished). The same call can succeed later. |
limit_exceeded |
-32602 | The request asks for more than a ceiling this server enforces. |
unsupported |
-32602 | This build, platform, or process privilege cannot do this. |
scope_refused |
-32602 | The caller's token scope does not cover this tool. |
declined |
-32602 | The person asked to confirm the request (MCP elicitation) declined it. |
no_data |
-32602 | The named call holds nothing of the kind asked for: no messages, no RTP streams, no request to replay. |
unknown_tool |
-32602 | The tool name is not loaded on this server; valid_values lists the loaded names. |
invalid_arguments |
-32602 | Arguments that do not match the tool's input schema (a wrong JSON type, a missing required key), an argument this server refuses whatever its value (a password-like argument name), or a refusal the MCP SDK built without naming the argument. |
at_capacity |
-32000 |
--mcp-max-concurrent calls are already running. There is no retry_after_ms, because a slot frees when some running call ends and the server cannot know when that happens. |
rate_limited |
-32000 | This peer used its --mcp-rate-limit-per-peer allowance for the current one-second window. retry_after_ms says when the window ends. |
Arguments that do not match a tool's input schema at all (a wrong JSON type,
a missing required key) fail the same way, with invalid_arguments and the
parse error as the message:
{"jsonrpc": "2.0", "id": 3, "error": {"code": -32602, "message": "failed to deserialize parameters: missing field `call_id`", "data": {"reason": "invalid_arguments"}}}| Limit | Default | Set by |
|---|---|---|
Default limit for list-style tools |
50 | the per-call limit
|
Ceiling on limit (clamps higher requests) |
1000 | --mcp-max-rows |
| SIP body / snippet bytes | 4096 | --mcp-max-body-bytes |
Messages per get_dialog page |
100 | the per-call max_messages, up to --mcp-max-rows
|
| Bytes of one whole paged response | 1048576 | the per-call max_response_bytes, up to --mcp-max-response-bytes
|
Set the ceilings for the client that reads the answers: lower for an agent
with a small context window, higher for a batch client that writes answers to
a file. Raise or lower them with --mcp-max-rows, --mcp-max-body-bytes and
--mcp-max-response-bytes, or the matching [limits] keys in the config
file. The per-call limit and max_response_bytes narrow an answer below the
ceiling, and cannot exceed it.
The row and body ceilings bound a row's count and a body's width. Neither bounds the response as a whole.
- Fifty
get_dialogmessages that each carry a long SDP body are within both. They can still exceed what a small model can read. - The byte budget covers that case. Its default is 1048576 bytes (1 MiB).
Lower it per call with
max_response_bytes, or for every call with--mcp-max-response-bytes.
A bound is not a loss. These tools each report total_matched beside their
page:
-
list_dialogs,find_problems,search_by_time,search_messages,security_findings, - the capture-wide
rtp_statssweep.
So a caller sees how much of the answer it holds.
That number describes the STORE, not the file.
- While the source is still loading, it counts what had arrived by then.
- That is why every one of them also carries
source_exhausted. - It is also why the response omits
truncated: falseuntil the answer is whole.
All of those except security_findings carry a cursor to the rest. So do
tail_dialogs, get_dialog and reconcile_orphans. Every one of the eight
cursor tools takes max_response_bytes.
Asking for a limit above the ceiling does nothing, because the ceiling
clamps it. So either raise the ceiling with --mcp-max-rows or page.
Some tools take limit and carry no cursor, and a caller has to know
which:
| Tool | Reports a total | Why no cursor | How to reach the rest |
|---|---|---|---|
security_findings |
Yes, total_matched
|
Its source is a ring buffer whose default depth is 1000, the same as the maximum limit, so one call reaches all of it |
Raise limit, and narrow with since
|
find_correlated |
Yes, total_matched
|
A ranking by score: a later page would hold weaker matches, not the rest of a list | Raise limit
|
get_call_tree |
No, truncated only |
A breadth-first walk outward from one call, stopped at limit legs |
Raise limit
|
top_talkers |
Yes, distinct_talkers
|
A ranking by dialog count | Raise limit
|
describe_endpoint |
Yes, dialogs
|
recent_dialogs is newest first by updated_at, which moves each time a call gains a message, so a cursor on it would skip rows |
list_dialogs with a filter such as src.ip == '192.0.2.10' or from.user == 'alice' pages that endpoint's dialogs |
hep_senders |
Yes, senders_tracked and refused_sources_tracked
|
Two lists in one answer, the refused sources ranked by count, and the same bytes GET /v1/hep/senders returns |
Raise limit, and raise --mcp-max-rows as well when the listener tracks more than 1000 senders |
tfps_labels |
No: total counts the rows TFPS returned, and truncated says it held more |
TFPS's log command takes a row count and no offset, so there is no position to resume from |
Raise limit, or run tfps_ctl log --json --limit N on the TFPS host |
tail_dialogs |
No | It has a cursor. The table names it because it has no total | Follow next_cursor until the page comes back empty |
aggregate_dialogs, group_dialogs and compare_captures take top_n rather
than limit.
- They sum everything past it into
other_countorother. - So they withhold nothing silently, and have no cursor either.
tail_dialogs reports no total, because a tail cannot have one. The store
keeps changing underneath it. So any number it gave would describe a moment
that has passed.
Each tool section on a bundle page names the capture its example ran against. Start a server on that capture, then call the tool. These two cover most of the examples:
# The paging examples: 1334 dialogs, no RTP.
sipnab -N --mcp --mcp-transport http --mcp-bind 127.0.0.1:8731 \
--mcp-file-root tests/pcap-samples \
-I tests/pcap-samples/sipp-branch-scenario.pcapng# The media examples: two calls (one completed, one still in progress at the end of the file) and two RTP streams.
sipnab -N --mcp --mcp-transport http --mcp-bind 127.0.0.1:8731 \
--mcp-file-root tests/pcap-samples \
-I tests/pcap-samples/sip-rtp-g711.pcapDrive either one with the raw HTTP test on the deployment page, or point
a client at http://127.0.0.1:8731/mcp. A loopback bind needs no token.
Numbers in the samples are what those captures produce. A file replay reads packet timestamps rather than arrival times, so jitter and MOS come out identical on every run. A value that does not match points at a change in the binary or the capture.
{ "code": -32602, "message": "cannot group by 'bogus'; one of: state, response_code, ...", "data": { "reason": "invalid_choice", // always present "param": "group_by", // the argument that was refused, when one was "valid_values": ["state", "response_code", "method", "..."] // the allowed set, when there is one } }