Bridge Microsoft Teams to the Agent Team

Engineers discuss work in Microsoft Teams. The Kata agent team listens on GitHub. Without a bridge, every interaction means context-switching: open a new tab, file an issue, hand-craft a workflow_dispatch, paste the verdict back into Teams when it's done. The msbridge service closes that gap. A user mentions the bot in a Teams thread, the bridge dispatches the channel-agnostic kata-dispatch.yml workflow with the conversation history, and posts the lead's reply back into the same thread when the workflow finishes.

This guide walks through the operational steps to stand up msbridge for a target GitHub repository: provisioning the Azure Bot resource, configuring the service, running it behind a tunnel, packaging the Teams app, and verifying the round trip end-to-end.

For the library primitives msbridge is built on, see Bridge a Threaded Channel to the Agent Team.

Prerequisites

  • A Microsoft 365 developer tenant with an Azure Bot resource registered for the Teams channel. The Teams channel must be enabled on the bot (Settings → Channels → add Microsoft Teams).
  • The Kata Agent Team workflows installed in a GitHub repository.
  • A GitHub token with actions:write on that repository. libconfig falls back to gh auth token when GH_TOKEN is not set in .env, so gh auth login is sufficient.
  • The mstunnel service available alongside msbridge to publish the bridge's HTTP endpoint to the public internet (uses cloudflared under the hood).

Architecture overview

msbridge runs alongside the mstunnel sidecar and connects three ends — the Teams channel via the Bot Framework, the GitHub Actions workflow via workflow_dispatch, and the same Teams thread for the reply:

Teams thread ──webhook── mstunnel ── msbridge ──dispatch──> kata-dispatch
     ▲                                  │
     └────────── callback ──────────────┘

The service is built on @forwardimpact/libbridge. The dispatch dance, callback handler, callback registry, rate limiter, history bound, prompt builder, lenient payload validator, and the acknowledgement lifecycle (reaction + randomized typing-verb ticker) all come from the library. Durable thread state lives in the shared services/bridge gRPC service, which msbridge reaches through a BridgeClient. Per-user GitHub auth (used to mint the dispatch token) lives in services/ghauth, reached through a GhauthClient. msbridge owns three Bot Framework adapters in src/teams.js:

  • botFrameworkIntake — converts Bot Framework's express-style adapter.process(req, res, cb) into a Hono request handler.
  • buildReactionAdapter / buildTypingAdapter — deliver libbridge's acknowledgement actions through the Bot Framework's continueConversationAsync.
  • sendReply — posts a reply message to the conversation reference saved on the discussion context.

Configure credentials

Set the credentials and service parameters in .env. All are loaded via createServiceConfig("msbridge"):

Env var Purpose
MICROSOFT_APP_ID Azure Bot app ID
MICROSOFT_APP_PASSWORD Azure Bot app password / secret
MICROSOFT_APP_TENANT_ID Azure AD tenant ID
SERVICE_MSBRIDGE_GITHUB_REPO owner/repo target for workflow dispatch
SERVICE_MSBRIDGE_CALLBACK_BASE_URL Public URL the workflow POSTs callbacks back to

Discussion context is persisted by the shared services/bridge gRPC service at data/bridges/discussions.jsonl. msbridge calls bridge through a BridgeClient channel — no per-bridge storage configuration is needed. services/ghauth similarly persists per-user GitHub link state under data/ghauth/ and is reached through a GhauthClient. Add both bridge and ghauth to config/config.json under init.services ahead of msbridge so they start first.

Start the bridge

Add mstunnel and msbridge to config/config.json under init.services, in that order, so restarting the bridge does not cycle the tunnel (declaration order determines restart scope).

Start both services:

bunx fit-rc start

The tunnel publishes a fresh trycloudflare.com hostname on every restart. Read it from the tunnel log:

grep trycloudflare.com data/logs/mstunnel/current

Configure two endpoints with that hostname:

  1. Azure Bot messaging endpoint — in the Azure portal (Settings → Configuration), set the endpoint to https://<tunnel-domain>/api/messages.
  2. Bridge callback URL — set SERVICE_MSBRIDGE_CALLBACK_BASE_URL=https://<tunnel-domain> in .env (no trailing path; the bridge composes /api/callback/<token> itself and strips any trailing slashes via normalizeBaseUrl).

Pick up the callback URL change without recycling the tunnel:

bunx fit-rc restart msbridge

The tunnel hostname survives bridge restarts because mstunnel is a separate service in config/config.json and fit-rc restart msbridge only restarts services listed after the tunnel.

Package and sideload the Teams app

Build the manifest archive:

just msbridge-package

The recipe reads MICROSOFT_APP_ID and the tunnel domain from .env via libconfig and produces dist/kata-agent-bridge.zip (git-ignored). Override the tunnel domain with --tunnel-domain=<host> when needed. The manifest uses Teams schema v1.17; the package can be rebuilt and re-uploaded without removing the app from Teams because Azure Bot routing depends on the messaging endpoint, not the manifest contents.

Sideload through Teams Admin Center:

  1. In Teams Admin Center, under Org-wide app settings, allow interaction with custom apps.
  2. Under Setup policies → Global, enable Upload custom apps.
  3. Open Teams → Apps → Manage your apps → Upload an appUpload a custom app → select kata-agent-bridge.zip.
  4. Add the app to a team or group chat.

Verify

You have reached the outcome of this guide when:

  • A @Kata Agent hello mention in the configured team or chat is acknowledged with both a like reaction on the user's message and a randomized typing verb posted into the thread ("Moonwalking...", "Unravelling...", "Tempering...", "Crafting...", "Simmering...", "Percolating...", "Decoding...") refreshed every ~25 seconds.
  • The bridge dispatches kata-dispatch.yml to the configured GitHub repository (visible under the repo's Actions tab).
  • When the workflow finishes, the facilitator's replies are posted back into the same Teams thread (one message per reply) and the like reaction is removed.
  • data/bridges/discussions.jsonl contains a JSONL record per conversation, keyed by msteams:<conversation-id> and written by the bridge service when msbridge calls SaveDiscussion.

If the workflow dispatch fails, the bridge posts Failed to reach the agent team. Please try again later. into the thread; confirm the GitHub token has actions:write on the target repository and check the bridge log for api.github.com errors. If you are on a corporate VPN with tenant restrictions, outbound calls to Azure AD or GitHub may be blocked; disconnect or allowlist the relevant endpoints.

What's next