Token导航 LogoToken导航TokenDH.com
pjsua MCP logo
开发工具未说明官方级别未说明来源级核验

pjsua MCP

MCP Server

PJSUA MCP Server是一个基于PJSUA2的多SIP用户代理管理服务器,通过MCP协议为AI助手提供对多个SIP电话的控制能力,适用于自动化测试和通信场景。

工具数

34

提示词数

0

GitHub Stars

1

资源数

0
DockerClaude开发工具Claude

安装说明

本站只整理中文说明和来源信息,不托管安装包,也不代用户安装。

作者 / 组织

Yevhenii-Yatchenko

提供方

Yevhenii-Yatchenko

最后核验

2026/5/17 20:22

快速接入

先看主来源和安装命令,再打开仓库或文档;下面只保留这个条目的关键接入事实。

详细介绍

PJSUA MCP服务器

一个MCP服务器进程并行管理N部手机。每部手机都有自己的 pj.Account 并且它自己的UDP传输在一个单一的 pj.Endpoint。添加手机时,服务器会为每个手机注册22个操作工具( _make_call, _hangup,…)通过 mcp.add_tool() 和火灾 notifications/tools/list_changed当你放下手机时,那些工具又消失了。

在这些原子工具之上,服务器还附带了 事件驱动场景引擎 (src/scenario_engine/)LLM代理将一个多步骤SIP流描述为一个YAML场景,引擎在自己的异步循环中执行整个过程,而没有每一步的LLM转换延迟。场景组成原子 模式 (14开箱即用)或定义内联 hooks: 对于一次性流程,并返回每个事件+行动的完整时间表以供事后检查。看 场景引擎 下面的部分。

建筑

┌──────────────────────────────────────────────────────────────┐
│  AI Assistant (Claude, etc.)                                 │
│                                                              │
│  "Load the test profile, then call from a to 002"            │
└──────────────┬───────────────────────────────────────────────┘
               │ MCP (JSON-RPC over stdio)
               ▼
┌──────────────────────────────────────────────────────────────┐
│  PJSUA MCP Server (one Docker container)                     │
│                                                              │
│  ┌───────────────────── scenario engine ─────────────────┐   │
│  │ EventBus ◄── emit reg.* / call.state.* / dtmf.* / im  │   │
│  │    ▲           from pjsua callbacks                   │   │
│  │    │                                                  │   │
│  │ HookRuntime                    ActionExecutor         │   │
│  │    │                            (maps 19 actions       │   │
│  │    │                             to CallManager etc.)  │   │
│  │    │                                                   │   │
│  │ Orchestrator ──► run_scenario / validate_scenario      │   │
│  │                                                          │   │
│  └──────────────────────────────────────────────────────┘   │
│                       ▼                                      │
│  ┌────────────┐  ┌───────────────┐  ┌─────────────────────┐  │
│  │ SipEngine  │  │ PhoneRegistry │  │     CallManager     │  │
│  │ (Endpoint, │  │  dict[pid]    │  │  dict[call_id],     │  │
│  │  codecs,   │  │  → SipAccount │  │  per-phone queues,  │  │
│  │  per-phone │  │   + Config    │  │  incoming routing,  │  │
│  │  transports)│  │               │  │  always-on recording│  │
│  └──────┬─────┘  └──────┬────────┘  └──────┬──────────────┘  │
│         │               │                   │                │
│         │          ┌────┴─── phone_tool_factory ─────┐       │
│         │          │  register_phone_tools(mcp, pid) │       │
│         │          │    → 22 closures per phone      │       │
│         │          │    → mcp.add_tool / remove_tool │       │
│         │          └─────────────────────────────────┘       │
│         │                                                    │
│  ┌──────┴──────────────────────────────────────────────────┐ │
│  │              PJSUA2 / pjproject 2.14.1                  │ │
│  └──────────────────────┬──────────────────────────────────┘ │
│                         │ SIP/UDP (1 socket per phone)       │
│  ┌──────────────┐       │                                    │
│  │ SipLogWriter  │ ◄────┘  captures every SIP message        │
│  └──────────────┘                                            │
│  ┌──────────────┐                                            │
│  │ PcapManager   │  tcpdump — host-wide or BPF per phone     │
│  └──────────────┘                                            │
└──────────────────────────────────────────────────────────────┘
               │ SIP/UDP
               ▼
        ┌─────────────┐
        │  SIP PBX /   │
        │  Registrar   │
        └─────────────┘

MCP工具

静态(11-始终存在)

电话CRUD

工具说明
list_phones所有已注册的电话,包括注册状态、传输端口、活动呼叫计数、每个电话工具名称
add_phone创建一个交通+SipAccount,发送REGISTER,注册22个手机操作工具
drop_phone挂断电话、注销、关闭传输、卸载手机工具
get_phone一部手机的完整信息——凭证(无密码)、注册状态、活动通话、, recording_enabled
update_phone更改运行时设置-- auto_answer / codecs (每部手机SDP过滤器,即时)/ recording_enabled / capture_enabled (即时)或凭据(强制重新注册)
load_phones批量添加YAML配置文件中列出的每个手机。默认情况下原子替换(merge=True 用于追加销售)

全球诊断

工具说明
get_sip_log检索pjsip日志条目(原始文本)。 phone_id 所有权过滤器(呼叫ID/传输端口/RISTER); call_id/method/direction/status_code/cseq 进一步缩小; filter_text 子字符串转义符
get_call_messages结构化SIP消息——解析的报头+解析的SDP。与相同的过滤器集 get_sip_logfilter_text.专为程序化计划检查而设计
list_recordings步行 /recordings/ (以及传统平面文件)用于每个WAV;筛选依据 phone_id / call_id
analyze_capture解析 `/captures/
/call__*.pcap 转换为结构化RTP/RTCP流计数。表面 phone_rtp_codecs_seen + non_phone_codecs_on_phone_port (与手机相比 codecs` config),因此调用者可以验证每个电话的SDP过滤器的一致性,而无需在bash中进行ad-hoc pcap解析

每部手机的数据包捕获仍在继续 update_phone(phone_id=..., capture_enabled=true/false) --自动在第一次音频活动呼叫时启动tcpdump,并在最后一次断开连接时停止。pcap路径在WAV中着陆 .meta.json 侧车(旁边 local_rtp_port / remote_rtp_port,在媒体处于活动状态时进行快照),因此可以进行录制、捕获和 analyze_capture 所有这些都在磁盘上配对。

场景引擎

工具说明
validate_scenario静态模拟运行——捕获未知动作、未知事件类型、格式错误的钩子——无需触摸pjsua
run_scenario执行场景字典,首先自动验证,返回完整的时间线+状态+错误

每部手机动态(每部手机22部)

注册时间 add_phone (或 load_phones)将手机上网;未注册 drop_phone。以下示例使用电话 a:

工具说明
a_make_call带可选自定义SIP标头的出站INVITE
a_answer_call接听电话a上的来电(如果 call_id 省略)
a_reject_call使用SIP状态码(486/603/480)拒绝
a_hangupBYE是一个正在进行的通话
a_get_call_info状态、编解码器、持续时间、RTP统计数据、远程/本地联系人、录制路径
a_get_call_history已完成的电话a通话
a_list_callsa跟踪呼叫的简明状态摘要
a_get_active_calls完整信息+RTP的主动通话
a_send_dtmf在通话中发送DTMF数字
a_hold / a_unhold重新邀请仅发送/恢复
a_blind_transfer请参考以重定向a的呼叫
a_attended_transfer参考+替换。双腿必须属于电话a——跨电话桥接被拒绝
a_conference将多个自有电话连接到会议中
a_play_audio / a_stop_audio将WAV播放到通话/恢复MOH中
a_get_recording电话呼叫的WAV+sidecar元的路径/大小
a_send_message / a_get_messagesSIP消息发件箱/收件箱
a_register / a_unregister新鲜寄存器循环/去寄存器(对称对)
a_get_registration_status手机a的快速注册状态

N个电话的总表面:10+22·N。

快速开始

1.构建Docker镜像

docker compose build

2.连接到AI助手

添加到MCP客户端配置中(例如。 .mcp.json):

{
  "mcpServers": {
    "pjsua": {
      "command": "docker",
      "args": ["compose", "-f", "/absolute/path/to/pjsua_mcp/docker-compose.yml",
               "run", "--rm", "-i", "pjsua-mcp"]
    }
  }
}

3.描述你的手机(YAML配置文件)

服务器附带没有SIP凭据——您可以在主机上的YAML配置文件中描述您的手机。

cp config/phones.example.yaml config/phones.yaml
$EDITOR config/phones.yaml

config/phones.yaml 被忽视;仅 phones.example.yaml 被跟踪。docker组合绑定挂载 ./config/config (只读)。

最小配置文件:

defaults:                 # optional — merged into every phone, phone-level keys win
  domain: sip.example.com
  password: change_me
  codecs: [PCMA, telephone-event]   # default for phones that don't override
  auto_answer: false

phones:
  - phone_id: a
    username: "1001"
  - phone_id: b
    username: "1002"
    codecs: [PCMU, telephone-event]   # phone-level override wins over defaults
    auto_answer: true

每部手机 codecs: SDP重写过滤器——每个传出的报价或 接听此电话只会列出这些编解码器。RTP发送/接收 自然会这样,因为pjsua的媒体激活从中选择编解码器 {SDP广告}∩{端点已启用}(端点固定 启动时的超集)。DTMF(telephone-event)由自动保存 即使没有明确列出重写器。没有手机 codecs 列表会回退到端点超集提供的任何内容。

4.加载配置文件并运行场景

mcp__pjsua__load_phones()                   # reads /config/phones.yaml
# → every phone registers; a_make_call, b_hangup, … appear via tools/list_changed.

mcp__pjsua__a_make_call(dest_uri="sip:002@sip.example.com")
mcp__pjsua__a_get_call_info(call_id=0)
mcp__pjsua__a_hangup(call_id=0)

load_phones原子替代 默认情况下:在加载之前,每个现有手机的活动通话都会挂断,手机也会掉线。通过 merge=True 保留新配置文件中未列出的手机。

对于不涉及配置文件的临时添加:

mcp__pjsua__add_phone(phone_id="alice",
                      domain="sip.example.com",
                      username="1099", password="x",
                      codecs=["PCMA", "telephone-event"])
# → phone alice's INVITEs list only PCMA + telephone-event in SDP,
#   and RTP for this phone uses PCMA.
mcp__pjsua__drop_phone(phone_id="alice")

通话场景——两种风格

服务器支持两种驾驶通话模式:

  • 原子工具模式 --直接拨打电话工具(a_make_call,

b_answer_call,…)并在步骤之间轮询 time.sleep很好 交互式调试和一次性实验。

  • 场景引擎模式 --将整个流程描述为YAML场景

交给 run_scenario发动机臂钩在活动总线上 并在一个紧密的异步循环中驱动流,因此时序是 确定性,没有每一步LLM转弯延迟。有益于 可复制的测试用例和票复制器。

以下示例显示了发动机形式(首选可重复工作); 原子工具等效工具始终可用作后备。

所有示例都假设配置文件已加载(a, b, c 在线的 用注册商的URI替换URI。

基本呼叫:A→ B带DTMF

# scenario
name: a-to-b-dtmf
phones: [a, b]
patterns:
  - {use: auto-answer, phone_id: b, delay_ms: 500}
  - {use: send-dtmf-on-confirmed, phone_id: a, digits: "1234"}
  - {use: hangup-after-duration, phone_id: a, duration_ms: 5000}
  - {use: make-call-and-wait-confirmed, phone_id: a,
     dest_uri: "sip:002@sip.example.com"}
stop_on: [{phone_id: a, event: call.state.disconnected}]
timeout_ms: 15000
run_scenario(scenario=)
# Returns: {status: "ok", timeline: [...events + actions with ms offsets...]}

原子工具等效物:

a_make_call(dest_uri="sip:002@sip.example.com")
# b auto-answers (auto_answer: true in profile)
a_send_dtmf(call_id=0, digits="1234")
a_hangup(call_id=0)

自动应答(IVR/bot模式)

auto_answer: true 对于YAML中的手机或运行时的切换:

update_phone(phone_id="b", auto_answer=True)

…或将其构建到场景中 auto-answer 图案。

盲转:B转A→ C

name: blind-transfer
phones: [a, b, c]
patterns:
  - {use: auto-answer, phone_id: b, delay_ms: 200}
  - {use: auto-answer, phone_id: c, delay_ms: 200}
  - {use: blind-transfer, phone_id: b,
     transfer_to: "sip:003@sip.example.com", after_ms: 2000}
  - {use: hangup-after-duration, phone_id: a, duration_ms: 5000}
  - {use: make-call-and-wait-confirmed, phone_id: a,
     dest_uri: "sip:002@sip.example.com"}
stop_on: [{phone_id: c, event: call.state.disconnected}]
timeout_ms: 12000

原子工具等效物:请参见 b_blind_transfer(dest_uri=...).

参加转移:B持有A,咨询C,桥接A↔ C

流程有足够的步骤,因此更清晰 内联场景挂钩 而不是作为复合图案:

name: attended-transfer
phones: [a, b, c]
patterns:
  - {use: auto-answer, phone_id: b, delay_ms: 200}
  - {use: auto-answer, phone_id: c, delay_ms: 200}
  - {use: make-call-and-wait-confirmed, phone_id: a,
     dest_uri: "sip:002@sip.example.com"}
hooks:
  - when: call.state.confirmed
    on_phone: a
    once: true
    then:
      - wait: 1000ms
      - hold
      - make_call: {phone_id: a, to: "sip:003@sip.example.com"}
      - wait: 2500ms
      - attended_transfer
stop_on: [{phone_id: c, event: call.state.disconnected}]
timeout_ms: 15000

双腿必须属于同一部手机——跨手机参与转接 返回一个错误,并显示一条明确的消息。

三方会议

name: conference
phones: [a, b, c]
patterns:
  - {use: auto-answer, phone_id: b, delay_ms: 100}
  - {use: auto-answer, phone_id: c, delay_ms: 100}
initial_actions:
  - {action: make_call, phone_id: a, to: "sip:002@sip.example.com"}
  - {action: make_call, phone_id: a, to: "sip:003@sip.example.com"}
hooks:
  - when: call.state.confirmed
    on_phone: a
    once: true
    then:
      - wait: 2500ms
      - action: conference
        phone_id: a
        call_ids: auto        # engine resolves to all active calls on a
      - wait: 5000ms
      - hangup_all: {phone_id: a}
stop_on: [{phone_id: a, event: call.state.disconnected}]
timeout_ms: 20000

编解码器选择和通话中期更改

每个手机的编解码器列表都经过SDP重写器;通话中重新邀请 通过 reinvite-codec-change 模式仍然使用全局优先级:

patterns:
  - {use: reinvite-codec-change, phone_id: a, new_codec: G722,
     trigger_at_ms: 3000}
  - {use: hangup-after-duration, phone_id: a, duration_ms: 5000}
  - {use: make-call-and-wait-confirmed, phone_id: a,
     dest_uri: "sip:002@sip.example.com"}

SIP消息传递

initial_actions:
  - {action: send_message, phone_id: a,
     to: "sip:002@sip.example.com", body: "Hello!"}
stop_on: [{event: im.received, phone_id: b}]
timeout_ms: 2000

监控(始终是原子性的--只读自检)

list_phones()                # reg state + active-call counts
a_get_active_calls()         # a's active calls with RTP
a_list_calls()               # compact summary incl. DISCONNECTED
get_sip_log(phone_id="a", last_n=30)

场景引擎

目标。 让LLM编写一次多步骤SIP流并执行它 确定性。发动机更换“呼叫工具,等待2秒,呼叫下一个” “工具”循环(在LLM转弯延迟上燃烧挂钟并与之竞争 真正的SIP定时器)与在一个异步循环中运行的YAML流。

典型工作流程中的两个工具

validate_scenario(scenario=)     # static dry-run (no pjsua touched)
run_scenario(scenario=)          # execute and return the timeline

这两个工具都只接受一个场景作为Python字典——文件路径不是 支持(代理在容器外运行,因此路径不会转换)。

场景的编写方式为 内联 hooks:when: + then: []. 规范参考 用于解剖学、习语、全动作表面,以及 工作实例(盲转、有人值守转)生活在 pjsua-scenarios 此MCP附带的技能。

事件分类

胡克在听:

  • 呼叫状态: call.state.{calling,incoming,early,connecting,confirmed,disconnected}
  • 双音多频: dtmf.in, dtmf.out
  • 注册: reg.{started,success,failed,unregistered}
  • 消息传递: im.received
  • 场景生命周期: scenario.{started,stopped}
  • 用户发出: user. (从 emit 行动)

动作词汇(19个动作)

  • 呼叫控制: answer, hangup, hangup_all, reject, hold,

unhold, send_dtmf, blind_transfer, attended_transfer, conference, make_call

  • 媒体: play_audio, stop_audio, send_message, set_codecs
  • 流量控制: wait, wait_until, emit, checkpoint, log

分派时继承的默认值: phone_id 从钩子 on_phone 或 触发事件, call_id 从触发事件开始。

stop_on 过滤器

stop_on:
  - phone_id: a
    event: call.state.disconnected
    call_id: 2                       # specific call-id
  - event: call.state.disconnected
    match: {last_status: "4xx"}      # predicate — supports exact, list,
                                     # "4xx"/"5xx", "~regex"

飞行前验证

run_scenario 自动运行 validate_scenario 第一。拼写错误(错误动作, 错误的事件前缀、格式错误的钩子)返回 status="error" 在\", "local_contact": "", "codec": "PCMA", "duration": 45, "recording_file": "/recordings/a/call_0_20260101_141603_528491.wav", "playing_file": "/app/audio/moh.wav", "rtp": { "tx_packets": 2250, "tx_bytes": 360000, "rx_packets": 2248, "rx_bytes": 359680, "rx_loss": 0, "rx_dup": 0, "rx_reorder": 0, "rx_discard": 0, "rx_jitter_usec": 875, "rtt_usec": 6362 } }


`
_get_active_calls` 一次为手机上的每个活动呼叫返回此值——无需迭代 `call_id`s

## 通话录音(每部手机切换,配对pcap)

录音是 **默认情况下关闭** --通过手机选择加入
`recording_enabled: true` 在YAML或 `recording_enabled=True` 在
`add_phone`。启用后,手机上的每个通话都会写入
集装箱路径 `/recordings/
/` 作为两个成对的文件:

/recordings/ ├── a/ │ ├── call_0_20260422_145828_123456.wav # local + remote audio mixed │ └── call_0_20260422_145828_123456.meta.json # context sidecar ├── b/ │ └── ...


文件名带有微秒后缀,因此单个调用可以产生
如果在通话过程中切换录音,则需要几个WAV(见下文)。侧三轮
承载着WAV本身所缺乏的背景:

{ "phone_id": "a", "call_id": 0, "direction": "outbound", "started_at": "2026-04-22T14:58:28+00:00", "ended_at": "2026-04-22T14:58:54+00:00", "duration": 26, "codec": "PCMA", "remote_uri": "sip:123002@...", "last_status": 200, "last_status_text": "OK", "recording": "/recordings/a/call_0_20260422_145828_123456.wav", "pcap": "/captures/a/call_0_20260422_145828.pcap" }


`pcap` 每当每个手机自动捕获时都会填充
(`capture_enabled=true`)在通话过程中跑步。pcap活着
在...之下 `/captures/
/` 与录音同名,
因此,音频和信令配对时没有任何时间戳匹配。

### 每部手机切换: `recording_enabled`

每部手机都有一个 `recording_enabled` 标志(默认 `false`).设置它
在YAML中或运行时,切换立即生效
该电话的当前通话:

config/phones.yaml

defaults: domain: sip.example.com password: xxx # recording_enabled: false # default — nobody records phones: - phone_id: a username: "1001" recording_enabled: true # per-phone opt-in - phone_id: b username: "1002" # stays off

add_phone(phone_id="c", domain="...", username="1003", password="x", recording_enabled=True) # opt-in at add time

update_phone(phone_id="a", recording_enabled=True) # flip on mid-call update_phone(phone_id="a", recording_enabled=False) # flip back off


每 `off → on` 使用新的微秒唯一文件名打开一个新的WAV
每一个 `on → off` 关闭当前WAV并写入 `.meta.json`
侧三轮。所以 `on → off → on → off → on → hangup` 生产 **三**
WAV+侧三轮配对 `/recordings/
/`,一个也没有。使用
`list_recordings(phone_id=..., call_id=...)` 查看每个片段
给定的呼叫; `
_get_recording(call_id=...)` 仅返回
目前开放的部分。

**对主机完全隐藏录制文件**,放下
`./recordings` 从您的 `docker-compose.yml` — `/recordings`
将存在于临时容器FS中,并随 `--rm`.

**音乐暂停** 当呼叫连接时自动播放——西班牙组曲Op.47——Leyenda(Albeniz),来自FreeSWITCH/MUSOPEN的CC0公共域,8kHz WAV。使用 `
_play_audio` 为了覆盖, `
_stop_audio` 恢复卫生部。

## SIP日志检查

PJSUA2堆栈处理的每个SIP消息都由自定义 `LogWriter` 转换为有界的内存双端队列(5000个条目):

get_sip_log() # everything (all phones) get_sip_log(last_n=20) get_sip_log(filter_text="401") # raw substring escape hatch get_sip_log(phone_id="a") # ownership-resolved (Call-ID + transport port + REGISTER username) get_sip_log(phone_id="a", call_id=0) # narrow to one SIP dialog get_sip_log(phone_id="a", method="INVITE") # structured method filter get_sip_log(phone_id="a", direction="TX") # outgoing only get_sip_log(phone_id="a", status_code=200) # 200 OK responses only get_sip_log(phone_id="a", method="INVITE", direction="TX") # composable


电话过滤使用结构所有权而不是子字符串匹配,
因此,另一部手机腿上的消息(例如bob的RX INVITE
`From: `,或者鲍勃的 `[DISCONNECTED]` 显示alice URI的转储
在 `To:`)做 **不** 泄露到爱丽丝的过滤日志中。所有者的条目
无法从结构上解析,退回到子字符串匹配
响应面a `warning` 带计数的字段。

每个条目包含:

- `level` --pjsip日志级别(1=错误…5=跟踪)
- `msg` --完整日志行,包括SIP消息转储
- `thread` --发起pjlib线程名称

### 结构化消息(`get_call_messages`)

当您需要解析SDP/报头(编解码器列表、媒体端口、RTCP端口、,
方向)而不是原始文本,使用 `get_call_messages`.相同的过滤器组
作为 `get_sip_log` 减 `filter_text`:

get_call_messages(phone_id="a", call_id=0, method="INVITE", direction="TX")


每条SIP消息返回一个条目:

{ "phone_id": "a", "messages": [ { "ts": "16:05:25.556", "direction": "TX", "method": "INVITE", "cseq": 8398, "call_id": "5d0cbc47-...", "from": "sip:6001@asterisk", "to": "sip:6002@asterisk", "headers": {"Call-ID": "...", "CSeq": "8398 INVITE", "Content-Type": "application/sdp"}, "sdp": { "version": 0, "origin": {"username": "-", "ip": "192.168.1.40"}, "media": [{ "type": "audio", "port": 4000, "protocol": "RTP/AVP", "payload_types": [0, 120], "codecs": [ {"pt": 0, "name": "PCMU", "clock_rate": 8000}, {"pt": 120, "name": "telephone-event", "clock_rate": 8000, "fmtp": "0-16"} ], "direction": "sendrecv", "rtcp_port": 4001 }] } } ], "total_count": 1 }


回复还包括 `status_code`.SIP消息没有
`application/sdp` 身体有 `sdp: null`.pjlib库日志行和
pjsua通话转储摘要(`[DISCONNECTED]`)默默地落下--
它们没有需要结构化的SIP信封。

## 数据包捕获

两种独立模式并存: **手册** (一枪tcpdump你开枪
来自工具调用)以及 **自动捕获** (每部手机 `capture_enabled`
flag--tcpdump在第一次音频活动呼叫时打开,在
最后断开连接)。两个土地下 `/captures/
/` 与相同
basename作为录音,因此pcap和WAV在磁盘上配对。

### 每部手机自动捕获(`capture_enabled`)

默认值为 `false` --除非您选择加入,否则tcpdump不会运行。请将其打开
YAML或运行时;每次通话都会检查状态,因此您可以翻转它
中期会议:

config/phones.yaml

phones: - phone_id: a username: "1001" capture_enabled: true # every call on 'a' → pcap - phone_id: b username: "1002" # inherits default → no pcap

add_phone(phone_id="c", domain="...", username="1003", password="x", capture_enabled=True) # opt-in at add time update_phone(phone_id="a", capture_enabled=False) # flip off mid-call update_phone(phone_id="a", capture_enabled=True) # flip back on


On→在实时通话中关闭会刷新并关闭当前的pcap; off→on
使用新的微秒唯一文件名打开一个新的pcap。 Off→on **做
不** 追溯捕获呼叫早期的数据包。

每次自动捕获都使用宽BPF滤波器 `udp`,所以重新邀请
更改RTP端口(保持/取消保持、编解码器交换)不会丢弃任何数据包
中途通话。代价是磁盘:在嘈杂的网络上,pcap增长更快
如果我们只锁定一个端口。如果你需要修剪,可以拆分pcap
post-hoc——见下文。

在会议中(一部电话上有两个活动电话)a **单个** pcap是
留着打电话,不是每条腿一个。第一个呼叫启动它;最后的
断开连接会关闭它。

### 事后拆分SIP和RTP

因为BPF滤波器很宽(`udp`),pcap包含SIP
信令和RTP媒体交织。与分开 `tshark` 事后:

tshark -Y 'sip' -r captures/a/call_0_*.pcap -w sip_only.pcap tshark -Y 'rtp' -r captures/a/call_0_*.pcap -w rtp_only.pcap


## 动态工具注册

`load_phones` / `add_phone` / `drop_phone` 呼叫 `mcp.add_tool()` 和 `mcp.remove_tool()` 在运行时。MCP服务器通过以下方式宣布更改 `notifications/tools/list_changed`;兼容的客户端会立即重新扫描工具列表。

Fresh server

list_tools() → 14 static tools add_phone("alice", ...) → 14 + 22 = 36 tools (alice_make_call, alice_hangup, ...) add_phone("bob", ...) → 14 + 22·2 = 58 tools drop_phone("alice") → 14 + 22 = 36 tools


这 `tools_changed=True` 能力是MCP协议中的选择加入;服务器通过 `create_initialization_options` 猴子补丁在启动。

## 测试

### 单元测试

docker compose run --rm --entrypoint pytest pjsua-mcp tests/ -m "not integration" -v


涵盖SipEngine生命周期、PhoneRegistry CRUD+双帐户隔离、CallManager查找、PcapManager、SipLogWriter以及完整 **场景引擎** 套件:EventBus发布/订阅+线程、HookRuntime匹配语义、通过MockCallManager的每个有线操作、TimelineRecorder偏移、覆盖每个操作/事件前缀的飞行前验证器。快速(约7秒,约125次测试),无需网络。

### 集成测试(自包含)

docker compose -f docker-compose.test.yml run --build --rm test-runner


在隔离的Docker网络上,每个测试类运行一个MCP服务器子进程+一个Asterisk PBX容器(ext 6001/6002/6003)。练习注册、出站/入站呼叫、盲人+有人值守的转移、会议、编解码器协商、SIP MESSAGE、拒绝、历史、YAML配置文件加载(替换与合并)、动态工具添加/删除、跨电话有人值守转移拒绝、带配对pcap的每部电话录音布局和 `.meta.json` 侧三轮。

全套测试大约需要2分钟(大约90次测试)。

┌──────────────────────────────────────────────────────────┐ │ Docker Compose network: sipnet │ │ │ │ ┌──────────────────────────────────────────────────────┐│ │ │ test-runner container ││ │ │ ││ │ │ pytest spawns ONE MCP server subprocess per test ││ │ │ class. That server adds several phones via ││ │ │ add_phone / load_phones and drives them: ││ │ │ ││ │ │ ┌──────────────────────────────────────┐ ││ │ │ │ MCP Server (a, b, c managed inside) │ ││ │ │ └──────────────┬───────────────────────┘ ││ │ │ │ SIP/UDP ││ │ │ ▼ ││ │ │ ┌──────────────────────┐ ││ │ │ │ Asterisk PBX │ ││ │ │ │ ext 6001/6002/6003 │ ││ │ │ └──────────────────────┘ ││ │ └──────────────────────────────────────────────────────┘│ └──────────────────────────────────────────────────────────┘


## 发布到Harbor(或任何OCI注册处)

此仓库仅提供图像伪影。分发到客户端(包装器脚本、斜线命令、MCP配置)属于单独的 **插件仓库** 它按标签引用已发布的图像。

### 一次性设置

1. 将注册表坐标放入 `.env` (见 `.env.example`):

HARBOR_HOST=harbor.example.corp HARBOR_PROJECT=voip-tools HARBOR_IMAGE=pjsua-mcp

1. 缓存凭据一次: `docker login "$HARBOR_HOST"` --他们住在 `~/.docker/config.json`.

### 发布发布(手册)

./scripts/publish.sh v0.3.0 # builds, tags :v0.3.0 + :latest, pushes both ./scripts/publish.sh v0.3.0-rc1 --no-latest # pre-release — keep :latest pointing at stable ./scripts/publish.sh v0.3.0 --platform linux/amd64,linux/arm64 # multi-arch via buildx


脚本是只读的,直到 `docker push` 跑步——手动干跑是安全的。 `.dockerignore` 保持构建上下文较小(不包括 `captures/`, `recordings/`, `config/phones.yaml`, `.env`,CI文件),因此没有任何秘密或笨重的东西被运送到图像层中。

### 客户如何消费

在插件仓库的包装脚本中:

IMAGE="${PJSUA_MCP_IMAGE:-harbor.example.corp/voip-tools/pjsua-mcp:v0.3.0}" exec docker run -i --rm \ --network host \ --cap-add NET_RAW --cap-add NET_ADMIN \ --user "$(id -u):$(id -g)" \ -v "$CONFIG_DIR:/config:ro" \ -v "$DATA_DIR/captures:/captures" \ -v "$DATA_DIR/recordings:/recordings" \ "$IMAGE"


针脚a **特定semver标签** 在插件中——从不 `:latest` 对于生产客户来说——因此,一个破坏性的图像更改不会悄无声息地落在每个用户的机器上。

## 项目结构

pjsua_mcp/ ├── src/ │ ├── server.py # MCP entry point, 19 static tool definitions, lifespan │ ├── sip_engine.py # Endpoint lifecycle, per-phone transport create/close, codecs │ ├── account_manager.py # PhoneRegistry, PhoneConfig, SipAccount (emits reg.* / im.* events) │ ├── call_manager.py # SipCall, per-phone queues, incoming routing (emits call.state.* / dtmf.in) │ ├── phone_tool_factory.py # 22 closures × N phones; add_tool / remove_tool │ ├── sip_logger.py # Custom LogWriter → bounded deque │ ├── pcap_manager.py # tcpdump subprocess management │ └── scenario_engine/ # Event-driven scenario runtime │ ├── event_bus.py # Thread-safe pub/sub; wildcard subscribe; wait_for │ ├── hook_runtime.py # Arm hooks, match events, dispatch actions │ ├── action_executor.py # 19 actions → CallManager / PhoneRegistry / SipEngine │ ├── orchestrator.py # ScenarioRunner — arms hooks, runs initial_actions, awaits stop_on │ ├── timeline.py # Chronological event+action recorder with ms offsets │ └── validator.py # Pre-flight static checker (typos, unknown actions/events) ├── config/ │ ├── phones.example.yaml # YAML profile template (tracked) │ └── .gitignore # ignores phones.yaml (real credentials stay out of git) ├── audio/ │ └── moh.wav # Default MOH — CC0, FreeSWITCH/MUSOPEN ├── tests/ │ ├── conftest.py │ ├── test_sip_engine.py │ ├── test_sip_logger.py │ ├── test_account_manager.py # legacy single-account API kept compatible │ ├── test_phone_registry.py # multi-phone registry + two-account isolation │ ├── test_call_manager.py │ ├── test_pcap_manager.py │ ├── test_integration.py # end-to-end against Asterisk │ ├── scenario_engine/ # ~70 unit tests for the engine │ │ ├── test_event_bus.py │ │ ├── test_hook_runtime.py │ │ ├── test_orchestrator.py │ │ ├── test_timeline.py │ │ ├── test_validator.py │ │ └── test_actions_direct.py │ └── asterisk/ │ ├── Dockerfile │ ├── pjsip.conf │ ├── extensions.conf │ └── modules.conf ├── scripts/ │ └── publish.sh # Build + tag + push image to Harbor (manual one-liner) ├── Dockerfile # Multi-stage: build pjproject + runtime ├── .dockerignore # Trim build context (ignore recordings/captures/secrets) ├── docker-compose.yml # Mounts ./config (ro), ./recordings, ./captures ├── docker-compose.test.yml # Asterisk + test runner on sipnet ├── .env.example # UID/GID + HARBOR_HOST/HARBOR_PROJECT (copy to .env) ├── requirements.txt # mcp[cli], PyYAML, pydantic, pytest, jinja2, jsonschema ├── pyproject.toml └── .mcp.json # MCP client config for AI assistants


## 技术说明

- **Python 3.13+pjproject 2.14.1** --在多阶段Docker构建中从源代码构建。Python 3.13已删除 `distutils`,所以 `setuptools` 在构建SWIG绑定之前安装。
- **零音频设备** --在Docker中无头运行,没有声卡。ALSA库在运行时仍处于链接状态。
- **一 `pj.Endpoint`N `pj.Account`** --pjsua2的原生多账户模型。每部手机都有自己的UDP传输(`ep.transportCreate`),因此每部手机的数据包捕获和SIP Contact端口保持不同。
- **来电路由** --每个 `SipAccount`s `onIncomingCall` 回叫是通过每个电话的盖子连接的 `CallManager._make_incoming_handler`,所以电话打到了正确的电话 `_incoming_queue`.
- **线程模型** — `threadCnt=0` 使用asyncio线程的手动事件循环轮询(~50次轮询/秒)。SWIG控制器回调(LogWriter)不能从执行器线程可靠地工作。
- **stdout保护** --C级fd 1在启动时被重定向到stderr。MCP JSON-RPC使用原始stdout fd的保存副本。防止pjlib控制台输出损坏MCP通道。
- **SIP日志** — `consoleLevel=5` (匹配 `level=5`)确保全局日志级别不会被抑制。LogWriter将所有内容捕获到线程安全的有界双端队列中。
- **自动应答** --推迟到事件轮询循环(不在内部 `onIncomingCall`)以避免PJSUA2呼叫状态机问题。
- **录制** --每部手机 `recording_enabled` 标志(默认关闭--每个手机选择加入)。打开后,写信给 `/recordings/
/call___.wav` 加上a `.meta.json` 带有调用上下文和配对pcap路径的sidecar(当手机正在运行捕获时)。记录器在播放器设置后连接,以避免会议桥中断,并在每次重新连接时重新连接 `onCallMediaState`本地+远程音频混合成一个单声道WAV。切换 `recording_enabled` 中途通话 `update_phone` 打开/关闭不同的WAV段——每个段都有自己的sidecar——因此,如果操作员想要更细粒度的捕获,一个调用可以发出多个记录。
- **自动捕获** --每部手机 `capture_enabled` 标志(默认关闭)。打开专用 `tcpdump -i any udp` 对第一个音频活动呼叫进行子处理,并在最后一次断开连接时将其关闭。过滤器保持宽泛,因此重新邀请RTP端口更改不会丢弃数据包;将SIP和RTP分开 `tshark -Y` 事后。启动/停止请求来自pj回调线程;实际的子进程启动通过基于双端队列的挂起队列在asyncio轮询循环上运行(模式与 `process_auto_answers`).会议(一部手机上的2+个电话)共享一个pcap,通过以下方式计算 `_active_calls_by_phone`.
- **参数** --音频播放器重新连接到新的 `aud_med` 端口重新INVITE(编解码器更改、会议转换)后,TX继续流动。
- **动态刀具注册** — `tools_changed=True` 通过以下方式启用功能 `create_initialization_options` 猴斑; `ctx.session.send_tool_list_changed()` 每次添加/删除手机后都会触发(每批一次 `load_phones`).
- **旧电话清理** --断开连接的呼叫将从跟踪中删除;在重新注册之前,帐户会被关闭,以防止幽灵会话。
- **单点故障** --一个集装箱的碰撞导致所有N部手机掉落。可用于开发/测试台。Docker编写可以 `restart: unless-stopped` 如果你需要韧性。
- **卫生部** --西班牙组曲Op.47-Leyenda(阿尔贝尼兹),古典吉他,来自FreeSWITCH/MUSOPEN的CC0公共领域。

目录标签

目录标签

DockerClaude开发工具SIP通信Python本地部署自动化测试多用户代理事件驱动Docker部署

支持客户端

Claude

接入字段

传输方式(transport,传输协议)

未说明

鉴权方式(authType,认证方式)

session

工具数量(toolCount,工具数)

34

资源数量(resourceCount,资源数)

0

提示词数量(promptCount,提示词数)

0

权限和风险

未说明session部署方式未说明

接入前请确认传输方式、认证方式和部署位置,并根据实际工具能力限制访问范围。

安装前确认

不要直接授予不必要的文件、网络或账号权限;先核对安装命令和配置内容。

仍需确认:installCommand

来源信息

继续浏览同类 MCP