Skip to main content
Glama

MWE审批MCP

MWE审批MCP 是部署在 https://dingtalk.mwexk.com/mcp 的自托管钉钉 OA 审批 MCP Server。

当前版本:0.14.0

当前架构

WorkBuddy / Codex
  -> 本服务 OAuth 2.1 + PKCE
  -> 钉钉 OAuth 验证真实企业用户
  -> 本服务签发限 audience/scope 的短期 MCP token
  -> Streamable HTTP /mcp
  -> MWE审批MCP 企业内部应用 access token
  -> 钉钉 OA OpenAPI

钉钉 bpms_task_change
  -> 官方 Stream 长连接(仅上游事件摄取)
  -> 本地待审批/已处理索引
  • 只提供自托管 Streamable HTTP;不提供 stdio。

  • 已删除 AIHub 版本,不使用 mcp-gw.dingtalk.com

  • 不提供或兼容 /platform/tools/* 旧路由。

  • 钉钉 userAccessToken 只用于登录身份验证,不作为 MCP Bearer、不持久化。

  • OA OpenAPI 仍使用 MWE审批MCP 企业内部应用的 App ID/App Secret。

  • MCP token 中的 corpId + unionId + userId 按请求绑定审批调用者,模型输入不能覆盖身份。

  • 钉钉 Stream 是服务端使用应用凭证建立的出站事件通道,不是 MCP 传输,不新增公网工具端点。

Related MCP server: DingTalk MCP Server V2

公共工具

正常 tools/list 只有三个按业务角色/主对象聚合的工具:

approval_inbox    # 当前审批人:批量发现待审批或已处理任务
approval_task     # 审批参与者:查看、同意、拒绝、评论
approval_request  # 申请人:准备附件、提交、评论、撤销

三个工具按业务角色和生命周期划分:approval_task 负责审批参与者对已知实例的查看、同意、拒绝和评论;approval_request 负责申请人准备/创建审批,并允许对本人发起的实例评论或撤销;approval_inbox 只读批量发现待审批/已处理记录并把 processInstanceId 交给 approval_task。评论是同一个内部动作,仅角色 scope 与实例关系校验不同;未实现的退回能力不会被公开宣称。

发现当前 OAuth 用户的待审批(recordStatus 省略时默认为 pendinglimit=1 为单条,最多 20 条):

{
  "recordStatus": "pending",
  "page": 1,
  "limit": 20
}

发现当前 OAuth 用户已经处理的审批任务:

{
  "recordStatus": "completed",
  "page": 1,
  "limit": 20
}

事件索引覆盖不足时,可显式调用普通 OA 实例 ID 列表接口做最近 1–30 天的有限刷新;服务端只扫描 APPROVAL_INBOX_PROCESS_CODES 中的精确模板,单次最多检查 40 个候选实例,并逐实例验证当前 OAuth 用户的任务:

{
  "recordStatus": "completed",
  "refreshWindowDays": 7,
  "page": 1,
  "limit": 20
}

若响应中的 refresh.truncated=true,继续把同一响应的 refresh.nextCursor 原样传回,并保持 recordStatusrefreshWindowDays 不变;循环到 truncated=false。cursor 由服务端 HMAC 认证,最多有效 24 小时,绑定当前用户、原时间窗口、状态和模板集合;篡改会失败关闭,且不包含用户、实例或模板明文:

{
  "recordStatus": "completed",
  "refreshWindowDays": 7,
  "refreshCursor": "上一次响应的 refresh.nextCursor",
  "page": 1,
  "limit": 20
}

approval_inboxbpms_task_change 事件索引取候选项,然后逐项调用审批详情确认任务属于登录用户并与请求状态一致,才返回唯一 processInstanceId 和可用的 taskId。同一实例的多个任务事件合并成一条;taskCount/verifiedTaskCount 保留完整计数,taskIds 最多返回 20 个并用 taskIdsTruncated=true 声明截断,decisionResults 仅从当前详情仍有效的任务重建。若源事件缺少任务 ID,则仅返回实例 ID,并标记 taskIdUnavailable=true。刷新统计中的 indexedRecordCount 按唯一实例计数,indexedTaskCount 按写入的任务证据计数。候选详情失败时不会越过该页,而是返回可重试 cursor。completed 保存最近 30 天的 finish 事件并返回 decisionResult=agree|refuse|redirect,取消事件不会被当作已审批。普通 OA 没有免费的全量回填 API,因此两类响应都固定声明 coverage=partialresyncRequired=true:它们只覆盖事件连接激活及保留窗口内的任务;若 5000 条容量边界截断更早记录,还会返回 capacityTruncated=true 并推进 coverageSince。该工具不冒充钉钉官方 DWS 的全历史收件箱。

刷新适配器同时接受钉钉详情中的毫秒时间戳和 ISO-8601 时间字符串;createdAt/completedAt/updatedAt 使用真实业务时间,不以本次扫描时刻替代。

读取审批:

{
  "action": "view",
  "processInstanceId": "审批实例ID"
}

换取选定附件的临时下载链接:

{
  "action": "view",
  "processInstanceId": "审批实例ID",
  "attachmentAction": "download",
  "attachmentIds": ["详情中的fileId"],
  "maxAttachments": 3
}

服务端不下载、解析或 OCR 附件。Agent 客户端必须立即下载临时链接,并自行执行大小限制、重定向 Host 校验、文件识别、解析和 OCR。

同意审批:

{
  "action": "approve",
  "processInstanceId": "审批实例ID",
  "taskId": "view返回的当前任务ID",
  "requestId": "每次业务决定稳定复用的UUID",
  "confirm": true,
  "remark": "符合要求"
}

拒绝时把 action 改为 reject,并提供非空 remark。写操作会重新读取当前任务,确认任务仍属于 OAuth 登录用户,并使用持久幂等账本阻止重复决定。

发起审批

approval_request 使用“默认拒绝 + 精确允许列表”。首版只接受:

  • expense_reimbursement:费用报销,固定 processCode=PROC-2DB91B79-3CDD-421D-A223-0489A7BAB2C0

  • payment_request:付款申请,固定 processCode=PROC-5E238117-7121-4CB3-8219-9F11A2E42BE4

加班审批及所有其他模板均拒绝。公开 Schema 不接受 processCode、申请人、审批人、抄送人或流程节点;申请人来自 OAuth 绑定的钉钉用户,审批流完全沿用 OA 后台模板。提交前服务端调用官方 forecast 接口,按实时模板解析必需的发起人自选节点,并仅把 forecast 返回的 actorKey/userId 注入 targetSelectActioners;客户端不能选择或覆盖。必填自选节点无法解析时,在附件 commit 前失败关闭。

先用 dryRun 验证付款申请:

{
  "action": "submit",
  "template": "payment_request",
  "fields": {
    "documentNumber": "FK-20260817-001",
    "payee": "收款单位",
    "currency": "CNY",
    "applicationDate": "2026-08-17",
    "lines": [{
      "purpose": "项目采购",
      "amount": 880,
      "reason": "合同付款",
      "expenseDepartment": "研发部"
    }]
  },
  "confirm": false,
  "dryRun": true,
  "requestId": "稳定复用的UUID"
}

deptId 为可选部门提示,不是客户端提供的身份或权限依据。服务端始终查询 OAuth 申请人的实时通讯录:账号只属于一个部门时自动使用该部门,并规范化客户端遗留的根部门 1;账号属于多个部门且无法唯一确定时返回 DEPARTMENT_SELECTION_REQUIRED 及安全的部门 ID/名称候选,Agent 再用候选中的 deptId 重试。

评论审批实例

approval_task action=comment 可由当前 OAuth 用户对其实际参与的审批实例添加评论;approval_request action=comment 允许申请人对本人发起的精确允许列表实例补充说明或附件。两者调用相同内部动作。纯文本评论直接使用 phase=submit;附件评论先用 phase=prepare 获取审批钉盘直传信息,由 Agent 客户端完成 PUT,再用同一工具的 phase=submit 提交匹配的 fileName/fileSize/uploadKey/spaceId。服务端重新校验审批空间、commit 文件并要求钉钉完整返回 fileId/fileName/fileSize/fileType/spaceId 后才构造评论附件。评论正文为 1–1024 字符,附件最多 20 个、单文件最大 20 MiB、合计不超过 50 MiB;commentUserId 始终由服务端注入。真实写入要求 confirm=true 和稳定 UUID requestId

{
  "action": "comment",
  "phase": "prepare",
  "processInstanceId": "审批实例ID",
  "text": "请查看补充材料",
  "attachments": [{ "fileName": "ROI说明.pdf", "fileSize": 4096 }],
  "confirm": true
}

Agent 完成返回地址的直传后提交评论:

{
  "action": "comment",
  "phase": "submit",
  "processInstanceId": "审批实例ID",
  "text": "请查看补充材料",
  "uploads": [{
    "fileName": "ROI说明.pdf",
    "fileSize": 4096,
    "uploadKey": "prepare返回值",
    "spaceId": "prepare返回值"
  }],
  "requestId": "稳定复用的UUID",
  "confirm": true
}

发起人补充材料时,把示例中的工具名从 approval_task 改为 approval_request,参数完全相同。审批写入仅依据 OAuth scope 与实时业务归属校验:approval_task 决策要求 approval:decide 且任务属于当前用户,approval_request 写操作要求 approval:create 且申请人身份由服务端绑定;不再维护部署级用户白名单。

钉钉当前公开的官方 OA 服务端 API 没有“保存到钉钉草稿箱”接口,发起审批接口也没有草稿标志。preparesubmit + dryRun=true 会读取实时模板、校验完整表单并构建最终请求,但不会创建审批实例,也不会在钉钉客户端草稿箱生成条目;项目不会用本地记录冒充钉钉草稿。

费用报销字段为 datereasoncounterparty 和至少一条 items;每条明细包含 amountcategory(仅 AI费用其它)、expenseDepartmentremark

附件采用两阶段直传:

  1. Agent 调用 action=prepare,传文件名、大小和模板允许的附件字段。

  2. MCP 返回钉钉签名的 HTTPS PUT 地址和请求头;Agent 直接把文件上传到钉钉,文件字节不经过 MCP。

  3. Agent 调用 action=submit,提交 uploadKeyspaceId、文件名和大小;MCP 提交文件元数据并发起审批。

  4. 带附件的最终创建请求由服务端注入与审批钉盘空间一致的 microappAgentId,并将附件表单值标记为 componentType=DDAttachment;客户端不能覆盖这两个字段。

单文件最大 20 MiB、每单最多 10 个、合计最大 50 MiB。费用报销仅允许附件字段 invoice/other,付款申请仅允许 attachment。实际提交和撤销必须 confirm=true 且提供稳定 UUID requestId;幂等命名空间绑定 OAuth 申请人。附件提交、审批创建或撤销结果不确定时会失败关闭,禁止自动换 UUID 重试。

IDEMPOTENCY_OUTCOME_UNKNOWN 会返回安全诊断字段:failureStage、已 commit/总附件数、当前附件序号、原始安全错误码、HTTP 状态、上游错误码和 upstreamRequestId。这些诊断会与 uncertain 幂等项一起持久化,同 UUID 回读结果一致;账本回读只重建这组允许字段,不保存表单、文件名、uploadKey、用户 ID 或临时 URL。失败阶段仅限 attachment_contextattachment_commitform_buildapproval_create。若审批已返回实例 ID,仅随后的幂等账本写入失败,服务保留成功结果并标记 idempotencyPersistence=failed,同时尽力持久化最小实例恢复记录,不再吞掉实例 ID。

OAuth 端点

端点

用途

/.well-known/oauth-protected-resource/mcp

MCP Protected Resource Metadata

/.well-known/oauth-authorization-server

本站 Authorization Server Metadata

/authorize

MCP 客户端授权入口

/oauth/dingtalk/callback

钉钉 OAuth 回调

/token

授权码或 refresh token 换 MCP token

/register

受限公共客户端动态注册

/revoke

撤销 refresh token family

/mcp

OAuth 保护的 Streamable HTTP MCP

/healthz

存活检查

Access token 默认 60 分钟;refresh token 默认 7 天、每次使用都在 Store 内原子轮换,并从本次刷新重新计算 7 天窗口。服务端仅保留当前 token 与最近一代重放墓碑:重放最近一代会撤销当前 family,更早 token 直接按无效凭证拒绝,因此持续刷新时每个 family 状态保持 O(1)。授权事务、客户端注册和 refresh 哈希保存在 MCP_AUTH_STORE_PATH,原始 token 不落盘;成功签发 token 后动态客户端注册会滑动续期,停止使用 30 天后自动清理。升级到本版本时,仍未过期的旧 8 小时 refresh token 会在启动阶段一次性迁移到新窗口,已过期 token 不复活。

动态客户端注册采用服务端幂等复用:经过校验且语义相同的公共客户端元数据(包括回调地址、客户端名称和协议能力)返回已有 client_id,并优先复用仍绑定有效 refresh family 的旧注册;不同元数据保持隔离。/register 重复请求本身不会延长注册寿命,只有成功签发或刷新 token 才滑动续期。既有重复记录不批量删除,停止使用后按 30 天 TTL 自然清理。安全审计只记录“新建/复用”结果,不记录 client ID、元数据、授权 URL 或凭据。

服务端在 OAuth scope 不变时更新,不要求用户重新登录钉钉:客户端应沿用现有 refresh token 静默换取 access token,重新执行 initializetools/list。Codex 的 URL-only 配置使用 startup_timeout_sec = 30codex mcp list/get 只读配置,不会为已启动任务热回填缺失工具,Codex Desktop 需在 MCP servers 设置执行 Restart 或重启应用后创建新任务。initialize.serverInfo.version/healthzversion/toolsRevision 以及 /mcpx-mcp-server-version/x-mcp-tools-revision 可用于检测工具版本;MCP 响应要求重新验证缓存。升级到 0.14.0 仅需用原持久卷与原签名密钥重建服务端,不变更 issuer、audience、scope 或 /app/data/auth,已授权客户端无需重新授权。只有新增 scope、refresh token 超过 7 天滚动窗口、用户主动撤销或检测到 token 重放时,才需要交互式重新授权。服务端的 DCR 幂等复用可减少客户端注册漂移,但不能修复客户端本地凭据丢失或工具注册表缓存故障,也不能强制不支持刷新机制的客户端主动清除其本地工具缓存。

开发者后台

MWE审批MCP 企业内部应用中配置精确 OAuth 回调:

https://dingtalk.mwexk.com/oauth/dingtalk/callback

并确认应用具备:

  • 登录用户身份/个人信息权限。

  • 根据 unionId 映射企业 userId 的通讯录权限 qyapi_get_member

  • 审批实例读写和审批表单读取权限。

  • 附件直传所需的 Storage.UploadInfo.ReadStorage.File.Write 权限。

  • H5 微应用能力及其 AgentId;将正整数配置为 DINGTALK_AGENT_ID。未配置时,无附件审批仍可使用,附件 prepare 会明确失败。

  • 事件订阅的推送方式选择 Stream模式推送,并订阅“审批任务开始、结束、取消/转交”(bpms_task_change)。生产配置 DINGTALK_APPROVAL_EVENTS_ENABLED=true,索引保存到 APPROVAL_INBOX_PATH

Premium.Workflow.ReadWrite.All 及 OA 高级版待审批/已处理列表不是生产依赖。Todo.Todo.Read 只在 2026-08-17 做过可行性探测:当前企业的未完成和已完成企业待办都返回 0,不能用它冒充 OA 收件箱;服务代码不调用该接口。

附件上传 URL 必须经过 HTTPS 主机白名单校验。2026-08-17 的企业实测返回 sh-dualstack.trans.dingtalk.com,因此默认精确允许 .trans.dingtalk.com;同时保留 钉钉可能返回的 .aliyuncs.com 存储域。不要把白名单放宽为任意 .dingtalk.com

OAuth 授权范围包含 approval:readapproval:decideapproval:create。未认证请求的 HTTP 401 challenge 只声明 resource_metadata,由客户端从 metadata 的 scopes_supported 选择授权范围;服务端不再用 scope=approval:read 覆盖 metadata。本站 /authorize 只校验并保存 MCP 授权事务,然后立即跳转到钉钉官方 OAuth 页面,不展示自建权限确认页。客户端以较小 scope 连接后,调用发起审批或审批决定动作时,服务端仍会按 MCP Authorization 规范返回 HTTP 403 insufficient_scope challenge;不要把缺 scope 仅包装成 HTTP 200 的工具业务错误。所有实际写操作仍要求工具层 confirm=true

无效、过期、签名错误或已被替换的 MCP access token 统一返回 HTTP 401 invalid_token 及 canonical resource_metadata challenge,不得因底层 JWT 异常返回 500。这样客户端可以进入标准 refresh/login 恢复链路,而不会把 MCP 标记为服务端故障。

本地验证

cd "D:\codex项目\金蝶领星钉钉三端数据同步开发\approval-mcp"
npm ci
npm test
npm run typecheck
npm run build

生成 Ed25519 PKCS#8 签名私钥:

mkdir -p secrets
openssl genpkey -algorithm Ed25519 -out secrets/mcp-signing-private.pem
openssl rand -base64 32 > secrets/mcp-audit-hmac.key
chmod 600 secrets/mcp-signing-private.pem secrets/mcp-audit-hmac.key

复制 .env.example 配置真实环境变量。服务不会自动读取 .env;Compose、systemd 或密钥管理器必须显式注入。

启动:

npm run build
node .\dist\transports\http.js

客户端

WorkBuddy 与 Codex 的无密钥 OAuth 配置模板和测试顺序见:

客户端配置中只出现公开 MCP URL,不填写 App Secret、Bearer token 或钉钉 userAccessToken。

部署

  • 使用 compose.example.yaml 部署应用。

  • MCP 签名私钥和独立审计 HMAC key 均以只读 Secret 文件挂载。

  • deploy/cvm/edge/dingtalk.conf 只转发 /mcp、OAuth/metadata 端点和 /healthz

  • 当前 CVM 入口以 /public/cvm-web-edge/README.md 为准:应用只发布一个唯一的 loopback 后端端口,由 edge-nginx 转发;不得绑定 0.0.0.0

  • 127.0.0.1:3000 已登记给唯一的正式 DingTalk 服务。后续并行灰度须通过 APPROVAL_HOST_PORT 选择并登记另一个经 ss -lntp 验证为空闲的临时端口;临时实例不得继续占用正式端口。

  • 容器 Router 与共享外部 Docker 网络是后续全站入口迁移目标,不在本次 MCP 鉴权升级中单点切换。

  • OAuth/审批状态、事件驱动的待审批及最近 30 天已处理索引、幂等账本和最多 30 天审计日志位于持久卷 /app/data

  • 同一应用 Client ID 同时只能运行一个生产 Stream 消费者。灰度容器不得在旧生产容器仍运行时开启 DINGTALK_APPROVAL_EVENTS_ENABLED

详细安全与模块设计见:

Related MCP Connectors

Related MCP Servers

  • F
    license
    Not graded
    quality
    C
    maintenance
    A Model Control Protocol server that provides access to DingDing (Chinese workplace collaboration platform) API features, including retrieving access tokens, department lists, user information, and searching users by name.
    2
    -
  • A
    license
    Not graded
    quality
    D
    maintenance
    A Model Control Protocol server for integrating with DingTalk, enabling users to send messages, retrieve conversation/user information, and query calendar events through Claude.
    6
    MIT
  • F
    license
    C
    quality
    D
    maintenance
    MCP server that enables AI agents to control all DingTalk features (messaging, calendar, tasks, approvals, etc.) via natural language using the DingTalk Workspace CLI.
    83
    1
    -
  • A
    license
    Not graded
    quality
    C
    maintenance
    MCP server for Feishu/Lark API integration, enabling AI agents to send messages, manage groups, create and edit documents and spreadsheets, and search knowledge bases.
    MIT