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:writeon that repository.libconfigfalls back togh auth tokenwhenGH_TOKENis not set in.env, sogh auth loginis sufficient. -
The
mstunnelservice available alongsidemsbridgeto publish the bridge's HTTP endpoint to the public internet (usescloudflaredunder 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-styleadapter.process(req, res, cb)into a Hono request handler. -
buildReactionAdapter/buildTypingAdapter— deliver libbridge's acknowledgement actions through the Bot Framework'scontinueConversationAsync. -
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:
-
Azure Bot messaging endpoint — in the Azure
portal (Settings → Configuration), set the endpoint to
https://<tunnel-domain>/api/messages. -
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 vianormalizeBaseUrl).
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:
- In Teams Admin Center, under Org-wide app settings, allow interaction with custom apps.
- Under Setup policies → Global, enable Upload custom apps.
-
Open Teams → Apps → Manage your apps →
Upload an app →
Upload a custom app → select
kata-agent-bridge.zip. - Add the app to a team or group chat.
Verify
You have reached the outcome of this guide when:
-
A
@Kata Agent hellomention in the configured team or chat is acknowledged with both alikereaction 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.ymlto the configured GitHub repository (visible under the repo's Actions tab). -
When the workflow finishes, the facilitator's
repliesare posted back into the same Teams thread (one message per reply) and thelikereaction is removed. -
data/bridges/discussions.jsonlcontains a JSONL record per conversation, keyed bymsteams:<conversation-id>and written by thebridgeservice whenmsbridgecallsSaveDiscussion.
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.