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_configlist_resource_typeslist_schemas

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

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

Related MCP server: mcp-m365-mgmt

前提条件

在此服务器能与你的租户通信之前,需要先完成 Microsoft 文档 中的一次性设置:

  1. 需要 Entra ID P1(或任何包含 P1 的 SKU)以及用于关联计费的 Azure 订阅。

  2. ID Governance → Dashboard 中启用 SCIM Provisioning API,并关联一个计费资源组。

  3. 注册一个应用,并授予你所需的 Microsoft Graph application 权限:

    • User.ReadWrite.AllGroup.ReadWrite.All(核心生命周期)

    • CustomSecAttributeAssignment.ReadWrite.AllCustomSecAttributeDefinition.Read.All(CSA 工具)

    • User-LifeCycleInfo.ReadWrite.All(生命周期工具)

    • User-Mail.ReadWrite.AllUser-Phone.ReadWrite.AllUser.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_SECRETENTRA_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 扩展设置 mailEnabledsecurityEnabledmailNicknamedescription

update_group

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

little_members

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

remove_group_member

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

delete_group

删除组。

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

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

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

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

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

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

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

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

值得了解的 API 行为

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

DELETE 不得携带 Accept 头。 API 返回 400 Accept header application/json is invalid。四种变体都已在真实环境中实际探测过:无 header → 204*/*204application/json400application/scim+json400。只有 DELETE 反转了这一规则——其他每个方法都要求 JSON Accept,省略它则是文档中有据可查的 400。这一条曾让 deprovision_userdelete_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,没有任何响应会回显它。创建时的完整必填集合是 userNamepassworddisplayNamename.givenNamename.familyNamemailNickname——比 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

错误是结构化的,而且值得原样呈现。 失败响应携带 statusscimTypedetail,其中 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_RUNENTRA_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 lintformat:checktypechecktest 是 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.jsonnode/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,并比较其中的 mcpNameserver.json 中的 name,来证明你对 npm 包的所有权——两者都是 io.github.darrenjrobinson/entra-scim-mcp,并 check:version 会校验它们仍保持匹配。

修改工作流的文件名,或在发布任务中添加 environment:,都会让 npm 的受信任发布者配置失效,直到 npmjs.com 上的配置也同步更新为止——因为 OIDC 声明会被精确比对。

许可证

MIT — 参见 LICENSE

A
license - permissive license
Not graded
quality - not tested
B
maintenance

Maintenance

Maintainers
Response time
Release cycle
Releases (12mo)
Commit activity

Resources

Unclaimed servers have limited discoverability.

Looking for Admin?

If you are the server author, to access and configure the admin panel.

Related MCP Servers

View all related MCP servers

Related MCP Connectors

  • Create and manage AI agents that collaborate and solve problems through natural language interacti…

  • Runtime permission, approval, and audit layer for AI agent tool execution.

  • Give AI agents the LinkedIn tools to find, qualify, engage, and follow up with prospects.

View all MCP Connectors

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/darrenjrobinson/entra-scim-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server