关于 Microsoft Entra SCIM
Microsoft Entra SCIM 是面向 Agent 的 MCP 服务,将 Microsoft Entra SCIM 2.0 API 的用户与组生命周期操作封装为工具,并提供本地模拟服务,便于无租户测试。
主要功能
- 01发现 SCIM 能力、资源类型和架构
- 02创建、读取、更新及停用用户
- 03管理用户生命周期属性和安全属性
- 04创建、更新、删除组及管理成员
- 05本地模拟 API 与请求预演
适用场景
- 01自动化 Entra 用户入职与离职
- 02维护用户组及其成员关系
- 03在本地模拟环境验证工具调用
MCP 相关
作为 MCP 服务,它向客户端暴露 Microsoft Entra SCIM 用户和组生命周期工具。另附本地 SCIM 模拟服务,可在不连接 Entra 租户时测试这些工具。
配置
本地服务的配置在各客户端中通用,复制后粘贴到客户端的 MCP 配置文件即可。
npx -y entra-scim-mcpENTRA_TENANT_IDDirectory (tenant) GUID. Required to authenticate against a live tenant; not needed for ENTRA_SCIM_DRY_RUN or ENTRA_SCIM_STATIC_TOKEN, which supply their own placeholder.ENTRA_CLIENT_IDApp registration (client) GUID. Required to authenticate against a live tenant; not needed for ENTRA_SCIM_DRY_RUN or ENTRA_SCIM_STATIC_TOKEN.ENTRA_CLIENT_SECRET密钥Client secret value. For a live tenant set exactly one of ENTRA_CLIENT_SECRET or ENTRA_CLIENT_CERT_PATH; a certificate is preferred for anything long-lived.ENTRA_CLIENT_CERT_PATHPath to a PEM holding the certificate and its private key. Set exactly one of ENTRA_CLIENT_SECRET or ENTRA_CLIENT_CERT_PATH.ENTRA_CLIENT_CERT_PASSWORD密钥Password for the PEM, if it is encrypted.ENTRA_SCIM_BASE_URLOverride the SCIM base URL (default https://graph.microsoft.com/rp/scim). Point it at the bundled mock to try the tools without a tenant.ENTRA_SCIM_STATIC_TOKEN密钥Use a fixed bearer token instead of Azure AD. Refuses any microsoft.com/microsoft.us host and requires ENTRA_SCIM_BASE_URL; intended for the local mock only.ENTRA_SCIM_DRY_RUNSet to 1 to run every client-side validation and return the request that would have been sent, without sending it or acquiring a token.本地服务需要在你的电脑上安装运行环境(如 Node.js 18+)。各客户端的配置文件位置不同,请参考客户端文档。
工具
本地服务需要在你的电脑上运行后才能读取工具,维护者暂未提供工具列表。
详细介绍
entra-scim-mcp
用于 Microsoft Entra SCIM 2.0 Provisioning API(2026 年 4 月正式发布)的 Model Context Protocol 服务。它将针对 https://graph.microsoft.com/rp/scim 的用户和组生命周期操作作为 MCP 工具提供给 Claude 等 Agent。
可以用它做什么
- 发现租户的 SCIM 功能(
get_service_provider_config、list_resource_types、list_schemas) - 配置、读取、更新和取消配置用户,包括 Custom Security Attributes 和生命周期属性
- 创建、更新和删除组,并管理成员关系;自动遵守 API 严格的 PATCH 规则
先决条件
在此服务能够连接到你的租户之前,请按照Microsoft 文档完成一次性设置:
- Entra ID P1(或包含 P1 的任意 SKU)以及用于关联计费的 Azure 订阅。
- 在 ID Governance → Dashboard 中启用 SCIM Provisioning API,并关联计费资源组。
- 注册应用,并授予所需的 Microsoft Graph application 权限:
User.ReadWrite.All、Group.ReadWrite.All(核心生命周期操作)CustomSecAttributeAssignment.ReadWrite.All、CustomSecAttributeDefinition.Read.All(CSA 工具)User-LifeCycleInfo.ReadWrite.All(生命周期工具)User-Mail.ReadWrite.All、User-Phone.ReadWrite.All、User.EnableDisableAccount.All(最小权限替代方案)- 2026 年 9 月新增的更精细权限;如果只使用部分工具:
User.ReadBasic.All(只读;仅支持对id和userName进行筛选)、User.Create(仅创建)、User.ReadUpdate.All(仅更新,不支持创建或删除)、Group.Create(仅创建)、GroupMember.ReadWrite.All(仅用于add_group_members/remove_group_member;update_group的属性变更仍需要Group.ReadWrite.All) 授予管理员同意。
- 创建客户端密钥或上传 PEM 客户端证书(二选一)。
每次 SCIM API 调用都会计费;此服务不会进行超出 API 要求的批处理。
无需 Entra 租户即可试用
此软件包附带 Entra SCIM API 的本地模拟器(entra-scim-mock-server),可在无需 Azure 设置且不会产生 API 费用的情况下使用所有工具:
# shell 1 — start the mock (seeds a small demo tenant)
npx -y --package entra-scim-mcp entra-scim-mock-server
然后将 MCP 服务指向它:
{
"mcpServers": {
"entra-scim-mock": {
"command": "npx",
"args": ["-y", "entra-scim-mcp"],
"env": {
"ENTRA_SCIM_BASE_URL": "http://127.0.0.1:8990",
"ENTRA_SCIM_STATIC_TOKEN": "dev-token"
}
}
}
}
模拟器标志:--port、--token、--seed <file.json>、--no-seed、--capture <file.jsonl>(记录每个请求/响应)、--validator-compat(符合 RFC 标准的行为,适用于 Microsoft SCIM Validator;参见 docs/scim-validator.md)。
安装 / 运行
此服务是 stdio MCP 服务,专为由 MCP 客户端(Claude Desktop、Claude Code 等)启动而设计。
npx -y entra-scim-mcp
必需的环境变量:
| 变量 | 必需 | 说明 |
|---|---|---|
ENTRA_TENANT_ID | 是 | 目录(租户)GUID |
ENTRA_CLIENT_ID | 是 | 应用注册(客户端)GUID |
ENTRA_CLIENT_SECRET | 二选一 | 客户端密钥值(开发环境) |
ENTRA_CLIENT_CERT_PATH | 二选一 | 包含证书和私钥的 PEM 文件路径 |
ENTRA_CLIENT_CERT_PASSWORD | 可选 | PEM 已加密时使用的密码 |
ENTRA_CLIENT_SECRET 和 ENTRA_CLIENT_CERT_PATH 中必须且只能设置一个。
开发 / 测试环境变量
| 变量 | 描述 |
|---|---|
ENTRA_SCIM_BASE_URL | 覆盖 SCIM 基础 URL(默认值为 https://graph.microsoft.com/rp/scim)。将其指向本地模拟服务。 |
ENTRA_SCIM_STATIC_TOKEN | 使用固定的 bearer 令牌,而非 Azure AD。**防护措施:**需要设置 ENTRA_SCIM_BASE_URL,拒绝任何 *.microsoft.com / *.microsoft.us 主机,对非环回主机发出警告,且不能与真实凭据同时使用。在此模式下无需租户 / 客户端 ID。 |
ENTRA_SCIM_DRY_RUN | 设置为 1:工具运行所有客户端验证,然后返回本应发送的精确请求,而不实际发送。不会获取令牌 — 无需配置任何凭据即可使用。 |
Dry-run 结果会以成功的负载形式返回:
{
"dryRun": true,
"request": {
"method": "DELETE",
"url": "https://graph.microsoft.com/rp/scim/users/u-1",
"headers": {}
}
}
(DELETE 不携带 Accept 标头 — API 会拒绝特定的 JSON 媒体类型。其他所有方法都会发送 Accept: application/json。)
多请求工具(例如 add_group_members 在 ID 超过 20 个时)在 dry-run 中只会显示第一个分块请求。
Claude Desktop 配置
~/Library/Application Support/Claude/claude_desktop_config.json(macOS)或 %APPDATA%\Claude\claude_desktop_config.json(Windows):
{
"mcpServers": {
"entra-scim": {
"command": "npx",
"args": ["-y", "entra-scim-mcp"],
"env": {
"ENTRA_TENANT_ID": "00000000-0000-0000-0000-000000000000",
"ENTRA_CLIENT_ID": "11111111-1111-1111-1111-111111111111",
"ENTRA_CLIENT_SECRET": "..."
}
}
}
}
在生产环境中,将密钥替换为证书:
{
"env": {
"ENTRA_TENANT_ID": "...",
"ENTRA_CLIENT_ID": "...",
"ENTRA_CLIENT_CERT_PATH": "/secure/path/entra-scim-mcp.pem"
}
}
工具
类型列是工具的 MCP 注释,客户端会读取这些注释,以决定调用是否需要获得你的批准:读取对应 readOnlyHint,添加是只创建内容的写入操作(destructiveHint: false),覆盖是会丢弃或替换状态且无法通过此 API 撤销的写入操作(destructiveHint: true)。
| 工具 | 类型 | 用途 |
|---|---|---|
get_service_provider_config | 读取 | 一次性发现功能。每个 API 版本的配置都是静态的——获取一次后重复使用。 |
list_resource_types | 读取 | 枚举 SCIM 资源类型(User、Group)。 |
list_schemas | 读取 | 枚举 SCIM 架构和 Entra 扩展,并列出每个属性的类型、可变性和默认返回值。构建补丁前,请先在此处检查补丁路径。 |
list_users | 读取 | 列出用户;支持 API 的受限筛选器(eq/ew、仅 and,以及围绕 ew 的 not())和游标分页,每页最多 999 条。还可用于将 userName 解析为其他所有用户工具需要的对象 ID,以及列出群组成员(groups.value eq "<groupId>")。 |
get_user | 读取 | 按 ID 读取单个用户,可选投影属性。永远不会包含 CSA 或群组成员关系。 |
provision_user | 添加 | 创建用户,并强制要求属性集(userName、password、displayName、name.givenName、name.familyName)。mailNickname 为可选项;省略时,Entra 会根据 userName 生成该值。 |
update_user | 覆盖 | PATCH 用户;禁止对 mailNickname 执行 remove,并强制地址路径使用 [type eq "work"]。 |
deprovision_user | 覆盖 | DELETE 用户。软删除后保留 30 天,只能通过 Graph 还原;同时会移除该用户的所有群组成员关系。 |
update_user_lifecycle | 覆盖 | 设置生命周期属性(例如 employeeLeaveDateTime),Lifecycle Workflows 可据此触发工作流。需要 User-LifeCycleInfo.ReadWrite.All。 |
get_user_custom_security_attributes | 读取 | 读取用户的 CSA,并按属性集投影。attributeSets 为必填项——API 会拒绝不带属性集的扩展 URN,而且普通的 get_user 永远不会返回 CSA。 |
update_user_custom_security_attributes | 覆盖 | PATCH 用户的 CSA。remove 或将空数组用于 replace 都会删除一项赋值。 |
list_groups | 读取 | 使用 API 的受限筛选器集列出群组。members.value 筛选器是读取成员关系的唯一方式。 |
get_group | 读取 | 读取单个群组(不会返回成员——请使用按 groups.value 筛选的 list_users 列出成员)。 |
create_group | 添加 | POST 一个群组。通过 Entra 扩展设置 mailEnabled、securityEnabled、mailNickname、description。displayName 不唯一。 |
update_group | 覆盖 | 仅 PATCH 群组属性(此处会拒绝成员操作;类型标志在创建时固定)。 |
add_group_members | 添加 | 向群组添加 ≥1 个用户——每个 PATCH 自动分批处理 20 个 ID(API 上限),每个 PATCH 包含一个 Operation。幂等。根据 RFC 7644,每个 PATCH 都是原子操作,但多批次序列并非如此:如果后续批次失败,此服务(而非 API)会抛出 AddGroupMembersPartialFailure,并列出 addedMemberIds / failedMemberIds / notAttemptedMemberIds,因此部分写入绝不会悄无声息地发生。 |
remove_group_member | 覆盖 | 从群组中移除单个用户(API 仅允许每个 PATCH 移除一个成员,且不得包含其他操作)。此处的 404 通常表示不是成员,而不是群组不存在。 |
delete_group | 覆盖 | DELETE 一个群组。统一群组可通过 Graph 恢复,期限为 30 天;安全组则无法恢复。 |
告诉 Agent 的内容及其所在位置
模型不会读取此文件,因此它所需的一切都必须通过协议传递。有三个位置承载这些内容,这样划分是有意为之的:
- 服务端说明在握手时发送一次,包含适用于所有工具的信息:使用 ID 而非名称、成员关系只能单向读取、普通读取看不到 CSA、受限的筛选语法,以及每次调用都是计费的 Graph 请求。把这些内容重复写进 18 条描述,耗费的上下文远多于带来的收益。
- 工具描述包含单个工具特有的信息,包括值得提前说明的失败情况——用户不是成员时,404 会指出组名;mailNickname 一旦设置便无法移除;第二次调用
provision_user会产生冲突,而不是合并。 - 输入描述包含各参数的语法。三个 PATCH 工具各自都有一个
path示例,因为 path 语法是这里唯一无法靠猜测得出的内容:API 只接受 RFC 7644 的一个受限子集,而且普通属性和 CSA 的子集有所不同。
设置 ENTRA_SCIM_DRY_RUN=1 后,说明中会增加一段相应内容,因为试运行结果的格式与成功结果相同,否则就会被报告为已生效的更改。
test/tool-metadata.test.ts 会强制执行以上所有要求:每个工具都必须带有注解,只读和写入工具集按名称固定,每个输入都必须有描述,而且 PATCH 工具必须显示一个 URN。默认看起来是只读的新工具会导致测试套件失败。
此服务为你强制执行的规则
Entra SCIM API 有一些容易忽略的限制。工具层会在发送请求之前拒绝无效输入:
- 筛选器允许列表:每种资源只允许使用文档规定的属性和运算符;不接受
or;not()仅可用于包裹userName或mailNickname上的ew子句;externalId不能与其他子句组合;布尔属性(active、securityEnabled、mailEnabled)必须是true/false,且发送时不加引号。这是必要要求,而非格式问题:在实际环境中,securityEnabled eq "true"会返回 400。 - 页面大小:
list_users会将count上限设为 999。超过 100 的值只有在attributes投影中排除 enterprisemanager时才会生效;否则 API 会悄然只返回 100。 - 查询字符串:
=两侧不能有空格(API 遇到任何此类情况都会返回 400)。 - 用户 PATCH:禁止通过
remove移除mailNickname;addresses 路径筛选器必须严格为[type eq "work"]。 - 组 PATCH:成员关系操作通过专用工具执行,以确保遵守添加成员数上限 20 人以及每次只能移除一人的规则。
- 幂等的成员添加:API 会将重复添加视为成功;
add_group_members会对输入去重。
错误会以包含 status、scimType 和 detail 的结构化载荷返回给 Agent。
值得了解的 API 行为
以下是实际 API 的行为,而文档对此要么表述含糊,要么只字未提。每项行为都是通过真实租户或 Microsoft SCIM Validator 测试发现的,并且都已由你使用的工具处理——之所以列出,是因为它们会影响你对响应的理解。
DELETE 请求不得携带 Accept 标头。 API 会返回 400 Accept header application/json is invalid。已在实际环境中测试全部四种情况:无标头 → 204,*/* → 204,application/json → 400,application/scim+json → 400。只有 DELETE 与此规则相反——其他每种方法都必须携带 JSON Accept,省略则会按文档规定返回 400。首次实际运行之前,这个问题一直导致 deprovision_user 和 delete_group 无法正常工作。
普通读取永远不会返回自定义安全属性(CSA)。 在 /Schemas 中,属性集级别的属性为 returned: "request",因此无论你要求什么,get_user 都不会包含 CSA。只有明确指定它们时才会出现,而且投影是按属性集粒度进行的:urn:...:CustomSecurityAttributes:<Set>。单独的扩展 URN 会被直接拒绝(400 ... not supported in the "attributes" or "excludedAttributes" query parameter),因此 attributeSets 是必填输入,而非可选输入。
CSA 值有类型限制,且会强制执行类型检查。 使用匹配的 JSON 类型发送时,Boolean、Integer、String 和多值 String 都能完整往返,一次 PATCH 也可以同时携带多个属性。以下两种删除行为均未被 Microsoft 记录:
- 对 CSA 路径使用
op: "remove"会清除该项赋值,并保留其他赋值。 - 对多值属性使用
replace并将值设为[]会移除该项赋值——之后读取时会完全省略该属性,而不是返回空数组。
创建时必须提供 password,但永远无法读取。 它的属性为 writeOnly / returned: never,响应中绝不会回传该值。创建时完整的必填字段集为 userName、password、displayName、name.givenName 和 name.familyName,严格程度远高于 RFC 7643(其中只有 userName 是必填项)。
创建时 mailNickname 是可选的,设置后则永久保留。 自 2026 年 8 月起,若省略、设为 null 或空值,Entra 会根据 userName 推导该值:取第一个 @ 之前的全部内容;如果没有 @,则取整个值([email protected] 会得到 abc123)。无论 /Schemas 怎么说,该别名并不唯一:它将该属性标记为 uniqueness: server,但 2026-09-30 的实时探测创建了第二个别名明确重复的用户,并获得了 201。因此,[email protected] 和 [email protected] 最终都可以是 a。模拟服务也与此一致。如果别名很重要,请明确传入 mailNickname。创建后,无法通过 PATCH 将其移除。资源类型现在将两个用户扩展都报告为 required: false,但在 2026-09-30,实时 /Schemas 仍将该属性本身报告为 required: true。创建行为才是可靠的依据:同一探测已实时确认,在省略 mailNickname 和将其设为空值两种情况下都会进行推导。使用 npx tsx scripts/probe-mailnickname.ts --confirm 重新运行(约 14 次计费调用)。
群组 displayName 不唯一。 Entra 接受重复的群组名称并返回 201。面向 RFC 的工具通常会在此处假定返回 409,因此不要依赖创建失败来检测现有群组——请先筛选。
移除群组成员针对的是成员关系,而不是用户。 对 members[value eq "<id>"] 执行 remove,若没有匹配项,无论该 ID 属于谁,都会返回 404——一个只是不在该群组中的有效用户,与一个从未是用户的 GUID 一样,都会被拒绝。该行为是在真实租户上探测的,且始终保留了一个额外成员作为参照,因此结果并非清空群组所致:
| 情况 | 结果 |
|---|---|
| 真实成员 | 204 |
| 从未是成员的有效用户 | 404 |
| 从未是用户的格式正确的 GUID | 404 |
| 用户已先被删除的成员 | 404 |
有两点值得注意。删除用户会移除其成员关系,因此先删除用户再移除成员关系,结果与其他任何非成员情况相同——通过在删除前后使用 members.value 筛选条件查询 list_groups 已确认这一点。错误消息还会指出群组,而不是成员(Resource '<groupId>' does not exist or one of its queried reference-property objects are not present),这读起来有些奇怪,因为群组显然存在;使用故意无效的群组 ID 进行探测时,返回的句子相同并指出了该 ID,因此该文本只是回显 PATCH 目标。模拟服务也复现了所有这些行为,而不是擅自改进。使用 npx tsx scripts/probe-member-removal.ts --confirm 重新运行(约 17 次计费调用)。
读取组时从不包含成员。 get_group 在任何页面大小下都不会返回 members 数组。要列出组的直接成员,请使用 list_users,并设置 groups.value eq "<groupId>" 筛选条件,这是 Microsoft 目前推荐的方式。要查找用户所属的组,则反向筛选:使用 list_groups,并设置 members.value eq "<userId>"。所有权的处理方式相同。永远不会返回 owners 和 ownedGroups,但 list_groups 支持 owners.value eq "<userId>",而 list_users 支持 ownedGroups.value eq "<groupId>"。
错误采用结构化格式,值得原样呈现。 失败时会包含 status、scimType 和 detail,而且 detail 文本异常具体(会指出出错的操作索引和约束)。这些工具会原样传递该文本,而不会将其简化成一条消息。
每次调用都会计费。 除 API 本身要求的批处理外,没有其他批处理方式,因此频繁调用的 Agent 会产生实际费用。add_group_members 会按 API 的 20 名成员上限分批,这是唯一进行批处理的地方。
使用真实租户进行测试
测试套件不会访问真实租户。若要对照实时 Entra 验证这些工具,请将凭据放入已加入 git 忽略列表的 .env 文件中,并运行冒烟测试脚本。
cd node
cp .env.example .env # then fill in tenant id, client id, and the secret VALUE
.env 仅由 scripts/ 中的脚本读取。已发布的服务始终读取 process.env,因此绝不会从 MCP 客户端启动时所在的目录中意外读取 .env。环境中已设置的变量始终优先于文件中的值。
| 变量 | 用途 |
|---|---|
ENTRA_SCIM_SMOKE_DOMAIN | 已验证的域名;临时 scim-smoke-* 身份将在该域名下创建 |
ENTRA_SCIM_SMOKE_CSA_SET | 属性集名称;设置此项以覆盖两个 Custom Security Attribute 工具 |
ENTRA_SCIM_SMOKE_CSA_ATTR | 该属性集中的属性名称 |
ENTRA_SCIM_SMOKE_CSA_VALUE | 要赋予的值。CSA 有类型限制,API 会拒绝类型不匹配的值:true/false 会作为 JSON 布尔值发送,不带引号的整数会作为数字发送,其他值则作为字符串发送。默认值为字符串,因此对于 Boolean 或 Integer 属性,请设置此项 |
烟雾测试脚本
ENTRA_SCIM_LIVE=1 npm run smoke:live # bash
$env:ENTRA_SCIM_LIVE=1; npm run smoke:live # PowerShell
要传递标志,请直接调用脚本——在 Windows 上,npm run x -- --flag 无法可靠地转发参数:
npx tsx scripts/live-smoke.ts --confirm
按顺序遍历全部 18 个工具,约产生 21 次计费调用。脚本会创建两个用户和一个组,对它们执行所有读取、PATCH 和删除操作,然后再次删除它们。亮点:
- 脚本不会意外运行。 如果没有设置
ENTRA_SCIM_LIVE=1或--confirm,脚本会显示租户、端点和费用,然后退出。如果设置了ENTRA_SCIM_DRY_RUN或ENTRA_SCIM_STATIC_TOKEN,除非传入--rehearse,否则脚本会直接拒绝运行,因为这样的运行无法证明实时 API 的任何情况。 - 脚本不会在首次失败时停止。 失败步骤的依赖项会标记为
skip,所有互不依赖的步骤仍会继续运行,因此一次运行就能告诉你实时 API 接受哪些工具。只要有任何失败,退出代码就非零。 - 测试身份一目了然。
scim-smoke-<runId>-1@<domain>用户和名为SCIM Smoke <runId>的组。 - 保证清理。 所有创建的对象都会在
finally块中删除,并打印任何残留对象的 ID。若运行崩溃,可使用npx tsx scripts/live-smoke.ts --sweep列出残留的scim-smoke-*用户,然后添加--confirm将其删除。清理操作绝不会触及不带scim-smoke-前缀的账户。
在租户中创建属性集(Entra 门户 -> Protection -> Custom security attributes)并通过 ENTRA_SCIM_SMOKE_CSA_SET / ENTRA_SCIM_SMOKE_CSA_ATTR 指定名称之前,两个自定义安全属性工具都会报告 skip。其他所有工具都可无人值守地运行。
创建属性集后,可只验证这两个工具,约需 9 次调用,而不是 21 次——这会读回每个值(否则,API 接受但未存储任何内容的 PATCH 也会看起来像是通过了),覆盖每种已声明的数据类型,并测试移除操作:
npx tsx scripts/live-smoke.ts --csa-only --confirm
只需声明一次属性集的结构,运行时就会为每种类型生成测试值:
ENTRA_SCIM_SMOKE_CSA_ATTRS=isManaged:bool,accountType:string,trustLevel:int,locations:string[]
已针对全部四种类型进行实时验证:值往返后保持不变;op: "remove" 会清除一个赋值并保留其余赋值;将多值属性替换为 [] 会将其移除——之后的读取会完全省略该属性,而不是返回空数组。
在花费任何费用之前,可零成本进行预演——这会验证脚本,而非 API:
# no network at all
ENTRA_SCIM_DRY_RUN=1 npx tsx scripts/live-smoke.ts --rehearse
# or against the local mock: start it in one shell...
npm run mock
# ...and in another, aim the script at it
export ENTRA_SCIM_BASE_URL=http://127.0.0.1:8990
export ENTRA_SCIM_STATIC_TOKEN=dev-token
npx tsx scripts/live-smoke.ts --rehearse
值得进行的是模拟预演:它会使用真实的 HTTP、真实的 ID,并完整执行创建/PATCH/删除顺序,因此能在你花费任何费用之前发现排序和清理问题。但它不能替代实时运行——首次实时运行发现了两个模拟运行未能发现的问题(参见测试环节实际发现的问题)。
测试环节实际发现的问题
三个相互独立的环节,每个环节都发现了其他环节无法发现的问题——这就是三者都存在的原因:
| 环节 | 费用 | 发现的问题 |
|---|---|---|
| 模拟 + 单元测试套件 | 免费 | 排序、验证和清理问题。速度快,但依赖自身假设,因此无法发现错误假设。 |
实时租户(smoke:live) | 约 21 次计费调用 | DELETE 的 Accept 问题(两个从未正常工作的工具)、无效的裸 CSA URN,以及 CSA 类型/移除语义问题。 |
| SCIM Validator | 免费 | 七处模拟保真度差异——模拟的宽松程度超出了真实 SCIM 客户端的预期;每一处差异都掩盖了一种真实行为。 |
值得记住的规律是:模拟的宽松处理会掩盖真实 API 行为。 实时运行发现的每个缺陷都曾通过完整的模拟测试套件,因为模拟程序和客户端一样,都是基于对文档的相同解读编写的。只有第三方客户端(验证器)和真实租户才能打破这种循环。
以对话方式操作实时租户
仓库根目录中的 .mcp.json 使用 scripts/dev-server.mjs 注册服务器,以供 Claude Code 使用。该脚本会加载 node/.env 并启动构建后的服务器,因此无需将密钥写入已提交的配置文件。
它运行的是构建后的服务器,因此 MCP 客户端启动服务器前,node/dist 必须已存在。克隆仓库后,运行以下命令即可完成构建:
cd node && npm install # the "prepare" script builds as part of install
更改任何源代码后,请重新构建并重启客户端,让它重新启动该命令:
cd node && npm run build
开发
cd node
npm install # installs, then builds via "prepare"
npm test
npm run lint # ESLint, type-aware
npm run format:check # Prettier
npm run typecheck # strict tsc over src, test and scripts
npm run test:coverage # vitest with the coverage gate
npm run build # rebuild after a source change
npm run mock # run the local mock server (tsx, no build needed)
npm run mock:capture # mock in validator-compat mode, capturing traffic to captures/
npm run lint、format:check、typecheck 和 test 是 CI 每次推送和拉取请求都会运行的四项检查,此外还会运行 npm audit --audit-level=high。
服务器的测试不依赖真实租户。单元测试覆盖筛选、补丁、查询和客户端层;集成测试会启动进程内模拟服务器,并通过真实 HTTP 对每个 MCP 工具进行端到端测试(node/test/integration/)。捕获的 SCIM Validator 会话可通过 npm run fixtures:convert 转换为回放测试夹具。至于这些测试无法证明的一件事——实时 API 是否接受这些负载——请参阅针对真实租户进行测试。
发布
版本号存在于四处:node/package.json、node/package-lock.json(两处),以及 server.json(两处,一处是注册表记录,另一处是它指向的 npm 包)。一条命令即可写入所有这些位置:
cd node
npm version minor # or patch / major — writes all four, stages three
cd ..
VERSION=$(node -p "require('./node/package.json').version")
git commit -m "v$VERSION"
git tag -a "v$VERSION" -m "v$VERSION"
git push --follow-tags
VERSION 是从 package.json 读取的,而不是手动输入,因此提交和标签的版本号永远不会与 npm 刚写入的版本号不同。
-a 很重要:--follow-tags 只会推送附注标签,因此轻量标签 git tag "v$VERSION" 会留在本机,推送虽报告成功,却没有发送任何标签——发布根本不会运行。
npm version 会递增 package.json 和 lockfile 中的版本号,然后由 version 生命周期脚本将其同步到 server.json 并暂存更改。它不会提交或创建标签,尽管 npm version 通常会执行这两项操作:npm 会在正在进行版本更新的包旁查找 .git,而此包位于 node/ 中,仓库的 .git 则高一级——因此 npm 会判断它不在 git 仓库中,并悄悄跳过这些步骤。所以才需要像上面那样显式提交并创建标签。如果这里出错,git push --follow-tags 会悄悄地什么也不推送,因为它本该跟随的标签根本没有创建。
npm run check:version 会验证这四处的版本号是否一致,CI 会在每次推送时运行它,发布工作流也会针对标签本身再次运行——因此,与 package.json 不一致的标签会在发布任何内容之前失败。服务在 MCP 握手中报告的版本会在运行时从 package.json 读取,因此会自动保持同步。
推送 v* 标签会运行 .github/workflows/release.yml:
- 验证——lint、格式检查、类型检查、带覆盖率的测试、版本与标签检查,以及针对实时注册表运行
mcp-publisher validate。 - 在 Windows 上验证——在
windows-latest上再次运行相同的测试,因为 mock 会绑定真实套接字,捕获接收端会写入真实路径。如果没有这一步,发布门槛就会弱于普通提交的门槛,而 CI 对普通提交也会在 Windows 上运行测试。 - 发布——运行
npm publish,然后等待新版本在 npm 上可见,之后再将server.json发布到 MCP Registry。
只有这些步骤全部通过后,才会发布任何内容。
两次发布都通过 GitHub OIDC 进行身份验证,因此仓库中完全没有密钥——没有发布令牌需要担心泄露、轮换,或在最糟糕的时候才发现过期。
| 注册表 | 此工作流的授权方式 |
|---|---|
| npm | 在此包上设置可信发布者,指定此仓库和工作流文件 release.yml,并将 environment 留空。npm 会将 OIDC 声明与之匹配,并使用同一令牌附加来源证明。需要 npm >= 11.5.1,因此作业会在发布前升级 npm。 |
| MCP Registry | mcp-publisher login github-oidc。在 darrenjrobinson/entra-scim-mcp 中运行此命令,即可授权 io.github.darrenjrobinson/* 命名空间。 |
这两项都是发布作业请求 id-token: write 权限的原因。
MCP Registry 会通过获取 package.json 并将其中的 mcpName 与 server.json 中的 name 进行比较,来验证你是否拥有该 npm 包——两者都是 io.github.darrenjrobinson/entra-scim-mcp,且 check:version 会断言它们始终一致。
更改工作流文件名,或在发布作业中添加 environment:,都会导致 npm 可信发布者失效,直到更新 npmjs.com 上的配置以匹配为止——OIDC 声明会进行精确匹配。
许可证
MIT — 详见 LICENSE。
评论
一个开放协议,规定了 AI 应用如何以统一方式连接外部工具与数据源。客户端负责与模型对话,MCP 服务负责提供工具、资源和提示模板,两者通过标准消息通信,同一个服务可以被不同客户端复用。