npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@komaa/openclaw-msteams-bridge

v0.4.3

Published

Put your OpenClaw agent in a real Microsoft Teams call: it sees your screen-share and camera, holds a live spoken conversation, and appears as a lip-synced avatar. Realtime speech-to-speech or streaming STT-to-TTS, meeting recaps, outbound call-backs, HMA

Readme

Microsoft Teams Bridge for OpenClaw

CI npm version downloads docs MIT License TypeScript PRs Welcome

Put your OpenClaw agent on a real Microsoft Teams call. It turns an ordinary Teams call into a true two-way video conversation: the agent sees what you show it, talks back in real time, and appears in the call as an animated, lip-synced avatar.

It is one plugin, depending only on the published openclaw plugin-sdk + api.runtime. No fork, no vendored runtime, no trusted-plugin privileges required.

Three pillars

| Pillar | What it does in the call | |:--|:--| | Perceptionthe agent sees you | Reads camera and screen-share (VBSS) frames: look_at_screen on demand, a keyframe per turn in streaming mode, an ambient view in realtime mode. Frames are attributed per participant and vision spend is capped per call. | | Dialoguethe agent talks with you | Holds a spoken conversation - realtime speech-to-speech or a streaming STT to agent to TTS pipeline. Barge-in, verbal interrupts, a "speak only when addressed" gate, DTMF entry, English and Arabic, and greeting attendees by name. | | Renderingthe agent is seen by you | Appears as a lip-synced avatar tile. Emits expression cues, viseme lip-sync, and show_to_caller overlays, which the hosted StandIn bridge draws into the video the caller sees. |

Details of each capability follow below.

Capabilities

  • Two dialogue modes - realtime speech-to-speech (OpenAI or Azure OpenAI), or a streaming STT to agent to TTS pipeline that works with any provider.
  • Vision - reads camera and screen-share frames, keeps a continuous ambient view, retains a retroactive keyframe history, and stays inside a per-call budget cap.
  • Group and meeting etiquette - stays silent until addressed by a wake phrase, then answers through a short follow-up window; 1:1 calls always answer; every frame and utterance is attributed to the right speaker.
  • Outbound call-backs - hand over a long task and the agent rings you back and speaks the result when it is done, or answer a Teams chat with a phone call. An unanswered call is finalized after a no-answer timeout.
  • Meeting recap and minutes - end-of-call summary of key points, decisions, and action items, plus an on-demand .docx of minutes with per-person attribution.
  • Avatar driver cues - expression changes, viseme lip-sync, and picture-in-picture image sharing, rendered by the StandIn bridge.
  • Chat governance - an "Ask about this" message action, voice-message transcription, an audit-log mirror, and outbound DLP redaction.
  • Secure transport - a replay-proof HMAC handshake, a caller allowlist that is closed by default, and a recording-status gate that holds media until recording is active.

Getting started

There are two ways to connect this plugin to Teams. Pick one - they differ in who owns the Teams bot.

StandIn Managed Bot (recommended)

StandIn provides the Teams bot. You install StandIn from the Teams Store, connect this agent in the StandIn portal, and paste one secret here. No Azure bot registration, no App ID, no client secret, no endpoint configuration.

That one secret covers both lanes of the connection: calls arrive on the calling WebSocket and Teams messages on the messages endpoint. They are two lanes of a single StandIn binding, not two products, which is why there is a single value to paste and no enable flag to remember.

This is the whole configuration. It is what runs on a working install, with the tuning knobs left out - every other setting has a default that is already correct.

{
  "plugins": {
    // `allow` is what lets OpenClaw load the plugin at all; `entries` configures it.
    "allow": ["msteams-bridge"],
    "entries": {
      "msteams-bridge": {
        // NOTE: `enabled` sits beside `config`, not inside it.
        "enabled": true,
        "config": {
          // The connection secret from the StandIn portal. One value, BOTH lanes:
          // calling (ws://:9442/msteams/calling) and messages (http://:9444/msteams/messages).
          "secret": "paste-the-value-from-the-StandIn-portal",

          // REQUIRED, and the most common reason a first call fails. Without a realtime
          // provider the plugin still starts and still connects, then rejects every inbound
          // call with "realtime-unavailable" - it logs the reason once at startup and is
          // otherwise silent. `mode` is inferred: a resolved realtime provider means
          // "realtime", none means "streaming".
          "realtime": {
            "provider": "openai",
            "providers": {
              "openai": {
                "apiKey": "sk-..."
              }
            }
          }
        }
      }
    }
  }
}

Using Azure OpenAI instead? Realtime is served from a different host than your chat deployments - <resource>.cognitiveservices.azure.com, not <resource>.openai.azure.com, which returns 404 on the websocket handshake - and on its own api-version. Replace the openai provider block with:

"openai": {
  "apiKey": "your-azure-openai-key",
  "azureEndpoint": "https://<resource>.cognitiveservices.azure.com",
  "azureDeployment": "gpt-realtime",
  "azureApiVersion": "2025-04-01-preview",
  "voice": "cedar"
}

Azure needs no whisper or tts-1 deployment for this: gpt-realtime is speech-to-speech, so it is the only deployment involved.

Check it before you call. Restart the gateway after any config change, then:

openclaw plugins list          # msteams-bridge should be "enabled" and loaded

The startup log should show both lanes listening:

[msteams-bridge] MsteamsMediaStream listening host=127.0.0.1 port=9442 path=/msteams/calling
[msteams-bridge] msteams managed chat: listening on 127.0.0.1:9444/msteams/messages
[msteams-bridge] started (mode=realtime)

If mode=streaming appears when you expected realtime, the provider did not resolve - check the apiKey.

Then expose both endpoints to the internet (a public host, or a tunnel) and register them on your connection in the StandIn portal as the Agent calling URL and Agent messages URL.

Bind address: both lanes default to loopback (127.0.0.1), matching the documented posture of a tunnel that terminates TLS publicly and proxies in. The chat listener used to default to all interfaces while calling defaulted to loopback, so a config that named no bind address at all put the messages port on your LAN and the docs described the two as sharing one. If StandIn reaches this host directly, set bindAddress to that interface (or 0.0.0.0 behind a firewall) - the HMAC keeps unauthenticated callers out, but an open port is still an open port. messagesBindAddress overrides the messages lane alone.

One agent instance serves one StandIn connection: the chat secret is a single value, scoped to one tenant binding. Serving several tenants means running several instances, each with its own secret. Never share one secret across tenants.

Bring your own Azure bot (advanced)

You own the Microsoft Entra app, the client secret, and the Azure Bot resource. Choose this when you need the bot to live entirely inside your own tenant. This plugin then adds voice and video (CVI) on top of OpenClaw's Microsoft Teams chat channel, so set those up first:

  1. Install OpenClaw using the official docs at docs.openclaw.ai.

  2. Add Microsoft Teams as a channel (bot app + credentials) following the OpenClaw Teams channel docs.

  3. Use the StandIn sandbox (standin.komaa.com/sandbox, free, no Teams bot needed), the hosted media bridge that joins the call and connects to this plugin. Add your own Teams bot later at standin.komaa.com for inbound calls.

  4. Add this plugin. The one-line installer detects your OpenClaw install and walks you through the config (mode, shared secret, provider key), then prints the next steps:

    curl -fsSL https://standin.komaa.com/install.sh | bash

    Prefer to do it by hand? See Install and Configuration below.

Requirements

  • An OpenClaw install (host >= 2026.6.10).
  • StandIn to bridge the call: start free in the sandbox (no Teams bot), or add your own bot at standin.komaa.com for inbound calls. It is the hosted media bridge that joins the Teams call and connects to this plugin's WebSocket.
  • For realtime mode: a realtime voice provider + key (OpenAI or Azure OpenAI). For streaming mode: your OpenClaw-configured STT / TTS / agent (no realtime key needed).

Install

Do these in order.

Step 2 is BRING-YOUR-OWN-BOT only. On the StandIn Managed Bot path there is no Teams channel to create and no bot credentials to hold - StandIn owns the bot. If you followed the Managed Bot quickstart above, skip straight from step 1 to step 3 and set secret.

  1. OpenClaw is installed and running (host >= 2026.6.10).

  2. (BYO only) Microsoft Teams is added as a channel (bot app + credentials).

  3. Install this plugin, then restart the gateway so it loads:

    openclaw plugins install npm:@komaa/openclaw-msteams-bridge
    openclaw gateway restart
  4. Configure it under plugins.entries."msteams-bridge".config. The minimum that actually WORKS - a secret alone leaves inboundPolicy unset, which denies every inbound call, so the bridge connects and then answers nothing:

    "msteams-bridge": {
      "config": {
        "enabled": true,
        "secret": "PASTE_THE_STANDIN_CONNECTION_SECRET",   // covers calling AND messages
        "bindAddress": "127.0.0.1",
        "inboundPolicy": "open"                            // or "allowlist" + allowFrom: [...]
      }
    }

    Set secret (the value the StandIn portal shows you). You also need your provider key. See Configuration and Security below. Nothing starts until a secret is set somewhere.

  5. Connect StandIn to the plugin's WebSocket (start in the sandbox) and place a test call.

Prefer a guided setup? The one-line installer detects your OpenClaw install and walks you through steps 3 and 4 (mode, shared secret, provider key), applying the secure defaults for you:

curl -fsSL https://standin.komaa.com/install.sh | bash

Also on ClawHub: openclaw plugins install clawhub:@komaa/msteams-bridge (OpenClaw falls back to npm automatically if the ClawHub fetch fails). The package ships prebuilt (v0.1.10+): no build step either way.

Two modes

| | realtime | streaming | |---|---|---| | How it talks | speech-to-speech model | your OpenClaw STT to agent to TTS | | Needs a realtime key | yes | no | | Latency | lowest | higher (per turn) | | Vision | continuous push | attached per turn |

Set mode to "realtime" or "streaming". If omitted, the runtime auto-selects realtime when a realtime provider resolves, else streaming.

Configuration

Config lives under plugins.entries."msteams-bridge".config. secret must match the value set in your StandIn dashboard.

bindAddress defaults to loopback for both lanes, because the documented posture is a tunnel that terminates TLS publicly and proxies to 127.0.0.1 - nothing is exposed on your LAN. Widen it to 0.0.0.0 only when StandIn reaches this host directly, and only on a trusted interface.

You do not need OpenClaw's voice-call plugin. That one is telephony (Twilio, fromNumber, webhook URLs); this one is self-contained. If your config nests Teams settings inside it as plugins.entries."voice-call".config.msteams, lift that block up to plugins.entries."msteams-bridge".config and drop the voice-call entry.

Two models, two places

The voice model is configured here (realtime.providers.<provider>.model) and runs the spoken conversation. The agent model - used by openclaw_agent_consult and openclaw_agent_task for lookups and background work - is inherited from your OpenClaw config (agents.defaults.model.primary). There is nothing to set here for it.

FailoverError: Unknown model: openai/gpt-5.5 logged mid-call, while the voice side works fine? On v0.2.x and earlier the agent model fell back to OpenClaw's compiled-in default pair rather than your configured agent, so a host on any other provider failed every consult - and only after the caller had already been greeted, since the voice lane has its own provider block. Nothing in your config names gpt-5.5, which is what makes it hard to find. Fixed in v0.3.1.

responseModel is an optional override for when the voice lane should consult a different model than your default agent. Order: responseModelagents.defaults.model.primary → OpenClaw's built-in default (only with no agent configured at all).

Realtime (OpenAI)

{
  "plugins": {
    "entries": {
      "msteams-bridge": {
        "config": {
          "enabled": true,
          "mode": "realtime",
          // Loopback + a tunnel; 0.0.0.0 only when StandIn reaches this host directly.
          "bindAddress": "127.0.0.1",
          "callingPort": 9442,
          "messagesPort": 9444,
          "path": "/msteams/calling",
          // ONE connection secret from the StandIn portal, covering calling AND messages.
          "secret": "<the connection secret from StandIn>",
          "requireRecordingStatus": true,
          "inboundPolicy": "allowlist",
          "allowFrom": ["<caller AAD object id>"],
          "realtime": {
            "provider": "openai",
            "providers": {
              "openai": { "apiKey": "<key>", "model": "gpt-realtime" }
            }
          }
        }
      }
    }
  }
}

Realtime (Azure OpenAI)

Azure is the openai provider plus azureEndpoint and azureDeployment:

"realtime": {
  "provider": "openai",
  "providers": {
    "openai": {
      "apiKey": "<azure-key>",
      "azureEndpoint": "https://<resource>.cognitiveservices.azure.com",
      "azureDeployment": "gpt-realtime"
    }
  }
}

Streaming (no realtime key)

"mode": "streaming",
"stt": {
  "provider": "<your-stt-provider>",
  "providers": { "<your-stt-provider>": { "apiKey": "<key>" } }
}

In streaming mode the TTS and agent come from your OpenClaw configuration. STT uses stt.provider if set, else your configured transcription provider, else a VAD-segmented file fallback. Group-call gating, DTMF, and vision all work in streaming mode too.

Outbound call-backs (optional)

"outbound": {
  "enabled": true,
  "workerBaseUrl": "https://<your-standin-endpoint>",
  "tenantId": "<aad-tenant-id>",
  "answerTimeoutMs": 120000,
  "defaultMode": "notify"
}

This one block turns on two things:

  • Call me back when done. openclaw_agent_task takes deliverVia. The default "message" sends the result as a Teams chat message; "call" rings the caller back and speaks it once the work finishes.
  • Chat-to-call. The call_me_with_the_answer tool answers a Teams chat with a phone call. It rings the person it is chatting with - always, and only. There is no target parameter, deliberately: the agent reads untrusted text all day, and a tool that took a user id would turn any of it into "ring this person". Needs the messages lane as well.

Both are offered ONLY when enabled, workerBaseUrl, tenantId and secret are all set. Miss any one and the plugin does not expose the capability at all - the background task quietly delivers by message and the chat tool refuses and says why. An agent that promises "I'll call you back" and then cannot is worse than one that answers in the chat.

Your Azure bot also needs Calls.Initiate.All, admin-consented, on top of the join/media permissions. Without it every attempt fails at Graph no matter how this is configured.

Needs v0.4.0+. Earlier versions accepted deliverVia: "call" and told the caller "I'll call you back", then never placed a call - the delivery was routed to a tool that does not exist in OpenClaw. Nothing errored, so there was no way to tell misconfiguration from breakage.

StandIn Managed Bot: what differs from BYO

On a managed connection StandIn owns the Teams bot, so this plugin has no customer Bot Framework credentials. Everything that would normally be sent "as your bot" goes through StandIn's signed gateway instead, and two behaviours follow from that:

  • Meeting recap, minutes and background-task results are delivered as TEXT. The gateway hop carries text and cards, not files, so the Word document is not attached; the message says so. BYO deployments post through your own bot and still get the .docx.
  • post_chat_message needs a meeting call. A 1:1 call has no Teams conversation of its own, so there is nothing to post into and the tool is not offered for those calls.

Call outcomes. StandIn POSTs the real terminal state of a call you placed to {calling path}/outcome/{callId}, signed with the same HMAC recipe as the WebSocket upgrade. The route is served whenever the calling lane is - nothing to configure - and it is what makes a declined or busy call finalize immediately instead of waiting out outbound.answerTimeoutMs. See Outbound calls.

Security

The plugin ships with secure defaults, and the recommendation is simple: keep them. Each option below states its safe default and why it is safe. You only relax a default when your deployment genuinely needs it, and only in the narrow way described.

| Option | Safe default | Why it is safe | When to change it | |:--|:--|:--|:--| | secret | none (fails closed) | Both lanes authenticate every connection with a replay-proof HMAC handshake keyed on this secret. With no secret the server refuses to start, so a misconfig can never expose an unauthenticated port. A non-string value coerces to empty and also fails closed. | Always set it, to a strong random value that matches your StandIn dashboard. Prefer an OpenClaw secret reference over a literal in config. | | inboundPolicy | unset = deny all | Inbound calls are rejected until you name a policy, so the agent never answers an unknown caller by default. | Set allowlist and list trusted callers in allowFrom (by AAD object id or phone number). Reserve open for throwaway sandbox testing only. | | requireRecordingStatus | true | Media is held until Teams reports recording is active, so the agent never sees or hears the call before participants have the recording indicator. This keeps you on the right side of Teams' notice expectations. | Leave it on. Only disable for a controlled test where no real participants are present. | | bindAddress | 127.0.0.1 (loopback) | The WebSocket listens on localhost only, so it is unreachable from other hosts by default. | Widen to 0.0.0.0 only when the StandIn bridge runs on a different host, and only on a trusted or VPN-only interface behind your firewall. The HMAC handshake still guards it, but do not expose the port to the open internet. | | realtime.toolPolicy | none | The voice model cannot invoke any agent tools, so a caller cannot drive tools by voice unless you opt in. | Use safe-read-only to allow read-only tools. Reserve owner (full tool access) for calls you have restricted to trusted owners via inboundPolicy. | | Installer | applies the above | The one-line installer configures these secure defaults for you rather than leaving them blank. | If your policy forbids piping a script to a shell, download and read install.sh first, then run it, or follow the manual Install steps. |

In short: set a strong secret, keep inboundPolicy restrictive with an explicit allowFrom, leave requireRecordingStatus on, keep bindAddress as tight as your topology allows, and only raise toolPolicy for callers you trust.

Key reference

Full reference in the Configuration Reference. Common keys:

Core

| Key | Description | |---|---| | enabled | master on/off | | mode | realtime or streaming (auto if omitted) | | port | WebSocket port (default 9442) | | bindAddress | bind address; use 0.0.0.0 for the hosted bridge | | path | Calling WebSocket path (default /msteams/calling) | | secret | HMAC secret for BOTH lanes; must match StandIn | | requireRecordingStatus | engage only once recording is active | | inboundPolicy | disabled, allowlist, pairing, open. pairing currently behaves exactly like allowlist (the plugin issues no pairing codes or approvals for calls; callers must be in allowFrom) | | allowFrom | allowlisted caller ids | | inboundGreeting | opening line | | sessionScope | per-phone, per-call, per-thread | | maxConcurrentCalls | concurrent-call cap | | maxDurationSeconds | max answered-call duration | | maxVisionPerMinute | paid vision calls per call per minute (default 30), shared by look_at_screen, the ambient push and the streaming per-turn attach. 0 switches vision spend OFF - it is the kill switch, not "unlimited". There is no unlimited setting; set a large number for a cap that never bites | | ambientVision | keep pushing the newest changed camera / screen-share frame at the agent between turns, so it stays visually aware without being asked. Off by default - it spends a vision call per scene change. look_at_screen works either way | | meetingRecap | post end-of-call minutes | | bilingual | Arabic / English |

Group call

| Key | Description | |---|---| | groupCall.requireAddress | answer only when addressed | | groupCall.wakePhrases | wake words | | groupCall.followUpWindowMs | follow-up window (ms) |

Realtime

| Key | Description | |---|---| | realtime.provider | openai | | realtime.providers.openai.apiKey | provider key (secret) | | realtime.providers.openai.model | e.g. gpt-realtime | | responseModel | optional - overrides the agent model for consult/task; omit to inherit agents.defaults.model.primary | | outbound.enabled + workerBaseUrl + tenantId | enables call-back delivery (deliverVia: "call") and chat-to-call (call_me_with_the_answer); also needs Calls.Initiate.All on the bot | | realtime.providers.openai.azureEndpoint | Azure OpenAI endpoint | | realtime.providers.openai.azureDeployment | Azure deployment name | | realtime.instructions | system instructions | | realtime.toolPolicy | safe-read-only, owner, none | | realtime.suppressInputDuringPlayback | echo guard (both modes) | | realtime.echoSuppressionWindowMs | echo guard window (ms) | | realtime.echoBargeInRms | barge-in RMS threshold |

Streaming

| Key | Description | |---|---| | stt.provider | transcription provider | | stt.providers.<id>.apiKey | STT key (secret) |

Outbound

| Key | Description | |---|---| | outbound.enabled | enable call-backs | | outbound.workerBaseUrl | StandIn outbound API URL | | outbound.tenantId | your AAD tenant id | | outbound.answerTimeoutMs | no-answer timeout | | outbound.defaultMode | notify or conversation |

Links