mcpField
M
Microsoft Entra SCIM

Microsoft Entra SCIM

本地服务
github.com安全21 天前更新

关于 Microsoft Entra SCIM

Microsoft Entra SCIM 是面向 Agent 的 MCP 服务,将 Microsoft Entra SCIM 2.0 API 的用户与组生命周期操作封装为工具,并提供本地模拟服务,便于无租户测试。

主要功能

  1. 01发现 SCIM 能力、资源类型和架构
  2. 02创建、读取、更新及停用用户
  3. 03管理用户生命周期属性和安全属性
  4. 04创建、更新、删除组及管理成员
  5. 05本地模拟 API 与请求预演

适用场景

  1. 01自动化 Entra 用户入职与离职
  2. 02维护用户组及其成员关系
  3. 03在本地模拟环境验证工具调用

MCP 相关

作为 MCP 服务,它向客户端暴露 Microsoft Entra SCIM 用户和组生命周期工具。另附本地 SCIM 模拟服务,可在不连接 Entra 租户时测试这些工具。

配置

本地服务的配置在各客户端中通用,复制后粘贴到客户端的 MCP 配置文件即可。

mcp.json · 通用配置
{
"mcpServers": {
"darrenjrobinson-entra-scim": {
"command": "npx",
"args": [
"-y",
"entra-scim-mcp"
],
"env": {
"ENTRA_TENANT_ID": "<ENTRA_TENANT_ID>",
"ENTRA_CLIENT_ID": "<ENTRA_CLIENT_ID>",
"ENTRA_CLIENT_SECRET": "<ENTRA_CLIENT_SECRET>",
"ENTRA_CLIENT_CERT_PATH": "<ENTRA_CLIENT_CERT_PATH>",
"ENTRA_CLIENT_CERT_PASSWORD": "<ENTRA_CLIENT_CERT_PASSWORD>",
"ENTRA_SCIM_BASE_URL": "<ENTRA_SCIM_BASE_URL>",
"ENTRA_SCIM_STATIC_TOKEN": "<ENTRA_SCIM_STATIC_TOKEN>",
"ENTRA_SCIM_DRY_RUN": "<ENTRA_SCIM_DRY_RUN>"
}
}
}
}
启动命令npx -y entra-scim-mcp
需要配置的环境变量
ENTRA_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+)。各客户端的配置文件位置不同,请参考客户端文档。

安装包
npmentra-scim-mcp0.4.0

工具

还没有获取到工具列表

本地服务需要在你的电脑上运行后才能读取工具,维护者暂未提供工具列表。

详细介绍

entra-scim-mcp

npm version npm downloads license

用于 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 文档完成一次性设置:

  1. Entra ID P1(或包含 P1 的任意 SKU)以及用于关联计费的 Azure 订阅。
  2. 在 ID Governance → Dashboard 中启用 SCIM Provisioning API,并关联计费资源组。
  3. 注册应用,并授予所需的 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) 授予管理员同意。
  4. 创建客户端密钥或上传 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 投影中排除 enterprise manager 时才会生效;否则 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
从未是用户的格式正确的 GUID404
用户已先被删除的成员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:

  1. 验证——lint、格式检查、类型检查、带覆盖率的测试、版本与标签检查,以及针对实时注册表运行 mcp-publisher validate。
  2. 在 Windows 上验证——在 windows-latest 上再次运行相同的测试,因为 mock 会绑定真实套接字,捕获接收端会写入真实路径。如果没有这一步,发布门槛就会弱于普通提交的门槛,而 CI 对普通提交也会在 Windows 上运行测试。
  3. 发布——运行 npm publish,然后等待新版本在 npm 上可见,之后再将 server.json 发布到 MCP Registry。

只有这些步骤全部通过后,才会发布任何内容。

两次发布都通过 GitHub OIDC 进行身份验证,因此仓库中完全没有密钥——没有发布令牌需要担心泄露、轮换,或在最糟糕的时候才发现过期。

注册表此工作流的授权方式
npm在此包上设置可信发布者,指定此仓库和工作流文件 release.yml,并将 environment 留空。npm 会将 OIDC 声明与之匹配,并使用同一令牌附加来源证明。需要 npm >= 11.5.1,因此作业会在发布前升级 npm。
MCP Registrymcp-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。

评论

支持 Markdown

第一次接触 MCP?

从协议是什么,到在客户端里装上第一个服务,这几条回答覆盖了大多数入门问题。

一个开放协议,规定了 AI 应用如何以统一方式连接外部工具与数据源。客户端负责与模型对话,MCP 服务负责提供工具、资源和提示模板,两者通过标准消息通信,同一个服务可以被不同客户端复用。