Repository navigation
MCP Tools vCon
The vcon bundle exports, validates and analyzes vCon conversation
containers, reports a directory of them, reads one from the operator's vCon store, counts the vCon export
spool, and reports the SIPREC recording metadata a call carried.
Load only these tools with --mcp-tools vcon, 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 |
|---|---|---|
siprec_metadata |
call_id |
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 |
export_vcon |
call_id?, filter?, limit?, cursor?, media?
|
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 |
validate_vcon |
call_id?, container?
|
Checks a container against the working group's draft-ietf-vcon-vcon-core-04 schema and names every finding |
analyze_vcon |
container?, container_base64?, limit? (one of the first two) |
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 |
fetch_vcon |
uuid |
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 |
vcon_directory_report |
directory, limit?, cursor?, max_response_bytes?
|
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 |
limit?, cursor?, rule?
|
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 |
You have a call-id. These explain what happened to that one call -- its signaling, its media, and the specific negotiations that tend to break.
"Was this call recorded, and who is on the recording?" A session recording
client (SRC) — OpenSIPS's siprec module, an SBC — sends the recording server
an INVITE whose body carries an application/rs-metadata+xml part describing
what it is about to record. sipnab reads that part off the wire and returns it.
| Name | Type | Legal values | If omitted |
|---|---|---|---|
call_id |
string | A Call-ID the store holds. | Required — the call fails. |
Read recorded first. It is false when the dialog carried no SIPREC
metadata, and the response then holds only call_id, reason,
capture_identity and schema_version. The reason says what that does and
does not mean: sipnab reports what reached the capture point, so a recorder
signaling on a path it cannot see is indistinguishable here from a call nobody
recorded. It is not a statement that the call went unrecorded.
Otherwise siprec carries:
| Field | Answers |
|---|---|
session_id |
The recording session the SRC opened, which ties two legs of one recording together |
mode |
What the SRC declared it is recording, from <datamode> — complete for the whole session |
participants |
Each party, with participant_id, aor and name when the SRC sent one. A missing name is absent, not empty: an SRC that has no display name writes a self-closing <nameID>
|
streams |
Each recorded stream, with stream_id, label and participant_id
|
Use label and participant_id to tie a recorded stream to a person.
The label is the SDP a=label naming the m= line the SRC cut the recorded
stream from, so on a call with audio and video it says which is which. The
participant_id is the party that sends that stream. The stream element
itself names no party. sipnab reads it from <participantstreamassoc>, taking
the <send> children and not the <recv> children.
The example runs against
tests/pcap-samples/siprec-opensips-invite.pcap:
Against a call with no SIPREC body, such as 1-1966@10.0.2.20 in
tests/pcap-samples/sip-rtp-g711.pcap, the answer is:
// siprec_metadata { "call_id": "1-1966@10.0.2.20" }
{
"schema_version": 1,
"call_id": "1-1966@10.0.2.20",
"recorded": false,
"reason": "this dialog carried no SIPREC metadata. That means none reached the capture point -- a recorder signaled on a path sipnab does not see looks the same here as a call nobody recorded.",
"capture_identity": { … },
"source_exhausted": true,
"source_stopped_early": false
}sipnab reads SIPREC and does not take part in it: it is neither a session recording client nor a recording server.
Getting the result out of sipnab and into the next tool, the next team, or a bug report someone else can reproduce.
One observed dialog as a vCon conversation container
(draft-ietf-vcon-vcon-core,
syntax 0.4.0), the interchange format for a conversation record.
--export-vcon writes the same container from the CLI.
This tool writes no file and needs no --mcp-file-root. It returns the
container inline and is readOnlyHint: true.
| Name | Type | Legal values | If omitted |
|---|---|---|---|
call_id? |
string | A Call-ID the store holds. An unknown one fails with invalid_params (-32602) naming the value. |
Give filter instead. |
filter? |
string | A filter alias or a raw DSL expression, the vocabulary list_dialogs takes. The --export-vcon-when flag takes the same vocabulary, aliases included. Every matching dialog comes back. |
Give call_id instead. |
limit? |
integer | 1 to --mcp-max-rows (1000 by default). |
50. |
cursor? |
string | The previous response's next_cursor, passed back verbatim. It is the <RFC 3339 created_at>|<Call-ID> cursor list_dialogs uses. A cursor whose time half is not RFC 3339 fails with invalid_params (-32602). |
The first page. |
media? |
string |
inline: audio the run retained travels inline in a recording Dialog Object. omit: the exporter considers no audio, and each container records media: "not-considered". sipnab refuses any other value before the tool runs, with an error result that names inline and omit. |
inline. |
Give call_id or filter, never both. The CLI refuses the same pair. Both
fail with invalid_params:
`call_id` and `filter` are alternatives, not a pair: one names a dialog, the
other names a rule for choosing them. Give one
Neither fails with invalid_params too:
give `call_id` for one dialog, or `filter` for every dialog it selects -- for
example "response_code >= 400"
Export a set in one call. To export every failed call, pass a filter
instead of calling the tool once per Call-ID. limit bounds the set, and
total_matched counts every matching dialog in the store, so page against
it.
The tool withholds a dialog the run denies. On a run started with
--content-deny-header NAME, the tool does not export a dialog whose messages
carry that header, as the batch export does not write it. Asked for that
dialog by call_id, the tool refuses with not_enabled and names the header.
Under a filter, the tool leaves such dialogs out, and withheld_by_deny says
how many it left out.
Page through a large set. Containers come back ordered by the dialog's
created_at, then Call-ID. When more match than limit allows, truncated
is true and next_cursor names the last container returned. Pass it back as
cursor for the next page. The last page has truncated: false and a null
next_cursor. The cursor is a position within one capture: compare
capture_identity.instance between pages, as with every paged tool, because
open_capture replaces the dialogs a cursor points into.
Leave the audio out. A container that carries audio is up to the
inline-media budget (5 MiB by default) larger than one that does not. With
media: "omit" the exporter considers no audio, and each container says so in
its own completeness carrier with media: "not-considered" and a caveat that
names the omission as a fact about the export, not about the call. The
containers stay valid against the core-04 schema.
Read one container as a resource. resources/read on
sipnab://vcon/{call_id} returns the container this tool builds for that
dialog, with audio inline, and completion/complete fills in call_id from
the Call-IDs the capture holds. sipnab refuses a container over the 8 MiB
resource limit with invalid_params. Call this tool for it instead, with
media: "omit" if the audio is what makes it large. Only a build with the
vcon feature lists the template.
A filter matching nothing answers with an empty set (returned: 0,
total_matched: 0), not an error.
There is no format parameter: the answer is always the JSON container the
draft defines.
What the container contains, and what it leaves out. The container is an observer vCon and says so in its own parties: sipnab read the traffic, and did not place the call, record it, or obtain anyone's permission to keep it. The design record, vCon design, gives the reasons. As a result:
- The container is neither signed nor encrypted.
-
Every party carries
validation: "none".FromandToare what the sender wrote. sipnab writes anamewhen the wire carried a display name, besidesip_display_name. Treat the name as personal data: a redaction step must covernameas well assip_display_name. - There is no consent or lawful-basis attachment. sipnab was not in a position to record consent, so the absence says nothing about the call.
-
There is no
url. Media travels inline or not at all.
How the container carries audio. dialog.type: "recording" marks a Dialog
Object that carries media. sipnab reconstructed that audio from RTP at the
capture point. It is not a recording made by the endpoints, and every WAV
sipnab writes says so. Audio the run retained (--retain-audio) travels
inline: base64url, mediatype: "audio/x-wav", and a content_hash of sha512- plus the base64url SHA-512 of
the DECODED bytes, which sha512sum on the exported .wav reproduces. The
duration on that object is the FILE's, never the call's. sipnab keeps whole
calls by default. When frames are missing from the start of the file (an
explicit [limits] max_audio_frames cap dropped them, or the
[limits] max_audio_bytes budget released them), a recording-set wraps it,
carrying the call's media window beside the file's duration.
capture_completeness.media says which of five things happened to the audio:
media |
What it means |
|---|---|
carried |
The audio is inline in a recording Dialog Object |
refused-over-budget |
sipnab decoded audio and did not inline it, because the encoded body is over the inline-media budget (--vcon-max-inline-media, 5 MiB by default). The audio exists and was not truncated. Export it with export_audio
|
none-decodable |
The run decoded no audio. media_note reports the measurement -- never that the call was silent |
not-considered |
Nobody asked this export for media: the call to export_vcon gave media: "omit". A fact about the export, not about the call |
withheld-by-redaction |
The export ran with --redact, and redaction deletes audio rather than replacing it with a token, so the exporter considered none. The Dialog Object is the signaling one. A fact about the export, not about the call. The MCP tools do not redact, so they never write it |
What the capture missed travels with the container, in two places: the
capture_completeness object in the analysis entry, and the
sipnab-capture-completeness attachment. Both carry the same value. Read
capture_completeness.note before the contents: each clause in it is a
measurement of what this run read and dropped.
Every container comes back with its SHA-256, computed as --vcon-digest
computes it over the container's serialized bytes, so it compares directly
against a SHA256SUMS line or a store's ledger entry.
The digest identifies the document, not the dialog. created_at records when
sipnab wrote the container, so re-exporting one call produces a new digest and
the same uuid. Deduplicate on the uuid.
The response repeats what each container omits, in completeness, so a
caller does not have to open the container's attachments to learn it.
completeness.note is the container's own caveat, verbatim.
completeness.max_inline_media_bytes is the budget this run enforced. completeness.omissions carries one row per loss, each
naming the counter it came from, how many, and in what unit. An empty list and
complete: true say the container lost nothing.
The response is an envelope, so one dialog and a filtered set arrive in one
shape. The example runs against
tests/pcap-samples/sip-problem-call.pcap,
which holds four failed calls:
// export_vcon { "filter": "response_code >= 400", "limit": 2 }
{
"schema_version": 1,
"returned": 2,
"total_matched": 4,
"truncated": true,
"next_cursor": "2023-11-14T22:13:26Z|decline-7c6d5e@198.51.100.30",
"withheld_by_deny": 0,
"capture_identity": { /* which capture, and which revision of its stores */ },
"source_exhausted": true,
"source_stopped_early": false,
"containers": [
{
"call_id": "busy-3a2b1c@192.0.2.30",
"digest": "1c9e5fcfe825f9e90477c1d4fc49c23de6ceaeb8fcf6a60b141cfd563ed1bce6",
"completeness": {
"note": "Produced by sipnab 0.5.222 on node capture-01. sipnab OBSERVED this dialog and took no part in it: … sipnab read 23 frame(s) for this capture. No omissions recorded: every message sipnab held for this dialog is in this container. A capture-level analysis ran and ranked no blind spots.",
"media": "none-decodable",
"media_note": "No RTP streams to export",
"max_inline_media_bytes": 5242880,
"complete": true,
"omissions": []
},
"container": { /* the vCon itself; one is shown in full below */ }
},
…
]
}A container that lost something fills the same block differently. With
--retain-audio --vcon-max-inline-media 0, export_vcon { "call_id": "1-1966@10.0.2.20" } against tests/pcap-samples/sip-rtp-g711.pcap returns:
"completeness": {
"note": "Produced by sipnab 0.5.222 on node capture-01. … No media is in this container because sipnab REFUSED to inline it: it decoded audio and the encoded body was over the budget below. The audio exists and was not truncated. sipnab read 852 frame(s) for this capture. — INCOMPLETE: sipnab decoded 8.5 second(s) of audio for this dialog and REFUSED to carry it: base64url of the 136516 byte WAV is 182022 bytes, over the 0 byte budget this emitter enforces because one probed vCon store answers 204 and drops the payload without telling the producer. The audio was NOT truncated and is not in this container; export it beside this file with the export_audio MCP tool. A capture-level analysis ran and ranked no blind spots.",
"media": "refused-over-budget",
"media_note": "Produced by sipnab 0.5.222 via mechanism sipnab-capture. …",
"max_inline_media_bytes": 0,
"complete": false,
"omissions": [
{ "kind": "media_refused_over_budget", "count": 1, "unit": "recording" }
]
}omissions has one row per — INCOMPLETE: clause in the note. The same
counters produce both, so either one gives the whole account.
The container example runs against
tests/pcap-samples/sip-rtp-g711.pcap,
on a server started with --node-name capture-01 and without
--retain-audio:
// the "container" member of export_vcon { "call_id": "1-1966@10.0.2.20" }
{
"vcon": "0.4.0",
"uuid": "0158a120-0392-8940-a7b4-dd42980ec66e",
"created_at": "2026-10-16T12:03:45.845914820+00:00",
"subject": "SIP call 1-1966@10.0.2.20",
"extensions": ["sip-signaling", "CC"],
"parties": [
{
"name": "PCMU/8000",
"sip": "sip:sipp@10.0.2.20:5060",
"sip_contact": "sip:sipp@10.0.2.20:5060",
"sip_display_name": "PCMU/8000",
"validation": "none"
},
{
"name": "test",
"sip": "sip:test@10.0.2.15:5060",
"sip_contact": "<sip:test@10.0.2.15:5060;transport=udp>",
"sip_display_name": "test",
"sip_user_agent": "FreeSWITCH-mod_sofia/1.6.12-20-b91a0a6~64bit",
"validation": "none"
},
{
"role": "observer",
"sip_user_agent": "sipnab/0.5.222 (observer; node capture-01)",
"validation": "none"
}
],
"dialog": [
{
"type": "recording",
"start": "2016-11-26T14:52:59.666393+00:00",
"duration": 8.499,
"parties": [0, 1],
"party_history": [
{ "event": "join", "party": 0, "time": "2016-11-26T14:52:59.670743+00:00" },
…
],
"sip_call_id": "1-1966@10.0.2.20",
"sip_cseq": 1,
"sip_from_tag": "1",
"sip_to_tag": "QvN92t713vSZK"
}
],
"attachments": [
{
"purpose": "sip-message-trace",
"dialog": 0,
"party": 2,
"start": "2016-11-26T14:52:59.666393+00:00",
"mediatype": "application/json",
"encoding": "json",
"body": { "schema_version": 1, "version": "1.0", "call_id": "1-1966@10.0.2.20", "sip_call_id": "1-1966@10.0.2.20", "messages": [ /* the same objects --json emits, one per message */ ] }
},
{
"purpose": "sipnab-capture-completeness",
"dialog": 0,
"party": 2,
"start": "2016-11-26T14:52:59.666393+00:00",
"mediatype": "application/json",
"encoding": "json",
"body": {
"note": "Produced by sipnab 0.5.222 on node capture-01. sipnab OBSERVED this dialog and took no part in it: … sipnab read 852 frame(s) for this capture. No omissions recorded: every message sipnab held for this dialog is in this container. A capture-level analysis ran and ranked no blind spots.",
"node": "capture-01",
"sipnab_version": "0.5.222",
"frames_read": 852,
"undecodable_frames": 0,
"sip_discarded_by_port_gate": 0,
"sip_discarded_by_websocket_gate": 0,
"messages_evicted": 0,
"headers_dropped_oversize": 0,
"dialogs_refused": 0,
"dialogs_rotated": 0,
"dialogs_suppressed_by_deny": 0,
"gate_closed_during_run": false,
"call_ended": true,
"blind_spots": [],
"media": "none-decodable",
"media_note": "No audio payload retained: sipnab measured 425 RTP packet(s) of PCMU on 1 decodable stream, but kept none of their payload, so there is nothing to decode. This is a statement about what this run kept, not a finding that the call was silent. …"
}
},
{
"purpose": "sipnab-related-legs",
"dialog": 0,
"party": 2,
"start": "2016-11-26T14:52:59.666393+00:00",
"mediatype": "application/json",
"encoding": "json",
"body": { "legs": [ { "uuid": "0158a120-253e-8217-9d04-6a2a4a9edef7", "strategy": "sdp_origin", "score": 90, "identifier_match": true } ] }
}
],
"analysis": [
{
"type": "report",
"dialog": 0,
"vendor": "sipnab",
"product": "sipnab 0.5.222 (passive observer; not a recording system)",
"schema": "sipnab-dialog-diagnosis/1",
"mediatype": "application/json",
"encoding": "json",
"body": { "schema_version": 1, "sip_call_id": "1-1966@10.0.2.20", "final_status_code": 200, "media_quality": [ /* one row per stream: codec, packets, jitter_ms, loss_pct, mos, r_factor */ ], "capture_completeness": { /* the same object the attachment carries */ } }
}
]
}An unknown Call-ID answers with an error, never an empty container:
// export_vcon { "call_id": "no-such@nowhere" }
{ "code": -32602, "message": "call_id 'no-such@nowhere' not found" }A filter that selects nothing answers with an empty set instead.
How to read three fields.
blind_spots: [] means a capture analysis ran and ranked nothing. An absent
blind_spots means no analysis ran. This tool always runs it.
The observer party is always the last entry in parties, and every
attachment's party index points at it. Find it by position from the end or
by role: "observer", not by a fixed index: a container that names more
parties moves it.
uuid is a UUIDv8 whose timestamp half is the dialog's start, not the
export's, so re-exporting one dialog out of one capture keeps one identifier.
Its other 74 bits are digests keyed on the Call-ID, the capture identity and
the node name, so two dialogs that opened in the same millisecond on the same
node still differ.
A binary built without the vcon Cargo feature does not offer this tool:
it is absent from tools/list.
server_capabilities lists what a given binary carries.
Checks a vCon container against the working group's
draft-ietf-vcon-vcon-core-04 schema, vendored unchanged
(tests/schemas/vcon.schema.json).
Run it before handing containers to a store: a store that refuses a container reports the refusal to the client that sent it, not to the producer.
| Name | Type | Legal values | If omitted |
|---|---|---|---|
call_id? |
string | A Call-ID the store holds. sipnab exports its container, then checks it. | Give container instead. |
container? |
object | A container you already hold — one sipnab wrote, one another producer wrote, one a store rejected. It must be the object itself, not a string holding it. | Give call_id instead. |
Give one, never both.
Two verdicts.
verdict |
What it means |
|---|---|
valid |
Nothing disagrees with the schema |
invalid |
At least one finding. errors carries each one |
Each finding names the JSON Pointer of the offending value
(instance_path), the schema keyword that refused it (keyword) and what was
wrong (detail). The response is schema_version 2. With call_id it also
carries capture_identity, naming the capture sipnab exported the container
from. With container it does not, because that container did not come from
this capture. An empty Dialog Object
{} is an ordinary error:
section 4.3 of core-04
requires a placeholder that names its type.
// validate_vcon { "call_id": "1-1966@10.0.2.20" }
{
"schema_version": 2,
"verdict": "valid",
"schema_path": "tests/schemas/vcon.schema.json",
"schema_id": "https://ietf.org/vcon/schemas/unsigned-vcon.json",
"errors": [],
"call_id": "1-1966@10.0.2.20",
"capture_identity": { "node": "capture-01", "instance": "cc2cb18df0ea68adfb9c9-2",
"dialog_generation": 13, "stream_generation": 2 },
"source_exhausted": true,
"source_stopped_early": false
}A rule core-04 added: parties MUST NOT be present on a transfer Dialog
Object (section 4.3.4):
// validate_vcon { "container": { "uuid": "018f3a2b-4c5d-8e6f-9012-3456789abcde", "created_at": "2026-09-01T12:00:00Z", "dialog": [ { "type": "transfer", "parties": [0, 1] } ] } }
{
"schema_version": 2,
"verdict": "invalid",
"schema_path": "tests/schemas/vcon.schema.json",
"schema_id": "https://ietf.org/vcon/schemas/unsigned-vcon.json",
"errors": [
{
"instance_path": "/dialog/0",
"keyword": "not",
"detail": "carries `parties`, which the schema forbids here"
}
],
"source_exhausted": true,
"source_stopped_early": false
}A container that disagrees with the schema is an ANSWER, not a tool error. The
call fails with invalid_params when the REQUEST is wrong: neither
argument, both arguments, an unknown call_id, or a container that is not a
JSON object. It also fails with invalid_params (reason: invalid_value,
param: container) for a container whose critical lists an extension sipnab
does not support, naming each one:
section 4.1.4 of draft-ietf-vcon-vcon-core-04 says a reader "MUST NOT attempt to process or operate on the
vCon, except to reject it or report unsupported content". sipnab supports
sip-signaling and CC. With "critical": ["foo"] the message reads:
sipnab must not validate this vCon container: its critical parameter lists
foo, which sipnab does not support, and draft-ietf-vcon-vcon-core-04 section
4.1.4 forbids processing a vCon that lists an unsupported critical extension.
sipnab supports sip-signaling and CC
The validator reads the vendored schema file and implements the draft-07
subset that file uses. A schema keyword outside that subset makes every
validation report invalid, naming the keyword.
A binary built without the vcon Cargo feature does not offer this tool:
it is absent from tools/list.
server_capabilities lists what a given binary carries.
Analyzes a stored vCon container you hand over: one sipnab exported, or one
another producer wrote. POST /v1/vcon/analyze
gives the same answer for the same container.
sipnab rebuilds the container's SIP messages and analyzes them in stores of
their own. The tool leaves the loaded capture untouched, records nothing, and
connects nowhere. It reads two stored forms: a sip-message-trace attachment (sipnab's,
and the trace form of draft-howe-vcon-sip-signaling-00) and message/sip
attachments, one whole message each. It opens a signed, encrypted or gzip
format container first, with the server's --vcon-key, --vcon-signer-ca and
--vcon-signer-sha256: see
A signed, encrypted or compressed container.
To analyze a container on the command
line, give it to -I instead: see
Analyze a stored vCon.
| Name | Type | Legal values | If omitted |
|---|---|---|---|
container? |
object | The container itself, not a string holding it, in the unsigned, signed or encrypted form. | Give this or container_base64. With neither, the call fails with invalid_params. |
container_base64? |
string | The container's bytes in standard base64, in any form, the gzip format included. | Give this or container. With both, the call fails with invalid_params. |
limit? |
integer | The most dialogs to return. Ceiling --mcp-max-rows. |
50. total_dialogs counts every dialog the container holds, and truncated says whether the limit cut the list. |
Two verdicts.
verdict |
What it means |
|---|---|
analyzed |
sipnab rebuilt at least one SIP message, and the answer holds the analysis |
unreadable |
The container does not open (an encrypted container and no --vcon-key, a signature that does not verify, a signer the checks refuse), stores no SIP message sipnab can rebuild, is not a vCon at all, or lists in critical an extension sipnab does not support (sipnab supports only sip-signaling and CC; section 4.1.4 of draft-ietf-vcon-vcon-core-04). reason says which, and the answer holds no analysis. For an unsupported critical extension it holds no schema findings either |
A schema finding does not stop the analysis. schema_findings lists every
place the container disagrees with the vendored draft-ietf-vcon-vcon-core-04
schema, in the shape validate_vcon reports them, beside
either verdict. A container that went through vcon.store, which hands
extensions back as an object, draws one finding and an analysis.
The answer lists what a container cannot reproduce, and fills none of it in.
not_recomputed holds one sentence per gap. A vCon stores no RTP, so sipnab
computes no stream, MOS, jitter or loss. The producer's own media_quality
rows come back verbatim in stored_media_quality, as its numbers. The trace
stores no request-URI, so a rebuilt request carries
sip:request-uri-not-stored.invalid. It keys headers by name, so header order
across names does not survive. A body other than SDP, and headers the producer did not
store, are absent. The capture analysis in analysis opens with a
rebuilt_from_vcon finding that says the same, beside the producer's own
caveat (producer_note).
Each rebuilt message carries input_origin: "vcon" and no frame pointer,
because it did not come from a capture frame.
Real output for the container --export-vcon 1-1966@10.0.2.20 --node-name tap-a
writes from tests/pcap-samples/sip-rtp-g711.pcap:
// analyze_vcon { "container": <the contents of call.vcon.json> }
{
"analysis": {
"complete": false,
"dialogs_examined": 1,
"findings": [
{
"evidence": [
{
"counts": {
"messages": 6
},
"endpoints": [
"inline",
"vcon 0158a120-0392-8940-8117-7af10a0d166e"
],
"note": "6 SIP message(s) rebuilt from this container. Not recomputed or not stored: media: a vCon carries no RTP, so no stream, MOS, jitter or loss was computed from it; the producer's analysis holds 1 media_quality row(s), its own numbers, not recomputed; request-URI: not stored for 3 request(s), which carry sip:request-uri-not-stored.invalid; header order: the trace keys headers by name, so their order across names is the container's, not the wire's, and headers the producer did not store (credential headers, when sipnab produced it) are absent. The producer's caveat: Produced by sipnab 0.5.222 on node tap-a. sipnab OBSERVED this dialog and took no part in it: the parties below are what the From and To headers said, not identities anyone established, and nothing here is signed. No media is in this container because this run decoded none for the dialog; the capture_completeness media_note says what was measured. That is a statement about what this run kept, not a finding that the call was silent. sipnab read 852 frame(s) for this capture. No omissions recorded: every message sipnab held for this dialog is in this container. A capture-level analysis ran and ranked no blind spots."
}
],
"evidence_omitted": 0,
"kind": "rebuilt_from_vcon",
"occurrences": 1,
"severity": "blind",
"unit": "container"
}
],
"frames_read": 6,
"schema_version": 1,
"streams_examined": 0
},
"dialogs": [
{
"call_id": "1-1966@10.0.2.20",
"dialog": {
"call_id": "1-1966@10.0.2.20",
"diagnosis": {
"hints": [],
"nat_mismatch": false,
"no_media": false,
"one_way_audio": false,
"private_media_address": false
},
"duration_sec": 8.504,
"final_status_code": 200,
"final_status_reason": "OK",
"from": "sipp",
"from_display": "PCMU/8000",
"input_origin": "vcon",
"method": "INVITE",
"msg_count": 6,
"schema_version": 1,
"sdp_timeline": [
{
"codecs": [
"PCMU"
],
"direction": "offer",
"media_addr": "10.0.2.20",
"media_port": 6000,
"mode": "recvonly",
"timestamp": "2016-11-26T14:52:59.666393+00:00"
},
{
"codecs": [
"PCMU",
"telephone-event"
],
"direction": "answer",
"event": "MediaAnchorChange",
"media_addr": "10.0.2.15",
"media_port": 27942,
"mode": "sendonly",
"timestamp": "2016-11-26T14:52:59.670743+00:00"
}
],
"state": "Completed",
"streams": [],
"termination": {
"cause_code": 16,
"cause_text": "NORMAL_CLEARING",
"frame_ref": 4,
"protocol": "Q.850",
"source_header": "Reason"
},
"timing": {
"retransmits": 0,
"setup_ms": 4,
"teardown_ms": 0,
"trying_delay_ms": 0
},
"to": "test",
"to_display": "test"
},
"ladder": [
{
"dst": "10.0.2.15:5060",
"label": "INVITE",
"src": "10.0.2.20:5060",
"timestamp": "2016-11-26T14:52:59.666393+00:00",
"transport": "UDP"
},
{
"dst": "10.0.2.20:5060",
"label": "100 Trying",
"src": "10.0.2.15:5060",
"timestamp": "2016-11-26T14:52:59.666545+00:00",
"transport": "UDP"
},
{
"dst": "10.0.2.20:5060",
"label": "200 OK",
"src": "10.0.2.15:5060",
"timestamp": "2016-11-26T14:52:59.670743+00:00",
"transport": "UDP"
},
{
"dst": "10.0.2.15:5060",
"label": "ACK",
"src": "10.0.2.20:5060",
"timestamp": "2016-11-26T14:52:59.670837+00:00",
"transport": "UDP"
},
{
"dst": "10.0.2.20:5060",
"label": "BYE",
"src": "10.0.2.15:5060",
"timestamp": "2016-11-26T14:53:08.170086+00:00",
"transport": "UDP"
},
{
"dst": "10.0.2.15:5060",
"label": "200 OK",
"src": "10.0.2.20:5060",
"timestamp": "2016-11-26T14:53:08.170676+00:00",
"transport": "UDP"
}
],
"signaling": {
"abandoned": null,
"ack_missing": null,
"auth_loop": null,
"final_failure": null,
"hints": [],
"post_dial_delay": null,
"registration_failure": null,
"retransmissions": null
}
}
],
"messages_rebuilt": 6,
"not_recomputed": [
"media: a vCon carries no RTP, so no stream, MOS, jitter or loss was computed from it",
"the producer's analysis holds 1 media_quality row(s), its own numbers, not recomputed",
"request-URI: not stored for 3 request(s), which carry sip:request-uri-not-stored.invalid",
"header order: the trace keys headers by name, so their order across names is the container's, not the wire's, and headers the producer did not store (credential headers, when sipnab produced it) are absent"
],
"producer_note": "Produced by sipnab 0.5.222 on node tap-a. sipnab OBSERVED this dialog and took no part in it: the parties below are what the From and To headers said, not identities anyone established. No media is in this container because this run decoded none for the dialog; the capture_completeness media_note says what was measured. That is a statement about what this run kept, not a finding that the call was silent. sipnab read 852 frame(s) for this capture. No omissions recorded: every message sipnab held for this dialog is in this container. A capture-level analysis ran and ranked no blind spots.",
"schema_findings": [],
"schema_version": 1,
"stored_media_quality": [
{
"codec": "PCMU",
"jitter_ms": 0.0054046519599899685,
"loss_pct": 0.0,
"mos": 4.358100599599484,
"mos_grounded": true,
"mos_grounding": "published",
"packets": 425,
"r_factor": 90.79987028835296,
"ssrc": "0x343da99b"
}
],
"total_dialogs": 1,
"truncated": false,
"uuid": "0158a120-0392-8940-8117-7af10a0d166e",
"verdict": "analyzed"
}A container with no readable SIP is an answer with verdict: unreadable, not a
tool error. The call fails with invalid_params (-32602) only when
container is missing (reason: missing_param) or is not a JSON object
(reason: invalid_value), with param: container in both
error data objects.
Reads one container from the operator's vCon store by uuid and returns it in
the answer. It writes no file. The request, the credential, the CA, the size
limit, the decryption and the signer checks are the same as for
sipnab --vcon-fetch (Fetch a stored vCon).
GET /v1/vcon/fetch/{uuid} gives the same
answer.
It connects to the store, so it is off by default. It needs
--mcp-allow-vcon-fetch and a store in the [vcon_fetch] section of the
config file (url, kind, auth_file, and decrypt_key for encrypted
containers). The store, its kind, the credential and the key come from
operator configuration only: the tool takes a uuid and nothing that picks a
destination, and the answer never holds a key or the credential. The tool is
readOnlyHint: true and openWorldHint: true.
auth_file holds one Header-Name: value line, such as
Authorization: Bearer <token>. A file without a colon stops the run at
startup with exit status 2.
| Name | Type | Legal values | If omitted |
|---|---|---|---|
uuid |
string | The vCon's uuid: 1 to 64 characters, each a letter, a digit or -. Anything else fails with invalid_params (-32602), the uuid is not one (1 to 64 characters, each a letter, a digit or '-'), before sipnab sends anything. |
Required. |
Returns:
| Field | Type | Description |
|---|---|---|
uuid |
string | The uuid asked for |
stored_form |
string | The form the store held the container in: unsigned, signed or encrypted
|
decrypted |
boolean |
true when sipnab decrypted an encrypted container with [vcon_fetch] decrypt_key
|
signature_verified |
boolean |
true when the container's signature verified, and passed [vcon_fetch] signer_ca and signer_sha256 when the operator sets them: the signed form inside a decrypted container, or a signed container that is not encrypted, which needs no key. sipnab refuses a signature that does not verify, as the readers do. false for an unsigned container and for one left encrypted |
signer_sha256 |
string or null | The SHA-256 fingerprint of the certificate the signature verified against, as openssl x509 -noout -fingerprint -sha256 prints it |
schema_findings |
array | Where the container fails the draft-ietf-vcon-vcon-core-04 schema, one line each. Empty for a valid container and for one left encrypted |
container |
object | The vCon: the unsigned container when sipnab decrypted it, else as the store holds it, a signed container with its signature, without the store kind's envelope |
A second content block is the untrusted-content note: the container holds whatever its producer stored.
Refusals, each invalid_params (-32602) unless it says otherwise:
| Cause | Message |
|---|---|
No --mcp-allow-vcon-fetch
|
fetching a vCon from the store is off on this server: start sipnab with --mcp-allow-vcon-fetch, and configure the store in [vcon_fetch] |
The flag, and no [vcon_fetch] url
|
--mcp-allow-vcon-fetch is set, but no vCon store is configured: set [vcon_fetch] url and [vcon_fetch] auth_file in the config file |
The store answered 404
|
the store holds no such vCon (404), with data.reason: "not_found"
|
Anything else the fetcher refuses: another status, a timeout, an answer over [vcon_fetch] max_size, another uuid inside, a key that does not decrypt, a signer that fails a check |
internal_error (-32603), the fetcher's reason fenced as untrusted text, with the credential removed |
A [vcon_fetch] store whose credential or key file sipnab cannot read stops
the run at startup, naming the flag.
The example reads from a generic store on the local host, with this
[vcon_fetch] section in the file given to -f:
[vcon_fetch]
kind = "generic"
url = "http://127.0.0.1:8080/v/{uuid}"
auth_file = "/etc/sipnab/vcon-store.key"sipnab -N --mcp -I tests/pcap-samples/sip-rtp-g711.pcap -f sipnab.toml --mcp-allow-vcon-fetch// fetch_vcon { "uuid": "0158a120-0392-8940-bcd9-b35008fdf5f9" }
{
"schema_version": 1,
"uuid": "0158a120-0392-8940-bcd9-b35008fdf5f9",
"stored_form": "unsigned",
"decrypted": false,
"signature_verified": false,
"signer_sha256": null,
"schema_findings": [],
"container": { "vcon": "0.4.0", "uuid": "0158a120-0392-8940-bcd9-b35008fdf5f9", … }
}Reports every stored vCon container in one directory of --mcp-file-root:
which ones sipnab read, what each one held, which ones it skipped and why, and
the totals. It is the report sipnab -N -I <dir> --report prints after the
dialog table: see
Read many containers at once.
sipnab reads each container with the reader -I uses, and rebuilds the calls
into stores of their own. The tool leaves the loaded capture untouched,
changes no file, and connects nowhere. It opens a signed, encrypted or gzip
format container with the server's --vcon-key, --vcon-signer-ca and
--vcon-signer-sha256.
- It reads the files directly in the directory. It does not descend into subdirectories.
- It does not follow a symbolic link.
- A file is a container when its content says so, as for
-I: the first character other than whitespace is{, or the file is gzip that inflates to one. A capture or a README in the directory is not a row.
| Name | Type | Legal values | If omitted |
|---|---|---|---|
directory |
string | A directory directly inside --mcp-file-root, by bare name, or . for the root itself. |
Required. A path, or a name that leads outside the root, fails with invalid_params. |
limit? |
integer | The most rows to return. Ceiling --mcp-max-rows. |
50. |
cursor? |
string | The next_cursor of the previous page: the file name it ended at. |
The first page. |
max_response_bytes? |
integer | The byte budget for this page. Ceiling --mcp-max-response-bytes. |
The ceiling. The budget cuts rows whole, and truncated_by says bytes. |
Rows, then totals. One row per container file, in file-name order.
| Field | What it holds |
|---|---|
status |
read or skipped
|
file |
The file name, relative to directory
|
uuid |
The container's uuid, or null when it did not open far enough to show one |
form |
The outermost form the file arrived in: unsigned, signed, encrypted, gzip, or unknown for bytes that are not a JSON object |
validation_findings |
Places the container disagrees with the vendored draft-ietf-vcon-vcon-core-04 schema. Null when the schema check did not run: the file did not open, or is not JSON |
calls_rebuilt, call_ids
|
The calls (dialogs, by Call-ID) rebuilt from it |
dispositions |
Those calls counted by state, the word the --report dialog table's State column prints |
duration_secs |
Those calls' durations added up, each as the dialog table's Duration column computes it |
skip_reason |
On a skipped row only: no_key, no_sip_signaling, not_json, not_a_vcon, not_opened, unsupported_critical or unreadable
|
skip_detail |
On a skipped row only: the same reason in full |
totals adds up every row of the directory, whichever page this is:
containers, read, skipped, skipped_by_reason, failed_validation
(containers with at least one schema finding), calls, dispositions and
duration_secs. A call two containers both hold counts once in the totals,
and once in each of their rows.
no_key is an encrypted container on a server without --vcon-key. The row
names the flag. sipnab never reports an encrypted container as one with no SIP.
The example reads a directory stored holding six files: two unsigned
containers (--export-vcon of the two calls in
tests/pcap-samples/sip-rtp-g711.pcap), the gzip format of a third (the busy
call in tests/pcap-samples/sip-problem-call.pcap), the encrypted container
tests/fixtures/vcon-jose/ab_call_ext_rec_encrypted.vcon, a container from
the tests/fixtures/vcon-datasets/vcon-supreme-court-arguments dataset (which
stores no SIP), and the first 30 bytes of the first container. This is the
second page of three rows:
// vcon_directory_report { "directory": "stored", "limit": 3, "cursor": "c-busy.vcon.json.gz" }
{
"containers": [
{
"call_ids": [],
"calls_rebuilt": 0,
"dispositions": {},
"duration_secs": 0,
"file": "d-sealed.vcon.json",
"form": "encrypted",
"skip_detail": "cannot open the vCon container: the container is encrypted (draft-ietf-vcon-vcon-core-04 section 5.3), and no decryption key was given: give --vcon-key, or [vcon_input] key in the config file",
"skip_reason": "no_key",
"status": "skipped",
"uuid": null,
"validation_findings": null
},
{
"call_ids": [],
"calls_rebuilt": 0,
"dispositions": {},
"duration_secs": 0,
"file": "e-dataset.vcon.json",
"form": "unsigned",
"skip_detail": "a vCon container with no SIP signaling sipnab can read: it has no sip-message-trace or message/sip attachment",
"skip_reason": "no_sip_signaling",
"status": "skipped",
"uuid": "01a0be36-e172-8a14-9dd8-dd37220d739c",
"validation_findings": 4
},
{
"call_ids": [],
"calls_rebuilt": 0,
"dispositions": {},
"duration_secs": 0,
"file": "f-malformed.vcon.json",
"form": "unknown",
"skip_detail": "not a vCon container: not JSON (EOF while parsing a value at line 3 column 9)",
"skip_reason": "not_json",
"status": "skipped",
"uuid": null,
"validation_findings": null
}
],
"directory": "stored",
"next_cursor": null,
"returned": 3,
"schema_version": 1,
"totals": {
"calls": 3,
"containers": 6,
"dispositions": {
"Completed": 1,
"Failed": 1,
"InCall": 1
},
"duration_secs": 9,
"failed_validation": 1,
"read": 3,
"skipped": 3,
"skipped_by_reason": {
"no_key": 1,
"no_sip_signaling": 1,
"not_json": 1
}
},
"truncated": false
}A build without the vcon Cargo feature does not register this tool.
Counts the vCon export spool: the directory --export-vcon-dir names, which
the capture's --export-vcon-when writes containers into and which
sipnab --vcon-forward delivers to a
store. For each container the store refused, it returns the record the
forwarder wrote beside it, and it counts files by the forwarder's rules.
The same answer comes from
GET /v1/vcon/spool and from
sipnab --vcon-spool-status <SPOOL_DIR>
(CLI reference). This tool also fences each
store answer as untrusted text and cuts it to --mcp-max-body-bytes.
The answer is a snapshot of files on disk, taken when as_of says. The
forwarder is a separate process and may move a file a moment later. This tool
reads files and does nothing else: it sends nothing, fetches nothing, and
moves, deletes or creates no file, and makes no outbound vCon connection. It
is readOnlyHint: true.
It needs a run started with --export-vcon-when and --export-vcon-dir.
Without them it fails with invalid_params (-32602) naming the flags. It
reads the forwarder's DEFAULT directories beside the spool, delivered/ and
failed/. A forwarder started with --vcon-forward-done or
--vcon-forward-failed moves files to directories this tool does not read,
and does not count.
| Name | Type | Legal values | If omitted |
|---|---|---|---|
limit? |
integer | 1 to --mcp-max-rows (1000 by default). Bounds the list of failed records; the three counts are never bounded. |
50. |
cursor? |
string | The previous response's next_cursor, passed back verbatim. |
The first page. |
rule? |
string | The export rule whose spool to read: a [[vcon.rule]] name, or --export-vcon-when for the flags' rule. Several spools and no rule, or a rule the run does not have, fail with invalid_params (-32602) listing the run's rule names. |
The run's only spool. |
Returns:
| Field | Type | Description |
|---|---|---|
as_of |
string | When this tool read the directories, RFC 3339 UTC |
note |
string | One sentence saying the answer is a snapshot and that this tool moves nothing |
spool, delivered_dir, failed_dir
|
object | Each directory's path and whether it exists. A directory that does not exist counts as empty |
pending |
integer | Containers in the spool, by the forwarder's rule: regular files ending in .json whose names do not start with a dot |
delivered |
integer | Containers in delivered/
|
failed |
integer | Containers in failed/, the .error.json records not counted |
failures |
array | One page of failed containers by file name: file, and record, the <file>.error.json record (at, status, method, url, body, body_truncated, reason) |
returned |
integer | Rows in failures
|
truncated |
boolean | Whether failed containers remain after this page |
next_cursor |
string or null | Pass back as cursor for the next page; null on the last page |
rule |
string | The export rule whose spool this is |
record.body is the start of the store's answer as the forwarder kept it,
fenced as untrusted text and cut to --mcp-max-body-bytes (4096 by default).
record.status, method, url and body are null when the forwarder refused
the container before sending it, and record.reason then says why. A failed
container whose record is missing, unreadable or not in the forwarder's shape
has a null record and a record_problem saying which.
The example runs in a directory whose vcons/ spool holds one pending
container, delivered/sent.vcon.json and failed/refused.vcon.json with its
refused.vcon.json.error.json record. The run itself writes the four failed
calls of tests/pcap-samples/sip-problem-call.pcap into the spool, so five
are pending:
sipnab -N --mcp -I tests/pcap-samples/sip-problem-call.pcap --export-vcon-when 'state == Failed' --export-vcon-dir vcons// vcon_spool_status {}
{
"schema_version": 1,
"as_of": "2026-10-16T12:04:41.217Z",
"note": "A snapshot of files on disk at as_of. The vCon forwarder (sipnab --vcon-forward) is a separate process that moves files out of the spool; whatever produced this answer sends, fetches, moves and deletes nothing. The delivered and failed directories are the forwarder's defaults beside the spool; a forwarder started with --vcon-forward-done or --vcon-forward-failed moves files elsewhere, and those are not counted.",
"rule": "--export-vcon-when",
"spool": { "path": "vcons", "exists": true },
"delivered_dir": { "path": "vcons/delivered", "exists": true },
"failed_dir": { "path": "vcons/failed", "exists": true },
"pending": 5,
"delivered": 1,
"failed": 1,
"failures": [
{
"file": "refused.vcon.json",
"record": {
"at": "2026-10-11T11:58:02Z",
"body": "⟦untrusted-capture-data⟧{\"error\":\"vCon already exists\"}⟦/untrusted-capture-data⟧",
"body_truncated": false,
"file": "refused.vcon.json",
"method": "POST",
"reason": null,
"status": 409,
"url": "https://vcon.example.com/v1/vcons"
}
}
],
"returned": 1,
"truncated": false,
"next_cursor": null
}A build without the vcon Cargo feature does not register this tool.
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