Open-source FastAPI bridge between MeetStream and OpenAI Realtime via the OpenAI Agents SDK (openai-agents[voice]). It implements the two WebSocket endpoints and session lifecycle described in the official guide: Bridge Server Architecture & Session Management.
Use this repo as a starting point: run the server on a public URL (or tunnel), point MeetStream at your /bridge and /bridge/audio URLs, and supply your own OpenAI and MeetStream credentials where each service expects them.
| Step | Action |
|---|---|
| 1 | uv sync |
| 2 | Copy .env.example → .env and set OPENAI_API_KEY |
| 3 | Start the server |
uv run uvicorn app.server:app --host 0.0.0.0 --port 8000Check http://localhost:8000/health.
The MeetStream API expects public wss:// URLs, not localhost. With the bridge still on port 8000, start a tunnel in another terminal—for example ngrok:
ngrok http 8000Use the HTTPS hostname ngrok prints, swap the scheme to wss://, and append the paths:
| MeetStream field | Example URL |
|---|---|
socket_connection_url → control |
wss://abc123.ngrok-free.app/bridge |
live_audio_required → audio |
wss://abc123.ngrok-free.app/bridge/audio |
Same host for both; only the path changes.
| File | Role |
|---|---|
app/server.py |
Builds the FastAPI app, static files, .env load |
app/routes/websockets.py |
Route map: /bridge, /bridge/audio, optional debug UI |
You rarely edit server.py unless you add middleware or new HTTP routes.
4. What to open first (by goal)
| Goal | Open first |
|---|---|
| Change how the AI talks, tools, or MCP | app/agent.py |
| Change MeetStream audio decoding / speaker filtering | app/meetstream/audio.py, app/meetstream/config.py |
| Change what we send back to the meeting (audio/chat/interrupt JSON) | app/meetstream/outbound.py |
| Wire meeting audio ↔ model ↔ outbound commands | app/realtime/pipeline.py (RealtimeMeetingBridge) |
| See raw WebSocket receive loops | app/meetstream/ws_handlers.py |
5. Reading order for different levels
- New to the repo: this README → Repository layout (below) → skim
app/realtime/pipeline.py(the “glue” class). - Comfortable with FastAPI:
app/routes/→app/meetstream/→app/realtime/pipeline.py. - MCP setup (Canva, JSON config): docs/mcp.md → then README.agents.md for advanced agent/provider topics.
The layout is intentional: MeetStream wire format lives under app/meetstream/, OpenAI Realtime + pump under app/realtime/, persona and tools in app/agent.py. That matches the official docs split (audio streaming vs control commands) plus a single orchestrator in pipeline.py.
| Endpoint | Direction | Purpose |
|---|---|---|
GET /health |
— | Liveness check |
GET / |
— | Optional demo UI (app/static/) |
WebSocket /bridge (or /{bot_id}/bridge) |
MeetStream ↔ you | JSON control: handshake, usermsg, interrupt; outbound sendaudio, sendmsg, etc. |
WebSocket /bridge/audio (or /{bot_id}/bridge/audio) |
MeetStream → you | Binary frames (current protocol) or legacy JSON PCMChunk |
The bridge keeps one OpenAI Realtime session per bot_id. The session is created when the first channel connects and is torn down when both WebSockets have disconnected, matching the docs.
| Path | Role |
|---|---|
app/server.py |
FastAPI app factory, static mount, .env load |
app/routes/ |
HTTP (/, /health) and WebSocket route declarations |
app/meetstream/ |
MeetStream wire format: binary audio decode, resampling, speaker filter, outbound JSON commands, /bridge receive loops |
app/realtime/ |
OpenAI Realtime session lifecycle, MCP preconnect, event serialization, model → MeetStream pump (RealtimeMeetingBridge) |
app/agent.py |
RealtimeAgent, tools, MCP server list (LLM-facing only) |
app/static/ |
Optional browser UI for local debugging |
mcp.config.json |
MCP servers (default: Canva); expand with more mcpServers entries |
docs/mcp.md |
Reference for mcp.config.json transports and examples |
- Python 3.12+
- uv (recommended)
- OpenAI API key (realtime / voice stack)
- MeetStream API key (when you create bots/sessions via the MeetStream API—not stored in this app unless you add that yourself)
git clone <this-repo>
cd meetstream-agent
cp .env.example .env
# Edit .env: set OPENAI_API_KEY
uv sync
uv run uvicorn app.server:app --host 0.0.0.0 --port 8000Or:
uv run python main.pyLoad .env from the project root automatically (via python-dotenv in the app lifespan).
When you create a MeetStream session (HTTP API), point both WebSockets at your deployed bridge (use wss:// in production):
{
"meeting_url": "https://meet.google.com/your-meeting-id",
"bot_name": "My Assistant",
"live_audio_required": {
"websocket_url": "wss://your-host.example.com/bridge/audio"
},
"socket_connection_url": {
"websocket_url": "wss://your-host.example.com/bridge"
}
}The same bot_id is sent on both channels in the ready handshake so this server can pair them.
If your MeetStream API uses URL templates with {bot_id} in the path, use the same host and paths wss://…/{bot_id}/bridge and wss://…/{bot_id}/bridge/audio — this app registers those routes as well as /bridge and /bridge/audio.
| Variable | Required | Description |
|---|---|---|
OPENAI_API_KEY |
Yes | OpenAI API key for Realtime / Agents |
PORT |
No | HTTP port (default 8000) |
MEETSTREAM_BOT_NAME |
No | Display name of your bot in the meeting; audio from this speaker is ignored (echo control) |
MEETSTREAM_IGNORE_SPEAKERS |
No | Comma-separated display names to ignore |
AGENT_SPEAKER_KEYWORDS |
No | Comma-separated substrings (default bot,agent,assistant,ai) to filter self/agent audio |
MEETSTREAM_IN_RATE / MEETSTREAM_OUT_RATE |
No | PCM rates for MeetStream I/O (default 48000) |
MODEL_AUDIO_RATE |
No | Rate your model expects for input/output PCM in the bridge (default 24000 for OpenAI Realtime) |
MeetStream API keys belong in whatever client or backend calls MeetStream to create sessions—not in this bridge unless you add that integration.
See .env.example for a template.
The repo ships mcp.config.json with a Canva entry (npx mcp-remote → Canva’s MCP). Install Node.js so npx is on your PATH.
- Disable Canva: remove the
canvablock or rename the config file (see docs/mcp.md). - More MCPs in JSON: add entries under
mcpServers— docs/mcp.md. - Different file:
MCP_CONFIG=/path/to/file.jsonin.env.
Use this when your MCP runs in Docker (or elsewhere) and is reachable at an HTTPS URL (often via ngrok), with a Bearer token.
-
In
.env(never commit secrets):DOCKER_MCP_ENABLED=1 DOCKER_MCP_URL=https://YOUR-NGROK-HOST/mcp DOCKER_MCP_BEARER_TOKEN=YOUR_TOKEN
-
Set
DOCKER_MCP_ENABLED=0(or unset) to turn this MCP off. Canva frommcp.config.jsoncan stay on at the same time. -
Details and timeouts: docs/mcp.md.
Agent tuning, Framer/n8n via env only, multi-provider notes: README.agents.md
- Audio in: Binary frames per Live Audio Capture & Frame Decoding, or legacy JSON
PCMChunkwith base64 audio. - Control in: After
{"type":"ready","bot_id":"..."}, MeetStream may sendusermsgandinterrupt. - Control out: The bridge sends
command: "ack"after binding,sendaudiofor PCM back to the meeting,sendmsgfor chat (includes bothmessageandmsg), andinterruptwithaction: "clear_audio_queue"when the model stops audio or starts a tool (so queued TTS does not overlap the next utterance after the tool). Outbound model audio is batched (default ~240 ms at the model sample rate, then resampled to 48 kHz) so MeetStream gets fewer, larger chunks than raw Realtime deltas—tune withMEETSTREAM_OUT_AUDIO_CHUNK_MS(40–2000) if playback glitches or latency feels off. OptionalMEETSTREAM_SENDAUDIO_PACE_MSadds a short pause between consecutivesendaudioframes if the client struggles with burst traffic.
uv run uvicorn app.server:app --host 0.0.0.0 --port 8000 --reloadOptional: install SciPy for higher-quality resampling (scipy.signal.resample_poly); otherwise NumPy linear interpolation is used.
Use and modify per your open-source license (add a LICENSE file if this repo should ship under a specific license).