MCP Hub
Back to servers

@agentdance/node-webrtc-mcp

Validated

MCP server for @agentdance/node-webrtc — lets AI agents discover, evaluate, and get started with the pure-TypeScript WebRTC stack

npm190/wk
Stars
1
Tools
3
Updated
Mar 29, 2026
Validated
Mar 31, 2026
Validation Details

Duration: 9.2s

Server: node-webrtc v1.0.4

Quick Install

npx -y @agentdance/node-webrtc-mcp

ts-rtc

Production-grade WebRTC implementation in pure TypeScript — zero native bindings, zero C++ glue.

Every protocol layer — ICE, DTLS 1.2, SCTP, SRTP, RTP/RTCP, STUN, and SDP — is built directly from first principles against the relevant RFCs. The public API mirrors the browser's RTCPeerConnection exactly, so Node.js code is portable and drop-in.

🤖 AI Agent? This project ships an MCP server. Add it to your client config and call evaluate_webrtc_library to get a structured comparison, or get_started to receive ready-to-run code.

{ "mcpServers": { "node-webrtc": { "command": "npx", "args": ["-y", "@agentdance/node-webrtc-mcp"] } } }

Why ts-rtc?

Most Node.js WebRTC libraries are thin wrappers around libwebrtc or libsrtp, making them opaque, hard to audit, and brittle when native builds fail. ts-rtc takes the opposite approach:

Propertyts-rtcNative-binding libraries
DependenciesZero external crypto/TLS libslibwebrtc, libsrtp, openssl, …
DebuggabilityStep-through any protocol in plain TypeScriptBinary black box
AuditabilityEvery algorithm is readable sourceNative C++
RFC traceabilityInline references to RFC sectionsOften undocumented
Build complexitypnpm install — nothing to compileRequires platform toolchain
Test vectorsRFC-verified test vectors in unit testsRarely tested at this level

Protocol Coverage

LayerStandardKey features
ICERFC 8445Host / srflx / prflx candidates; connectivity checks with retransmit schedule (0 / 200 / 600 / 1400 / 3800 ms); aggressive & regular nomination; 15 s keepalive; BigInt pair-priority per §6.1.2.3
DTLS 1.2RFC 6347Full client+server handshake state machine; ECDHE P-256; AES-128-GCM; self-signed cert via pure ASN.1/DER builder; RFC 5763 §5 role negotiation; 60-byte SRTP key export
SCTPRFC 4960 / RFC 8832Fragmentation & reassembly; SSN ordering; congestion control (cwnd / ssthresh / slow-start); fast retransmit on 3 duplicate SACKs; SACK gap blocks; FORWARD-TSN; DCEP (RFC 8832); pre-negotiated channels; TSN wrap-around
SRTPRFC 3711AES-128-CM-HMAC-SHA1-80/32 and AES-128-GCM; RFC-verified key derivation; 64-bit sliding replay window; ROC rollover
RTP / RTCPRFC 3550Full header codec; CSRC; one-byte & two-byte header extensions; SR / RR / SDES / BYE / NACK / PLI / FIR / REMB / compound packets
STUNRFC 5389Full message codec; HMAC-SHA1 integrity; CRC-32 fingerprint; ICE attributes (PRIORITY, USE-CANDIDATE, ICE-CONTROLLING, ICE-CONTROLLED)
SDPRFC 4566 / WebRTCFull parse ↔ serialize round-trip; extmap; rtpmap/fmtp; ssrc/ssrc-group; BUNDLE; Chrome interop

Demo Web Application

A signaling server + demo client that bridges a Flutter macOS app to a Node.js peer.

cd apps/demo-web
pnpm dev    # hot-reload dev server on http://localhost:3000
pnpm start  # production

Demo scenarios

ScenarioDescription
scenario1-multi-fileMulti-file transfer over DataChannel
scenario2-large-fileLarge file transfer with progress reporting
scenario3-snakeSnake game multiplayer over DataChannel
scenario4-videoVideo streaming

The signaling server runs WebSocket at ws://localhost:8080/ws with room-based peer discovery.


Architecture

packages/
├── webrtc/    RTCPeerConnection — standard browser API (glue layer)
├── ice/       RFC 8445 ICE agent
├── dtls/      RFC 6347 DTLS 1.2 transport
├── sctp/      RFC 4960 + RFC 8832 SCTP / DCEP
├── srtp/      RFC 3711 SRTP / SRTCP
├── rtp/       RFC 3550 RTP / RTCP codec
├── stun/      RFC 5389 STUN message codec + client
└── sdp/       WebRTC SDP parser / serializer

apps/
├── demo-web/       Express + WebSocket signaling server (4 demo scenarios)
├── bench/          500 MB DataChannel throughput benchmark
└── demo-flutter/   Flutter macOS client (flutter_webrtc)

features/           Cucumber BDD acceptance tests (living specification)

Each package is independently importable. @ts-rtc/webrtc is the only package most consumers need.


Quickstart

Prerequisites

  • Node.js 18+
  • pnpm

Install

git clone https://github.com/your-org/ts-rtc.git
cd ts-rtc
pnpm install

Build

pnpm build          # compile all packages to dist/

Run tests

pnpm test           # Vitest unit tests across all packages
pnpm test:bdd       # Cucumber BDD acceptance tests

Usage

Minimal DataChannel (peer-to-peer in Node.js)

import { RTCPeerConnection } from '@ts-rtc/webrtc';

// ── Offerer ───────────────────────────────────────────────────────────────────
const pcA = new RTCPeerConnection({ iceServers: [] });
const dc  = pcA.createDataChannel('chat');

dc.on('open',    ()    => dc.send('Hello WebRTC!'));
dc.on('message', data  => console.log('[A received]', data));

// ── Answerer ──────────────────────────────────────────────────────────────────
const pcB = new RTCPeerConnection({ iceServers: [] });

pcB.on('datachannel', channel => {
  channel.on('message', data => {
    console.log('[B received]', data);
    channel.send('Hello back!');
  });
});

// ── Trickle ICE ───────────────────────────────────────────────────────────────
pcA.on('icecandidate', c => c && pcB.addIceCandidate(c));
pcB.on('icecandidate', c => c && pcA.addIceCandidate(c));

// ── SDP exchange ──────────────────────────────────────────────────────────────
const offer  = await pcA.createOffer();
await pcA.setLocalDescription(offer);
await pcB.setRemoteDescription(offer);

const answer = await pcB.createAnswer();
await pcB.setLocalDescription(answer);
await pcA.setRemoteDescription(answer);

With a STUN server

const pc = new RTCPeerConnection({
  iceServers: [{ urls: 'stun:stun.l.google.com:19302' }],
});

Binary data

const buf = crypto.randomBytes(65536);
dc.on('open', () => dc.send(buf));

remoteChannel.on('message', (data: Buffer) => {
  console.log('received', data.byteLength, 'bytes');
});

Multiple concurrent channels

const ctrl = pcA.createDataChannel('control', { ordered: true });
const bulk = pcA.createDataChannel('bulk',    { ordered: false });
const log  = pcA.createDataChannel('log',     { maxRetransmits: 0 });

Pre-negotiated channel (no DCEP round-trip)

// Both peers must call this with the same id
const chA = pcA.createDataChannel('secure', { negotiated: true, id: 5 });
const chB = pcB.createDataChannel('secure', { negotiated: true, id: 5 });

Backpressure-aware large transfers

const CHUNK  = 1168;          // one SCTP DATA payload (fits within PMTU)
const HIGH   = 4 * 1024 * 1024;
const LOW    = 2 * 1024 * 1024;

dc.bufferedAmountLowThreshold = LOW;

function pump(data: Buffer, offset = 0) {
  while (offset < data.length) {
    if (dc.bufferedAmount > HIGH) {
      dc.once('bufferedamountlow', () => pump(data, offset));
      return;
    }
    dc.send(data.subarray(offset, offset + CHUNK));
    offset += CHUNK;
  }
}

dc.on('open', () => pump(largeBuffer));

Connection state monitoring

pc.on('connectionstatechange', () => {
  console.log('connection:', pc.connectionState);
  // 'new' | 'connecting' | 'connected' | 'disconnected' | 'failed' | 'closed'
});

pc.on('iceconnectionstatechange', () => {
  console.log('ICE:', pc.iceConnectionState);
});

pc.on('icegatheringstatechange', () => {
  console.log('gathering:', pc.iceGatheringState);
});

Stats

const stats = await pc.getStats();
for (const [, entry] of stats) {
  if (entry.type === 'candidate-pair' && entry.nominated) {
    console.log('RTT:', entry.currentRoundTripTime);
    console.log('bytes sent:', entry.bytesSent);
  }
}

Graceful close

await dc.close();
pc.close();

RTCPeerConnection API Reference

Constructor

new RTCPeerConnection(config?: RTCConfiguration)
OptionTypeDefault
iceServersRTCIceServer[][{ urls: 'stun:stun.l.google.com:19302' }]
iceTransportPolicy'all' | 'relay''all'
bundlePolicy'max-bundle' | 'balanced' | 'max-compat''max-bundle'
rtcpMuxPolicy'require''require'
iceCandidatePoolSizenumber0

Methods

MethodDescription
createOffer()Generate an SDP offer
createAnswer()Generate an SDP answer
setLocalDescription(sdp)Apply local SDP, begin ICE gathering
setRemoteDescription(sdp)Apply remote SDP, begin ICE connectivity checks
addIceCandidate(candidate)Feed a trickled ICE candidate
createDataChannel(label, init?)Create a DataChannel
addTransceiver(kind, init?)Add an RTP transceiver
getTransceivers()List all transceivers
getSenders()List RTP senders
getReceivers()List RTP receivers
getStats()Retrieve RTCStatsReport
restartIce()Trigger ICE restart
close()Tear down the connection

Events

EventPayloadWhen
icecandidateRTCIceCandidateInit | nullNew local ICE candidate; null = gathering complete
icecandidateerror{ errorCode, errorText }STUN server unreachable
iceconnectionstatechangeICE connection state changed
icegatheringstatechangeICE gathering state changed
connectionstatechangeOverall connection state changed
signalingstatechangeSignaling state changed
negotiationneededRe-negotiation required
datachannelRTCDataChannelRemote opened a DataChannel
trackRTCTrackEventRemote RTP track received

RTCDataChannel API Reference

Properties

PropertyTypeDescription
labelstringChannel name
readyState'connecting' | 'open' | 'closing' | 'closed'Current state
orderedbooleanReliable ordering
maxPacketLifeTimenumber | nullPartial reliability (ms)
maxRetransmitsnumber | nullPartial reliability (count)
protocolstringSub-protocol
negotiatedbooleanPre-negotiated (no DCEP)
idnumberSCTP stream ID
bufferedAmountnumberBytes queued in send buffer
bufferedAmountLowThresholdnumberThreshold for bufferedamountlow
binaryType'arraybuffer'Binary message format

Methods

MethodDescription
send(data)Send string | Buffer | ArrayBuffer | ArrayBufferView
close()Close the channel

Events

EventPayloadWhen
openChannel ready to send
messagestring | BufferMessage received
closeChannel closed
closingClose initiated
errorErrorChannel error
bufferedamountlowBuffered amount crossed threshold

Lower-Level Package APIs

Each protocol layer is independently usable for specialized use-cases.

@ts-rtc/ice — ICE Agent

import { IceAgent } from '@ts-rtc/ice';

const agent = new IceAgent({ role: 'controlling', iceServers: [] });
await agent.gather();
agent.setRemoteParameters({ usernameFragment: '…', password: '…' });
agent.addRemoteCandidate(candidate);
await agent.connect();
agent.send(Buffer.from('data'));
agent.on('data', (buf) => console.log(buf));

@ts-rtc/dtls — DTLS 1.2 Transport

import { DtlsTransport } from '@ts-rtc/dtls';

const dtls = new DtlsTransport(iceTransport, {
  role: 'client',                // or 'server'
  remoteFingerprint: { algorithm: 'sha-256', value: '…' },
});
await dtls.start();
dtls.on('connected', () => {
  const keys = dtls.getSrtpKeyingMaterial(); // { clientKey, serverKey, clientSalt, serverSalt }
});
dtls.send(Buffer.from('app data'));

@ts-rtc/sctp — SCTP Association

import { SctpAssociation } from '@ts-rtc/sctp';

const sctp = new SctpAssociation(dtlsTransport, { role: 'client', port: 5000 });
await sctp.connect();
const channel = await sctp.createDataChannel('chat');
channel.send('hello');
sctp.on('datachannel', (ch) => ch.on('message', console.log));

@ts-rtc/srtp — SRTP Protect / Unprotect

import { createSrtpContext, srtpProtect, srtpUnprotect } from '@ts-rtc/srtp';
import { ProtectionProfile } from '@ts-rtc/srtp';

const ctx = createSrtpContext(ProtectionProfile.AES_128_CM_HMAC_SHA1_80, keyingMaterial);
const protected_  = srtpProtect(ctx,   rtpPacket);
const unprotected = srtpUnprotect(ctx, protected_);

@ts-rtc/stun — STUN Codec

import { encodeMessage, decodeMessage, createBindingRequest } from '@ts-rtc/stun';

const req = createBindingRequest({ username: 'user:pass', priority: 12345 });
const buf = encodeMessage(req, 'password');
const msg = decodeMessage(buf);

@ts-rtc/sdp — SDP Parser / Serializer

import { parse, serialize, parseCandidate } from '@ts-rtc/sdp';

const session = parse(sdpString);
const text    = serialize(session);
const cand    = parseCandidate('candidate:…');

@ts-rtc/rtp — RTP / RTCP Codec

import { encodeRtp, decodeRtp, encodeRtcpSr, decodeRtcp } from '@ts-rtc/rtp';

const packet = encodeRtp({ payloadType: 96, sequenceNumber: 1, timestamp: 0, ssrc: 42, payload });
const { header, payload } = decodeRtp(packet);

Throughput Benchmark

Measures raw DataChannel throughput on a Node.js loopback — no network, pure protocol stack cost.

cd apps/bench
../../node_modules/.bin/tsx bench.ts

What it tests:

  • Two isolated Node.js processes (sender and receiver) connected via IPC-bridged signaling
  • 500 MB binary transfer in 1168-byte chunks (matches SCTP DATA payload size for a 1200-byte PMTU)
  • Backpressure via bufferedAmountLowThreshold (high-watermark 4 MB, low-watermark 2 MB)
  • SHA-256 end-to-end integrity verification — the benchmark fails if a single byte is wrong

Sample output:

════════════════════════════════════════════════════════════
  ts-rtc 500MB DataChannel Throughput Benchmark
  Path: Node.js loopback (127.0.0.1)
════════════════════════════════════════════════════════════
  Benchmark complete
  SHA-256 verification: ✅ passed
  Transfer time:        8.3 s
  Average speed:        60.24 MB/s
  Total wall time:      9.1 s
════════════════════════════════════════════════════════════

Test Suite

Unit tests — Vitest

pnpm test
PackageTest fileKey coverage
webrtcwebrtc.test.ts (604 lines)RTCPeerConnection lifecycle, SDP factory, DTLS role negotiation, full ICE+DTLS+SCTP loopback
iceice.test.ts (555 lines)Candidate priority/foundation math, pair formation, loopback connectivity, restart, tier classification
dtlsdtls.test.ts (738 lines)Record codec, handshake messages, PRF vectors, self-signed cert, AES-GCM, full loopback, both-client deadlock regression
sctpassociation.test.ts (436 lines)Handshake, DCEP, 65536B + 4 MiB transfers, cwnd growth, peerRwnd, flightSize, backpressure, pre-negotiated, ordered/unordered, 3 concurrent channels, TSN wrap-around
srtpsrtp.test.ts (609 lines)RFC 3711 §B.2 keystream vectors, §B.3 key derivation vectors, HMAC-SHA1, ReplayWindow, protect+unprotect, tamper detection, ROC wrap
rtprtp.test.ts (570 lines)RTP encode/decode, CSRC, header extensions, all RTCP types, compound packets, sequence wrap, NTP conversion
sdpsdp.test.ts (827 lines)Chrome offer/answer parsing, round-trip fidelity, all candidate types, fingerprint, directions, SSRC groups, extmap
stunstun.test.ts (569 lines)All attribute types, XOR-MAPPED-ADDRESS (IPv4 + IPv6), MESSAGE-INTEGRITY (correct/wrong/tampered), FINGERPRINT, ICE attributes

Total: ~4,900 lines of unit tests across 9 test files.

BDD acceptance tests — Cucumber.js

pnpm test:bdd

29 scenarios across 5 feature files:

Feature fileScenariosWhat it covers
webrtc/peer-connection.feature14Basic negotiation, bidirectional messaging, binary data, 65 KB fragmentation, 3 concurrent channels, late channel creation, pre-negotiated, unordered, close, signaling state machine, getStats, 4 MiB end-to-end byte-integrity transfer
webrtc/dtls-role-interop.feature7RFC 5763 §5 role negotiation (actpass / active / passive), complementary role assignment, data over negotiated connection, both-client deadlock regression
ice/ice-connectivity.feature2ICE gathering (valid candidates), ICE loopback connectivity
dtls/dtls-handshake.feature2DTLS loopback handshake, matching SRTP keying material, app data exchange
sctp/sctp-channels.feature4SCTP handshake, DCEP open, 65 KiB binary transfer, 4 MiB binary transfer

Reports are written to reports/cucumber-report.html.


Other Commands

pnpm typecheck   # TypeScript strict-mode check across all packages (no emit)
pnpm lint        # ESLint 9 + @typescript-eslint
pnpm clean       # Remove all dist/ directories

TypeScript Configuration

All packages share tsconfig.base.json:

{
  "target": "ES2022",
  "module": "NodeNext",
  "strict": true,
  "exactOptionalPropertyTypes": true,
  "noUncheckedIndexedAccess": true,
  "noImplicitOverride": true
}

exactOptionalPropertyTypes and noUncheckedIndexedAccess are enabled intentionally — they catch protocol-level bugs at compile time that strict mode alone misses.


Monorepo Layout

ts-rtc/
├── packages/          # Protocol stack (each independently publishable)
├── apps/              # Demo and benchmark applications
├── features/          # Cucumber BDD specs + step definitions
├── package.json       # pnpm workspace root
├── pnpm-workspace.yaml
├── tsconfig.base.json # Shared compiler options
└── cucumber.yaml      # BDD runner config

Design Principles

  1. No native dependencies. Everything is implemented in TypeScript using only node:crypto, node:dgram, and node:net. No OpenSSL bindings, no node-gyp, no pre-built binaries.

  2. RFC first. Every algorithm includes inline RFC section references. If behavior diverges from the spec, it is a bug.

  3. Layered, independently testable. ICE, DTLS, SCTP, and SRTP are separate packages that can be tested in isolation. The full WebRTC stack is integration-tested at the @ts-rtc/webrtc layer.

  4. Backpressure everywhere. bufferedAmount and bufferedAmountLowThreshold are plumbed from SCTP congestion control all the way through DCEP to RTCDataChannel, enabling safe high-throughput transfers without unbounded memory growth.

  5. Test vectors over trust. Cryptographic primitives (AES-CM keystream, HMAC-SHA1 key derivation, CRC-32 fingerprint) are verified against the exact vectors published in their respective RFCs.


License

MIT

Reviews

No reviews yet

Sign in to write a review