@bachstudio/teams-card-mcp
v0.6.0
Published
Standalone MCP server: lets an agent send Microsoft Teams messages and Adaptive Cards to a specific user via its own configured bot. Outbound-only, no public endpoint required.
Maintainers
Readme
teams-card-mcp
一个独立的 MCP 服务:agent 绑定它,就能用它自己配置的 Teams bot 给指定用户发消息和卡片。
bach agent ──MCP──> teams-card-mcp ──出站 HTTPS──> Bot Framework ──> Teams 用户
(自带 bot 凭据,不依赖任何其他服务)不需要公网入口、不需要 Power Automate、不需要 Webhook——发消息是纯出站调用。
配置
服务不需要任何环境变量就能启动并列出工具。 缺凭据时工具照常列出, 只有真正调用才会返回一条写明缺什么、怎么补的说明。这样网关永远不会因为配置没送到 而拿到一个"查不出任何工具"的空壳。
要真正能发消息,需要三个值:
| 变量 | 说明 |
|---|---|
| BOT_APP_ID | Microsoft App ID / Bot ID |
| BOT_APP_PASSWORD | Microsoft App Password / Client Secret |
| BOT_TENANT_ID | Microsoft Entra Tenant ID |
在 bach 后台配过「外部机器人 → Microsoft Teams」的话,这三个就是那里的
appId / appPassword / tenantId,直接抄过来(也可以用另一个完全独立的 bot——
本服务不调用 bach 的机器人服务)。
放进 MCP 启动配置(推荐)
{
"mcpServers": {
"teams": {
"command": "npx",
"args": ["-y", "@bachstudio/teams-card-mcp", "--stdio"],
"env": {
"BOT_APP_ID": "...",
"BOT_APP_PASSWORD": "...",
"BOT_TENANT_ID": "..."
}
}
}
}其它放法
也支持 .env 文件(放启动目录自动加载)、ENV_FILE=/path/to/file 指定路径、
systemd 的 EnvironmentFile=、Docker 的 env_file:、K8s 的 env / Secret。
优先级:已存在的环境变量 > .env 文件。
其余可选项见 env.example。
两种传输方式
| 方式 | 怎么启动 | 用在哪 |
|---|---|---|
| http(默认) | npx -y @bachstudio/teams-card-mcp | bach 用 HttpClientTransport 直连 /mcp |
| stdio | npx -y @bachstudio/teams-card-mcp --stdio | supergateway 等 stdio→SSE 网关托管 |
也可以用 MCP_TRANSPORT=stdio 代替 --stdio。
stdio 模式下 stdout 完全属于 MCP 协议,本服务所有日志都写 stderr,不会污染协议流。
supergateway 托管示例
supergateway --stdio "npx -y @bachstudio/[email protected] --stdio" --port 8000环境变量不要写在 stdio 命令串里——那是一段交给 /bin/sh 解析的字符串,容易出问题
(比如变量名里混进一个肉眼看不出来的非 ASCII 字符,shell 就会把整串当命令执行,
报 not found 且 exit 127,网关侧表现成「连上了但查不到工具」)。
改成在容器/Pod 层面注入,子进程自然继承:
# K8s Deployment
env:
- name: BOT_APP_ID
valueFrom: { secretKeyRef: { name: teams-bot, key: appId } }
- name: BOT_APP_PASSWORD
valueFrom: { secretKeyRef: { name: teams-bot, key: appPassword } }
- name: BOT_TENANT_ID
valueFrom: { secretKeyRef: { name: teams-bot, key: tenantId } }跑起来
npm install && npm run build
BOT_APP_ID=xxx BOT_APP_PASSWORD=yyy BOT_TENANT_ID=zzz npm start启动时会自动换一次 token 验证凭据,凭据错了立刻在日志里告警,不用等到第一次发消息。
不接真实 Teams 也能验证整条调用链(内置假的 Entra / Bot Framework / Graph):
npm run smokeMCP 工具
| 工具 | 用途 |
|---|---|
| teams_send_message | 发纯文本私聊消息 |
| teams_send_notice | 发一张只读信息卡片(标题 + 说明 + 键值对),适合构建结果、告警 |
| teams_ask | 发一张带按钮的卡片(审批、确认、二选一) |
| teams_send_form | 发一张带输入框的表单卡片 |
| teams_send_raw_card | 发调用方自己构造的完整 Adaptive Card |
| teams_resolve_user | 邮箱 → Entra 对象 ID,用于确认用户存在 |
收件人怎么填
所有工具的 user 参数由 agent 在调用时传入,服务本身不维护任何用户名单。两种填法:
| 填什么 | 需要什么权限 |
|---|---|
| 企业邮箱([email protected]) | 应用注册需有 Graph User.Read.All 应用权限(管理员同意一次) |
| Entra 对象 ID(aadObjectId) | 不需要额外权限 |
没有 Graph 权限时用邮箱会返回明确报错并提示改用对象 ID,不会静默失败。
调用示例
// 通知
{ "user": "[email protected]", "text": "**构建失败**\n\n分支 dev-master-v6,日志见附件" }
// 信息卡片
{
"user": "[email protected]",
"title": "构建失败",
"description": "流水线 #2841",
"facts": { "项目": "bach", "分支": "dev-master-v6", "耗时": "3m12s" }
}
// 带按钮
{
"user": "[email protected]",
"question": "是否批准这笔报销?",
"facts": { "申请人": "张三", "金额": "5000 元" },
"options": [
{ "id": "approve", "title": "同意", "style": "positive" },
{ "id": "reject", "title": "驳回", "style": "destructive" }
]
}
// 带跳转按钮:点了跳到指定 Teams 应用,并把自定义信息送到目标网页
{
"user": "[email protected]",
"question": "任务已就绪",
"options": [
{
"id": "open",
"title": "在 Bach 里查看",
"style": "positive",
"deep_link": {
"app_id": "8b9a2465-fc14-4e9f-b07b-791c8b0c4fd4",
"entity_id": "bach",
"data": { "conversationId": "abc-123" }
}
}
]
}
deep_link让 MCP 自己拼出https://teams.microsoft.com/l/entity/{app_id}/{entity_id}?context=...深链。data会被序列化后放进 subEntityId,目标网页用 Teams JS SDK 读回:const ctx = await microsoftTeams.app.getContext(); const raw = ctx.page.subPageId; // MCP 送来的 subEntityId const payload = JSON.parse(raw); // 若 data 是对象 // -> { conversationId: "abc-123" }也可以自己手拼 URL 走
url字段(跳任意 https 网址)。url和deep_link不能同时填。
前提条件
- 目标用户必须已安装这个 Teams 应用(个人范围)。 bot 只能给装过应用的用户发私聊, 没装会返回 403,服务会把这个原因原样说出来。
- bot 的单租户 / 多租户类型要和
BOT_TENANT_TYPE一致,否则 token 拿得到但发送时 401。
接入 bach
bach 走远程 HTTP MCP(Super.Agent.Core 用 McpClient + HttpClientTransport 连 plugin.api_url),
在 bach 后台注册成 MCP 插件即可:
| 配置项 | 填什么 |
|---|---|
| api_url | http(s)://你的服务地址/mcp |
| 认证头 | x-api-key: <你的 MCP_API_KEY>(没配 MCP_API_KEY 就不用带) |
| name_space | 例如 teams |
注意:bach 注册工具时会执行 f.Name.Replace("-",""),所以本服务所有工具名都用下划线,已规避。
当前边界
这一版只做发送。 用户点击卡片按钮后,Teams 会往你的 bot 消息端点发一条
text 为空、value 带着按钮 data 的 message 活动——本服务不接收这个回传,
需要由那个端点侧处理。卡片按钮的 data.__action 就是你在 options[].id 里填的值,
端点侧凭它判断用户点了哪个。
要让 agent 能同步拿到点击结果(teams_ask 发出去后直接等到答案),
需要本服务额外提供 /api/messages 端点并接管 bot 的消息路由——那是下一步。
项目结构
src/
loadEnv.ts 启动前加载 .env / ENV_FILE(必须先于 config 求值)
config.ts 环境变量读取与启动校验
logger.ts 分级日志
cards.ts Adaptive Card 构造
botFramework.ts token / 建会话 / 投递 activity,含 token+会话+用户 ID 三级缓存
tools.ts 6 个 MCP 工具
mcp.ts McpServer 工厂(http / stdio 共用)
server.ts Express:/mcp、/healthz、鉴权
index.ts 按 transport 分支启动、凭据自检、优雅退出
test/
smoke.ts HTTP 传输端到端冒烟(内置假微软云)
stdio.ts stdio 传输冒烟(复现 supergateway 托管方式)