Skip to main content
Glama
darrenjrobinson

entra-scim-mcp

entra-scim-mcp

面向 Microsoft Entra SCIM 2.0 Provisioning API(GA,2026年4月)的 Model Context Protocol 服务器。将针对 https://graph.microsoft.com/rp/scim 的用户和组生命周期操作作为 MCP 工具提供给 Claude 等智能体。

可用功能

  • 发现租户的 SCIM 能力(get_service_provider_config、list_resource_types、list_schemas)

  • 设置、读取、更新和取消设置用户——包括 Custom Security Attributes 和生命周期属性

  • 创建、更新和删除组,并自动遵守该 API 严格的 PATCH 规则来管理成员

Related MCP server: mcp-server-microsoft365

前提条件

在此服务器能与你的租户通信之前,需要先完成 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(最小权限替代项) 然后授予管理员同意。

  4. 创建 一个 client secret 或 上传 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(用于 Microsoft SCIM Validator 的 RFC 标准行为——参见 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 主机;对非 loopback 主机发出警告;不能与真实凭据同时使用。此模式下不需要租户/客户端 ID。

ENTRA_SCIM_DRY_RUN

设为 1:工具执行所有客户端验证,然后返回将要发送的精确请求,而不是发送它。不获取令牌——可在零凭据配置下运行。

试运行结果会以成功载荷的形式返回:

{
  "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 超过 20 个时)在试运行中只显示它的第一个管道请求。

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": "..."
      }
    }
  }
}

对于生产环境,将 client secret 替换为证书:

{
  "env": {
    "ENTRA_TENANT_ID": "...",
    "ENTRA_CLIENT_ID": "...",
    "ENTRA_CLIENT_CERT_PATH": "/secure/path/entra-scim-mcp.pem"
  }
}

工具

工具

用途

get_service_provider_config

一次性能力发现。

little_resource_types

枚举 SCIM 资源类型(User、Group)。

little_schemas

枚举 SCIM 架构和 Entra 扩展。

little_users

列出用户;支持 API 受限的过滤器(eq/ew,且运算符)和游标分页。

little_user

按 ID 读取单个用户,并可选属性投影。

little

创建用户,强制要求的属性集(userName、password、displayName、name.givenName、name.familyName、mailNickname)。

little_user

执行 PATCH 更新用户;阻止移除 mailNickname,并强制地址路径符合 [type eq "work"]。

little

删除用户。

little_lifecycle

设置生命周期属性(如 employeeLeaveDateTime)。需要 User-LifeCycleInfo.ReadWrite.All。

get_user_custom_security_attributes

按属性集投影读取用户的 CSAs。attributeSets 为必需——API 拒绝裸扩展 URN,且普通 get_user 不会返回 CSAs。

update_user_custom_security_attributes

对用户进行 CSA 的 PATCH 更新。

little_groups

使用 API 受限的过滤器集列出组。

get_group

读取单个组(不返回成员,请使用带 members.value 过滤器的 list_groups)。

little_groups

使用 POST 创建组。通过 Entra 扩展设置 mailEnabled、securityEnabled、mailNickname、description。

update_group

仅更新组属性(此处不接受成员操作)。

little_members

向组中添加 1 个或多个用户——每 PATCH 自动分块为 20 个 ID(API 上限),每个 PATCH 只有一个操作。中途失败时会报告 addedMemberIds / failedMemberIds / notAttemptedMemberIds,这样部分写入不会静默丢失。

remove_group_member

从组中移除单个用户(API 每 PATCH 只允许一次移除,且不能包含其他操作)。

delete_group

删除组。

此服务器为你强制处理的约束

Entra SCIM API 有一些很容易误解的约束。工具层在发送请求前会拒绝无效输入:

  • 过滤器白名单:每种资源只能使用文档规定的属性和操作;拒绝 or;externalId 不能与另一个子句一起使用。

  • 查询参数:在 = 两侧不能有空格(API 会对任何这种请求返回 400)。

  • 用户 PATCH:禁止移除 mailName;地址路径过滤器必须正好为 [type eq "work"]。

  • 组 PATCH:成员操作必须通过专用工具,以确保 20 个成员的添加上限和单移除规则被坚持。

  • 幂等成员添加:API 将重复添加视为成功;add_group_members 会对输入进行去重。

错误会作为包含 status、scimType、detail 的结构化载荷返回给智能体。

值得了解的 API 行为

真实 API 有一类行为,文档表述不清晰或完全没有说明。每一条都是通过与真实租户或 Microsoft SCIM Validator 的连接发现的,并且已经为你处理好了——列出它们,是因为它们会改变你读取响应时的理解。

DELETE 不得携带 Accept 头。 API 返回 400 Accept header application/json is invalid。四种变体都已在真实环境中实际探测过:无 header → 204,*/* → 204,application/json → 400,application/scim+json → 400。只有 DELETE 反转了这一规则——其他每个方法都要求 JSON Accept,省略它则是文档中有据可查的 400。这一条曾让 deprovision_user 和 delete_group 悄然失效,直到第一次真实运行才暴露。

自定义安全属性(Custom Security Attributes)从不会在普通读取中返回。 在 /Schemas 中,属性集层面的属性是 returned: "request",所以无论你请求什么,get_user 都不会包含 CSA。它们只在被显式指定名称时出现,而且投影是**按属性集粒度(set-granular)**的:urn:...:CustomSecurityAttributes:<Set>。裸的扩展 URN 会被直接拒绝(400 ... not supported in the "attributes" or "excludedAttributes" query parameter),所以 attributeSets 是必填输入而不是可选输入。

CSA 值有类型划分,并且类型会被强制校验。 Boolean、Integer、String 和多值 String 在以匹配的 JSON 类型发送时都能完好往返,而且一次 PATCH 可以同时携带多个属性。有两种删除行为,微软都没有文档化:

  • 对某个 CSA 路径执行 op: "remove",会只清掉该赋值,其余赋值保持原样。

  • 对多值属性执行 replace 并赋 [],会移除该赋值——之后读取时会完全省略该属性,而不是返回空数组。

password 在创建时必填,但永远不可读。 它是 writeOnly / returned: never,没有任何响应会回显它。创建时的完整必填集合是 userName、password、displayName、name.givenName、name.familyName 和 mailNickname——比 RFC 7643 严格得多,后者只要求 userName。

Group 的 displayName 并不唯一。 Entra 接受重复的组名并返回 201。面向 RFC 的工具通常在这里假设是 409,所以别指望创建失败来发现已存在的组——先过滤。

移除组成员关乎的是成员资格,而不是用户本身。 对 members[value eq "<id>"] 执行 remove 若没有匹配项,不论该 id 属于谁都会得到 404——一个从未入组的真实用户,会与一个从未成为用户的 GUID 被同样地拒绝。这个结论是在真实租户上探测的,且全程有一个陪衬组成员,所以并非清空组的假象:

Case

Result

真实成员

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 的目标。Mock 完整还原了这一切,而非修正它。用 npx tsx scripts/probe-member-removal.ts --confirm 重新运行(约 17 次计费调用)。

读取组永远不返还成员列表。 get_group 无论在什么页大小下都不返回 members 数组。要查某个用户所在的组,反过来过滤即可:用带 members.value eq "<userId>" 条件的 list_groups。

错误是结构化的,而且值得原样呈现。 失败响应携带 status、scimType 和 detail,其中 detail 文本异常具体(会指名操作序号和违反的约束)。工具会原样透传,而不把它拍平为一条 message。

每次调用都计费。 除 API 自身要求外没有任何批量合并,所以一个话多的 agent 是要花真钱的。add_group_members 按 API 的 20 成员上限分包,这是唯一发生批量的地方。

针对真实租户进行测试

测试套件绝不碰真实租户。要对真实 Entra 验证这些工具,请把凭据放到一个被 gitignore 的 .env 中并运行 smoke 脚本。

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 工具所需的 submit 范围

ENTRA_SCIM_SMOKE_CSA_ATTR

该属性集内属性名

ENTRA_SCIM_SMOKE_CSA_VALUE

要赋的值。CSA 有类型,API 拒绝类型不匹配:true/false 以 JSON 布尔发送,true 整数以数字发送,其他一律以字符串发送。默认为字符串,所以对 Boolean 或 Integer 属性需特别设置

冒烟脚本

ENTRA_SCIM_LIVE=1 npm run smoke:live              # bash
$env:ENTRA_SCIM_LIVE=1; npm run smoke:live        # PowerShell

要传递参数,直接调用脚本本身——而 npm run x -- --flag 在 Windows 上可能无法可靠转发:

npx tsx scripts/live-smoke.ts --confirm

它是有序地一次跑完全部 18 个工具,约 21 次计费调用。脚本创建两个用户和一个组,对它们执行每一种读、PATCH 和删除,再把这些资源一并清理掉。要点:

  • 它不会误运行。 没有 ENTRA_SCIM_LIVE=1 或 --confirm 就只打印租户、endpoint 和成本,然后退出。设置 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- 前缀的账号。

两个 Custom Security Attribute 工具会报告 skip,直到租户里已存在属性集(Entra portal -> Protection -> Custom security attributes)并且 ENTRA_SCIM_SMOKE_CSA_SET / ENTRA_SCIM_SMOKE_CSA_ATTR 指定了它的名字。其他一切都自动运行。

一旦属性集存在,即可只验证这两个工具,约 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

Mock rehearsal 是值得跑的那一个:它演练真实的 HTTP 和真实 id,以及完整 create/patch/delete 顺序,因此能在你花费之前抓到排序和清理的 bug。但它不能替代真实运行——第一次真实运行就发现了两个 mock 之前从未发现的 bug(见 测试环节实际抓到了什么)。

测试环节实际抓到了什么

三个相互独立的环节,每个都能发现另两个抓不到的东西——这正是三者并存的原因:

环节

成本

抓到的内容

Mock + 单元测试套件

免费

序列、校验和清理方面的 bug。快速,但它采用的是自身的一套假设,因此无法发现错误的假设。

真实租户(smoke:live)

~21 次计费调用

DELETE Accept bug(这两个工具本来又没工作过)、无效的裸 CSA URN,以及 CSA 的类型/删除语义。

SCIM Validator

免费

七处 mock 保真度缺口——mock 比真实 SCIM 客户端更宽松的地方,每个缺口都掩盖了一种真实行为。

值得学习的模式是:mock 的宽松隐藏了真实 API 的行为。 每次真实运行发现的 bug 都先通过了整套 mock 测试,因为 mock 与客户端对文档的解读同源。第三方客户端(validator)和真实租户,是能打破这种循环的仅有外部客观参照。

在对话中驱动真实租户

仓库根目录的 .mcp.json 通过 scripts/dev-server.mjs 向 Claude Code 注册服务;该脚本加载 node/.env 并启动构建后的服务器——因此不会把任何机密写入提交到仓库的配置里。

它运行的是构建后的服务器,所以你的 MCP 客户端必须让 node/dist 先存在才可启动该工具。全新 clone 只需:

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 在每次 push 和 pull request 上运行的四个门,另外还有 npm steps audit 也运行,npm audit。

服务器不需要真实租户进行测试。单元测试覆盖 filter、patch、query 和 client 各层;集成测试在进程内启动 mock server,并通过真实 HTTP 对每个 MCP 工具做端到端驱动(见 node/test/integration/)。捕获到的 SCIM Validator 会话可用 npm run fixtures:convert 转成回放 fixture。至于上述测试无法证明的事情——真实 API 是否接受这些 payload——详见 针对真实租户进行测试。

发布

版本号存在于四个地方——node/package.json、node/package-lock.json(出现两次)以及 server.json(出现两次,一次用于 registry 记录,一次用于其指向的 npm 包)。一个命令会同时写入各处:

cd node
npm version minor          # or patch / major — writes all four, stages three
cd ..
git commit -m "v0.2.0"     # the version npm just printed
git tag -a v0.2.0 -m v0.2.0
git push --follow-tags

-a 很重要:--follow-tags 只会推送附注标签,因此轻量级的 git tag v0.2.0 会留在你的机器上,而推送会报告成功,但实际上没有发送任何标签——发布流程根本不会运行。

npm version 会更新 package.json 和 lockfile,然后 version 生命周期脚本会把版本号传播到 server.json 并暂存结果。它不会提交或打标签,尽管 npm version 通常两者都会做:npm 会在它正在版本化的包旁边查找 .git,而这个包位于 node/ 中,仓库的 .git 则在上一级目录,所以 npm 判定自己不在 git 仓库中,于是便静默地跳过这些步骤。这正是上面需要显式的 commit 和 tag 的原因。如果这一步搞错,git push --follow-tags 就会悄悄顺利推送,但实际上什么也没有推送——因为它要跟随的标签从未被创建。

npm run check:version 会验证四处版本号是否一致,CI 在每次推送时都会运行它,发布工作流还会针对标签本身再运行一次——所以任何与 package.json 不一致的标签都会在任何内容发布之前失败。服务端在 MCP 连接握手中上报的版本是在运行时从 package.json 读取的,因此它会自动保持一致。

推送一个 v* 标签会运行 .github/workflows/release.yml:

  1. 验证 — lint、格式化、类型检查、带覆盖率的测试、版本/标签检查,以及针对实际 registry 的 mcp-publisher validate。

  2. 在 Windows 上验证 — 在 windows-latest 上再次运行相同的测试,因为 mock 绑定的是真实 socket,捕获 sink(capture sink)也会写入真实路径。若没有这一层验证,发布门槛就会比普通提交的门槛更弱,而普通提交的 CI 同样会在 Windows 上运行。

  3. 发布 — 执行 npm publish,然后等待新版本在 npm 上可见,再将 server.json 发布到 MCP Registry。

只有当以上步骤全部通过后,才会发布任何内容。

两处发布都通过 GitHub OIDC 进行身份认证,所以这个仓库里没有任何机密——没有发布 token 会泄露、轮换,也不会在最糟糕的时刻发现它已过期。

Registry

此工作流的授权方式

npm

在该包上的受信任发布者,配置为本仓库和工作流文件 release.yml,并使用空环境。npm 将 OIDC 声明与它匹配,并从同一个 token 附加来源证明(provenance)。它要求 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。

Related MCP Connectors

Related MCP Servers

  • A
    license
    A
    quality
    B
    maintenance
    Enables identity provisioning and management for Microsoft 365/Entra ID via Microsoft Graph, including user creation, license assignment, group membership management, and more, with a focus on least-privilege and idempotency.
    18
    Apache 2.0