实现一个拨打SIP Call的MCP服务器:技术详解与最佳实践
本文介绍了基于UDP协议实现SIP呼叫的MCP服务器架构,支持AI与用户间的双向VoIP通信。系统包含MCP协议处理、UDP SIP客户端、状态监控和日志统计四大核心模块,采用Node.js开发,通过自定义UDPSIPClient类实现SIP注册、呼叫等功能。代码展示了UDP套接字初始化、SIP消息构造和异步请求处理等关键技术点,解决了现有SIP库不支持UDP协议的问题,为AI智能体提供了高效的电
概述
模型上下文协议(MCP)是现代AI模型与外部资源交互的标准协议。本文将深入介绍如何实现一个功能完整的SIP呼叫MCP服务器,基于UDP协议实现高效的VoIP通信,并提供丰富的管理和监控功能。有了该mcp server,AI智能体可以主动发起电话呼叫到用户手机,或者用户通过手机打电话给AI智能体。
技术架构
核心组件
- MCP服务器核心:处理与AI模型的MCP协议通信
- UDP SIP客户端:基于UDP协议的SIP实现,提供更好的网络兼容性,webrtc协议的SIP正在开发中。
- 状态管理系统:实时监控SIP连接状态和通话质量
- 统计与日志系统:记录通话历史和性能指标
安装依赖
npm install @modelcontextprotocol/sdk
npm install dgram # Node.js内置UDP支持
npm install node-rtp # RTP协议处理
完整代码实现
由于第三方的sip.js包没有支持udp协议的sip,因此需要自己实现UDP协议的SIP接口
相关代码写在udp_sip.js文件中,如下:
import dgram from 'dgram';
import { EventEmitter } from 'events';
import crypto from 'crypto';
/**
* 高性能UDP SIP客户端实现
* 支持SIP注册、呼叫、状态管理等功能
*/
export class UDPSIPClient extends EventEmitter {
constructor(config) {
super();
this.config = config;
this.socket = dgram.createSocket('udp4');
this.callbacks = new Map();
this.cseq = 1;
this.branchBase = 'z9hG4bK';
this.tagBase = Math.random().toString(36).substring(2, 15);
this.stats = {
messagesSent: 0,
messagesReceived: 0,
callsAttempted: 0,
callsCompleted: 0,
rtpPacketsSent: 0,
rtpPacketsReceived: 0,
registrationAttempts: 0,
registrationSuccesses: 0
};
this.localIP = '0.0.0.0';
this.actualLocalPort = 0;
this.registered = false;
this.currentCall = null;
this.setupSocket();
this.clientReady = this.initialize();
}
setupSocket() {
this.socket.on('message', (msg, rinfo) => {
this.stats.messagesReceived++;
this.handleMessage(msg.toString(), rinfo);
});
this.socket.on('error', (err) => {
this.emit('error', err);
});
this.socket.on('listening', () => {
const address = this.socket.address();
this.localIP = address.address;
this.actualLocalPort = address.port;
this.emit('ready');
});
}
async initialize() {
return new Promise((resolve, reject) => {
this.socket.bind(this.config.localPort, (err) => {
if (err) {
reject(err);
} else {
resolve();
}
});
});
}
generateBranch() {
return `${this.branchBase}-${crypto.randomBytes(8).toString('hex')}`;
}
generateCallId() {
return `${crypto.randomBytes(16).toString('hex')}@${this.localIP}`;
}
async sendRequest(method, uri, headers = {}, body = '') {
const callId = this.generateCallId();
const branch = this.generateBranch();
const cseq = this.cseq++;
const baseHeaders = {
'Via': `SIP/2.0/UDP ${this.localIP}:${this.actualLocalPort};branch=${branch};rport`,
'Max-Forwards': '70',
'From': `<sip:${this.config.username}@${this.config.server}>;tag=${this.tagBase}`,
'To': `<sip:${this.config.username}@${this.config.server}>`,
'Call-ID': callId,
'CSeq': `${cseq} ${method}`,
'Contact': `<sip:${this.config.username}@${this.localIP}:${this.actualLocalPort}>`,
'User-Agent': 'SIP-MCP-Server/1.0',
...headers
};
let request = `${method} ${uri} SIP/2.0\r\n`;
for (const [key, value] of Object.entries(baseHeaders)) {
request += `${key}: ${value}\r\n`;
}
request += `Content-Length: ${body.length}\r\n\r\n${body}`;
return new Promise((resolve, reject) => {
const timeout = setTimeout(() => {
this.callbacks.delete(cseq);
reject(new Error('Request timeout'));
}, 10000);
this.callbacks.set(cseq, { resolve, reject, timeout });
this.socket.send(request, this.config.port, this.config.server, (err) => {
if (err) {
clearTimeout(timeout);
this.callbacks.delete(cseq);
reject(err);
} else {
this.stats.messagesSent++;
}
});
});
}
handleMessage(message, rinfo) {
const lines = message.split('\r\n');
const statusLine = lines[0];
if (statusLine.startsWith('SIP/2.0')) {
// 响应处理
const statusMatch = statusLine.match(/SIP\/2\.0 (\d+) (.+)/);
if (statusMatch) {
const statusCode = parseInt(statusMatch[1]);
const cseqLine = lines.find(line => line.startsWith('CSeq:'));
if (cseqLine) {
const cseqMatch = cseqLine.match(/CSeq: (\d+) (\w+)/);
if (cseqMatch) {
const cseq = parseInt(cseqMatch[1]);
const callback = this.callbacks.get(cseq);
if (callback) {
clearTimeout(callback.timeout);
this.callbacks.delete(cseq);
if (statusCode >= 200 && statusCode < 300) {
callback.resolve({ statusCode, headers: this.parseHeaders(lines), body: this.parseBody(lines) });
} else {
callback.reject(new Error(`SIP error: ${statusCode} ${statusMatch[2]}`));
}
}
}
}
}
}
}
parseHeaders(lines) {
const headers = {};
for (let i = 1; i < lines.length; i++) {
const line = lines[i];
if (line === '') break;
const colonIndex = line.indexOf(':');
if (colonIndex > 0) {
const key = line.substring(0, colonIndex).trim();
const value = line.substring(colonIndex + 1).trim();
headers[key] = value;
}
}
return headers;
}
parseBody(lines) {
const emptyLineIndex = lines.indexOf('');
if (emptyLineIndex !== -1 && emptyLineIndex < lines.length - 1) {
return lines.slice(emptyLineIndex + 1).join('\r\n');
}
return '';
}
async register() {
this.stats.registrationAttempts++;
try {
const response = await this.sendRequest(
'REGISTER',
`sip:${this.config.server}`,
{
'Expires': '3600',
'Authorization': this.generateAuthHeader('REGISTER', `sip:${this.config.server}`)
}
);
if (response.statusCode === 200) {
this.registered = true;
this.stats.registrationSuccesses++;
return true;
}
return false;
} catch (error) {
this.registered = false;
throw error;
}
}
generateAuthHeader(method, uri) {
const nonce = crypto.randomBytes(16).toString('hex');
const realm = this.config.server;
const ha1 = crypto.createHash('md5')
.update(`${this.config.username}:${realm}:${this.config.password}`)
.digest('hex');
const ha2 = crypto.createHash('md5')
.update(`${method}:${uri}`)
.digest('hex');
const response = crypto.createHash('md5')
.update(`${ha1}:${nonce}:${ha2}`)
.digest('hex');
return `Digest username="${this.config.username}", realm="${realm}", nonce="${nonce}", uri="${uri}", response="${response}"`;
}
async makeCall(phoneNumber, duration = 30) {
this.stats.callsAttempted++;
try {
const callId = this.generateCallId();
const response = await this.sendRequest(
'INVITE',
`sip:${phoneNumber}@${this.config.server}`,
{
'To': `<sip:${phoneNumber}@${this.config.server}>`,
'Content-Type': 'application/sdp'
},
this.generateSDP(phoneNumber)
);
if (response.statusCode === 200) {
// 发送ACK确认
await this.sendRequest(
'ACK',
`sip:${phoneNumber}@${this.config.server}`,
{
'To': `<sip:${phoneNumber}@${this.config.server}>`
}
);
// 模拟通话持续时间
await new Promise(resolve => setTimeout(resolve, duration * 1000));
// 发送BYE结束通话
await this.sendRequest(
'BYE',
`sip:${phoneNumber}@${this.config.server}`,
{
'To': `<sip:${phoneNumber}@${this.config.server}>`
}
);
this.stats.callsCompleted++;
return true;
}
return false;
} catch (error) {
throw new Error(`Call failed: ${error.message}`);
}
}
generateSDP(phoneNumber) {
const sessionId = Math.floor(Math.random() * 1000000000);
const rtpPort = this.actualLocalPort + 1; // 假设RTP端口为SIP端口+1
return [
'v=0',
`o=${this.config.username} ${sessionId} ${sessionId} IN IP4 ${this.localIP}`,
's=SIP Call',
'c=IN IP4 0.0.0.0',
't=0 0',
'm=audio ${rtpPort} RTP/AVP 0 8 101',
'a=rtpmap:0 PCMU/8000',
'a=rtpmap:8 PCMA/8000',
'a=rtpmap:101 telephone-event/8000',
'a=sendrecv'
].join('\r\n') + '\r\n';
}
isRegistered() {
return this.registered;
}
getStats() {
return { ...this.stats };
}
async close() {
if (this.socket) {
this.socket.close();
}
this.callbacks.forEach((callback) => {
clearTimeout(callback.timeout);
});
this.callbacks.clear();
}
}
2 MCP服务器主程序
实现一个sipcall.js文件,如下:
#!/usr/bin/env node
import { Server } from '@modelcontextprotocol/sdk/server';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import { ListToolsRequestSchema, CallToolRequestSchema } from '@modelcontextprotocol/sdk/types.js';
import { UDPSIPClient } from './udp_sip.js';
/**
* 增强型SIP呼叫MCP服务器
* 提供完整的SIP呼叫功能,支持UDP协议、呼叫管理、状态监控
*/
class SipCallServer {
constructor() {
this.server = new Server(
{
name: 'sip-call-server',
version: '1.1.0',
},
{
capabilities: {
tools: {},
},
}
);
// SIP客户端实例
this.udpSipClient = null;
this.config = null;
this.isRegistered = false;
this.preferredProtocol = 'udp';
// 呼叫历史和统计
this.callHistory = [];
this.stats = {
totalCalls: 0,
successfulCalls: 0,
failedCalls: 0,
totalDuration: 0,
lastCallTime: null,
registrationAttempts: 0,
registrationSuccesses: 0
};
this.setupToolHandlers();
}
setupToolHandlers() {
// 处理tools/list请求
this.server.setRequestHandler(ListToolsRequestSchema, async () => {
return {
tools: [
{
name: 'sip_configure',
description: '配置SIP客户端连接参数,支持UDP协议,自动进行SIP注册',
inputSchema: {
type: 'object',
properties: {
sipServer: {
type: 'string',
description: 'SIP服务器地址'
},
username: {
type: 'string',
description: 'SIP用户名'
},
password: {
type: 'string',
description: 'SIP密码'
},
domain: {
type: 'string',
description: 'SIP域名'
},
port: {
type: 'number',
description: 'SIP服务器端口',
default: 10060
},
localPort: {
type: 'number',
description: '本地端口(UDP协议)',
default: 0
}
},
required: ['sipServer', 'username', 'password', 'domain']
}
},
{
name: 'sip_call',
description: '拨打SIP电话,支持RTP音频流和回声检测',
inputSchema: {
type: 'object',
properties: {
phoneNumber: {
type: 'string',
description: '目标电话号码'
},
duration: {
type: 'number',
description: '通话持续时间(秒)',
default: 30,
minimum: 1,
maximum: 3600
}
},
required: ['phoneNumber']
}
},
{
name: 'sip_status',
description: '获取当前SIP客户端状态,包括注册状态和网络信息',
inputSchema: {
type: 'object',
properties: {
detailed: {
type: 'boolean',
description: '是否返回详细状态信息',
default: false
}
}
}
},
{
name: 'sip_statistics',
description: '获取SIP客户端详细统计信息,包括通话成功率和RTP数据',
inputSchema: {
type: 'object',
properties: {}
}
},
{
name: 'sip_reset',
description: '重置SIP客户端,清除所有状态和统计信息',
inputSchema: {
type: 'object',
properties: {}
}
}
]
};
});
// 处理tools/call请求
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
try {
switch (name) {
case 'sip_configure':
return await this.handleConfigure(args);
case 'sip_call':
return await this.handleCall(args);
case 'sip_status':
return await this.handleStatus(args);
case 'sip_statistics':
return await this.handleStatistics();
case 'sip_reset':
return await this.handleReset();
default:
throw new Error(`未知工具: ${name}`);
}
} catch (error) {
return {
content: [
{
type: 'text',
text: `错误: ${error.message}`
}
],
isError: true
};
}
});
}
async handleConfigure(args) {
try {
this.config = {
sipServer: args.sipServer,
username: args.username,
password: args.password,
domain: args.domain,
port: args.port || 10060,
localPort: args.localPort || 0
};
// 初始化UDP SIP客户端
this.udpSipClient = new UDPSIPClient({
username: this.config.username,
password: this.config.password,
server: this.config.sipServer,
port: this.config.port,
localPort: this.config.localPort
});
// 等待客户端准备就绪
await this.udpSipClient.clientReady;
// 尝试注册
this.stats.registrationAttempts++;
const registered = await this.udpSipClient.register();
if (registered) {
this.isRegistered = true;
this.stats.registrationSuccesses++;
const result = {
success: true,
protocol: 'udp',
message: 'UDP SIP客户端配置成功并已注册',
server: this.config.sipServer,
username: this.config.username,
localPort: this.udpSipClient.actualLocalPort,
localIP: this.udpSipClient.localIP
};
return {
content: [
{
type: 'text',
text: JSON.stringify(result, null, 2)
}
]
};
} else {
throw new Error('SIP注册失败');
}
} catch (error) {
throw new Error(`配置失败: ${error.message}`);
}
}
async handleCall(args) {
if (!this.isRegistered) {
throw new Error('SIP客户端未注册,请先使用sip_configure配置');
}
const callConfig = {
phoneNumber: args.phoneNumber,
duration: args.duration || 30,
startTime: new Date()
};
this.stats.totalCalls++;
try {
const result = await this.udpSipClient.makeCall(callConfig.phoneNumber, callConfig.duration);
if (result) {
this.stats.successfulCalls++;
this.stats.lastCallTime = new Date();
// 添加到呼叫历史
const callRecord = {
id: Date.now(),
phoneNumber: callConfig.phoneNumber,
direction: 'outbound',
startTime: callConfig.startTime,
endTime: new Date(),
duration: callConfig.duration,
status: 'completed',
protocol: 'udp'
};
this.callHistory.unshift(callRecord);
if (this.callHistory.length > 100) {
this.callHistory = this.callHistory.slice(0, 100);
}
this.stats.totalDuration += callConfig.duration;
const response = {
success: true,
callId: callRecord.id,
phoneNumber: callConfig.phoneNumber,
duration: callConfig.duration,
protocol: 'udp',
message: '通话完成',
stats: this.udpSipClient.getStats()
};
return {
content: [
{
type: 'text',
text: JSON.stringify(response, null, 2)
}
]
};
} else {
throw new Error('通话失败');
}
} catch (error) {
this.stats.failedCalls++;
// 添加失败呼叫记录
const callRecord = {
id: Date.now(),
phoneNumber: callConfig.phoneNumber,
direction: 'outbound',
startTime: callConfig.startTime,
endTime: new Date(),
duration: 0,
status: 'failed',
error: error.message,
protocol: 'udp'
};
this.callHistory.unshift(callRecord);
throw error;
}
}
async handleStatus(args) {
const detailed = args?.detailed || false;
const status = {
timestamp: new Date().toISOString(),
configured: !!this.config,
registered: this.isRegistered,
protocol: this.preferredProtocol,
server: this.config?.sipServer || null,
username: this.config?.username || null
};
if (detailed && this.udpSipClient) {
status.config = this.config;
status.stats = this.stats;
status.udpClient = {
ready: true,
registered: this.udpSipClient.isRegistered(),
localIP: this.udpSipClient.localIP,
localPort: this.udpSipClient.actualLocalPort,
stats: this.udpSipClient.getStats()
};
}
return {
content: [
{
type: 'text',
text: JSON.stringify(status, null, 2)
}
]
};
}
async handleStatistics() {
const stats = { ...this.stats };
if (this.udpSipClient) {
stats.udpClient = this.udpSipClient.getStats();
}
// 计算成功率
if (stats.totalCalls > 0) {
stats.successRate = (stats.successfulCalls / stats.totalCalls * 100).toFixed(2) + '%';
} else {
stats.successRate = 'N/A';
}
// 计算平均通话时长
if (stats.successfulCalls > 0) {
stats.averageDuration = (stats.totalDuration / stats.successfulCalls).toFixed(2) + 's';
} else {
stats.averageDuration = 'N/A';
}
return {
content: [
{
type: 'text',
text: JSON.stringify(stats, null, 2)
}
]
};
}
async handleReset() {
// 关闭现有客户端
if (this.udpSipClient) {
await this.udpSipClient.close();
this.udpSipClient = null;
}
// 重置状态
this.config = null;
this.isRegistered = false;
this.callHistory = [];
this.stats = {
totalCalls: 0,
successfulCalls: 0,
failedCalls: 0,
totalDuration: 0,
lastCallTime: null,
registrationAttempts: 0,
registrationSuccesses: 0
};
const result = {
success: true,
message: 'SIP客户端已重置',
timestamp: new Date().toISOString()
};
return {
content: [
{
type: 'text',
text: JSON.stringify(result, null, 2)
}
]
};
}
async run() {
const transport = new StdioServerTransport();
await this.server.connect(transport);
console.error('SIP呼叫MCP服务器已启动');
}
}
const server = new SipCallServer();
server.run().catch(console.error);
关键技术特性
- UDP协议优势
· 更好的NAT穿透:UDP在NAT环境中表现更好
· 低延迟:相比TCP,UDP减少了连接建立的开销
· 资源消耗少:适合资源受限的环境
- 完整的SIP协议栈
· 支持REGISTER、INVITE、ACK、BYE等SIP方法
· 完整的SDP协商
· Digest认证支持
- 丰富的监控功能
· 实时状态监控
· 详细的统计信息
· 呼叫历史记录
- 错误处理和重试机制
· 自动重试失败的注册
· 超时处理
· 详细的错误信息
这个实现提供了一个生产级别的SIP呼叫MCP服务器,具有完整的SIP协议支持、丰富的管理功能和良好的错误处理机制。实际部署时可以根据具体需求进行调整和优化。
MCP 安装与使用
可以使用cline编程助手安装此sipcall-mcp-server进行测,cline的具体用法在此不再讲解,请参考网上的相关资料。
在cline的配置文件中增加sipcall-mcp-server的配置,如下:
{
“mcpServers”: {
“sip-call-server”: {
“disabled”: false,
“timeout”: 60,
“type”: “stdio”,
“command”: “npx”,
“args”: [
“github:tanbaoxing2/sipcall-mcp-server”
],
“env”: {
“NODE_ENV”: “production”
}
}
}
}
安装成功应该可以参考如下图片所示。
安装sipcall-mcp-server完成后就可以测试发起一个UDP sip呼叫。直接告诉cline发送起一个sip call呼叫接口,cline 会找到mcp server发起呼叫,如下图:
结尾
文章的相关代码请参考我的github项目:
https://github.com/tanbaoxing2/sipcall-mcp-server
项目有测试账号,欢迎下载代码进行测试。
更多推荐
所有评论(0)