概述

模型上下文协议(MCP)是现代AI模型与外部资源交互的标准协议。本文将深入介绍如何实现一个功能完整的SIP呼叫MCP服务器,基于UDP协议实现高效的VoIP通信,并提供丰富的管理和监控功能。有了该mcp server,AI智能体可以主动发起电话呼叫到用户手机,或者用户通过手机打电话给AI智能体。

技术架构

核心组件

  1. MCP服务器核心:处理与AI模型的MCP协议通信
  2. UDP SIP客户端:基于UDP协议的SIP实现,提供更好的网络兼容性,webrtc协议的SIP正在开发中。
  3. 状态管理系统:实时监控SIP连接状态和通话质量
  4. 统计与日志系统:记录通话历史和性能指标

安装依赖

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);

关键技术特性

  1. UDP协议优势

· 更好的NAT穿透:UDP在NAT环境中表现更好
· 低延迟:相比TCP,UDP减少了连接建立的开销
· 资源消耗少:适合资源受限的环境

  1. 完整的SIP协议栈

· 支持REGISTER、INVITE、ACK、BYE等SIP方法
· 完整的SDP协商
· Digest认证支持

  1. 丰富的监控功能

· 实时状态监控
· 详细的统计信息
· 呼叫历史记录

  1. 错误处理和重试机制

· 自动重试失败的注册
· 超时处理
· 详细的错误信息

这个实现提供了一个生产级别的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
项目有测试账号,欢迎下载代码进行测试。

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐