Skip to main content
Glama
darrenjrobinson

entra-scim-mcp

entra-scim-mcp

Servidor de Model Context Protocol para la API de aprovisionamiento SCIM 2.0 de Microsoft Entra (GA abril de 2026). Expone operaciones del ciclo de vida de usuarios y grupos contra https://graph.microsoft.com/rp/scim como herramientas MCP para agentes como Claude.

Qué puedes hacer con él

  • Descubrir las capacidades SCIM del inquilino (get_service_provider_config, list_resource_types, list_schemas)

  • Aprovisionar, leer, actualizar y desaprovisionar usuarios, incluyendo Atributos de Seguridad Personalizados y atributos de ciclo de vida

  • Crear, actualizar y eliminar grupos, y gestionar la pertenencia respetando automáticamente las estrictas reglas PATCH de la API

Related MCP server: mcp-m365-mgmt

Requisitos previos

Antes de que este servidor pueda comunicarse con tu tenant, completa la configuración única en la documentación de Microsoft:

  1. Entra ID P1 (o cualquier SKU que incluya P1) y una suscripción de Azure que vincular para facturación.

  2. Habilita la SCIM Provisioning API en ID Governance → Dashboard y vincula un grupo de recursos de facturación.

  3. Registra una aplicación con los permisos de aplicación de Microsoft Graph que necesites:

    • User.ReadWrite.All, Group.ReadWrite.All (ciclo de vida principal)

    • CustomSecAttributeAssignment.ReadWrite.All, CustomSecAttributeDefinition.Read.All (herramientas de CSA)

    • User-LifeCycleInfo.ReadWrite.All (herramientas de ciclo de vida)

    • User-Mail.ReadWrite.All, User-Phone.ReadWrite.All, User.EnableDisableAccount.All (alternativas de privilegio mínimo) Concede el consentimiento del administrador.

  4. Crea o bien un secreto de cliente o bien sube un certificado de cliente PEM.

Cada llamada a la API SCIM se factura; este servidor no agrupa solicitudes más allá de lo que la API exige.

Pruébalo sin un tenant de Entra

El paquete incluye un mock local de la API SCIM de Entra (entra-scim-mock-server) para que puedas usar todas las herramientas sin configuración de Azure y sin coste de API:

# shell 1 — start the mock (seeds a small demo tenant)
npx -y --package entra-scim-mcp entra-scim-mock-server

Después, apunta el servidor MCP hacia él:

{
  "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"
      }
    }
  }
}

Opciones del mock: --port, --token, --seed <file.json>, --no-seed, --capture <file.jsonl> (registra cada solicitud/respuesta), --validator-compat (comportamiento estándar RFC para el Validador SCIM de Microsoft — consulta la documentación).

Instalación / ejecución

El servidor es un servidor MCP stdio, diseñado para ser iniciado por tu cliente MCP (Claude Desktop, Claude Code, etc.).

npx -y entra-scim-mcp

Entorno requerido:

Var

Requerida

Descripción

ENTRA_TENANT_ID

GUID del directorio (tenant)

ENTRA_CLIENT_ID

GUID del registro de la aplicación (cliente)

ENTRA_CLIENT_SECRET

una de

Valor del secreto de cliente (desarrollo)

ENTRA_CLIENT_CERT_PATH

una de

Ruta a un PEM que contenga el certificado y la clave privada

ENTRA_CLIENT_CERT_PASSWORD

opcional

Contraseña si el PEM está cifrado

Configura exactamente una de las variables ENTRA_CLIENT_SECRET o ENTRA_CLIENT_CERT_PATH.

Variables de entorno para desarrollo / pruebas

Variable

Descripción

ENTRA_SCIM_BASE_URL

Sobrescribe la TDL SCIM (por defecto https://api..microsoft.com/root/scim). Apúntala al mock local.

ENTRA_SCIM_STATIC_TOKEN

Usa un token de portador fijo en lugar de Azure AD. Protecciones: requiere ENTRA_SCIM_BASE_URL, rechaza cualquier host *.microsoft.com / *.microsoft.us, avisa sobre hosts que no sean de bucle local, y no se puede combinar con una credencial real. No se necesitan los IDs de tenant/cliente en este modo.

ENTRA_SCIM_DRY_RUN

Configúrala a 1: las herramientas ejecutan todas las validaciones del lado del cliente y luego devuelven la solicitud exacta que se habría enviado en lugar de enviarla. No se adquiere token: funciona sin ninguna configuración de credenciales.

Los resultados de la simulación en seco devuelven un payload exitoso:

{
  "dryRun": true,
  "request": {
    "method": "DELETE",
    "url": "https://graph.microsoft.com/rp/scim/users/u-1",
    "headers": {}
  }
}

(DELETE no lleva ningún encabezado Accept; la API rechaza un tipo de medio JSON específico en ese caso. Todos los demás métodos envían Accept: application/json.)

Las herramientas de múltiples solicitudes (p. ej., add_group_members con más de 20 identificadores) solo devuelven su primera solicitud segmentada en modo de prueba.

Configuración de Claude Desktop

~/Library/Application Support/Claude/claude_desktop_config.json (macOS) o %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": "..."
      }
    }
  }
}

Para producción, cambia el secreto por un certificado:

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

Herramientas

Herramienta

Propósito

get_service_provider_config

Descubrimiento de capacidades automático.

list_resource_types

Enumerar tipos de recursos SCIM (Persona, Grupo).

list_schemas

Enumerar esquemas SCIM y extensiones Entra.

list_users

Listar usuarios; admite el filtro restringido de la API (solo eq/ew y and) y paginación por cursor.

get_user

Leer un único usuario por identificador con proyección de atributos opcional.

provision_user

Crear un usuario con el conjunto de atributos obligatorio impuesto (userName, password, displayName, name.givenName, name.familyName, mailNickname).

update_user

Actualizar un usuario con PATCH; impide remove para el mailNickname y obliga [type eq "work"] en las rutas de direcciones.

deprovision_user

Eliminar un usuario con DELETE.

update_user_lifecycle

Establecer atributos del ciclo de vida (p. ej., employeeLeaveDateTime). Requiere User-LifeCycleInfo.ReadWrite.All.

get_user_custom_security_attributes

Leer los ASC de un usuario, proyectados por conjunto de atributos. attributeSets es obligatorio; la API rechaza la ETR básica y los ASC nunca se devuelven con get_user.

update_user_custom_security_attributes

Actualizar los ASC de un usuario con PATCH.

list_groups

Listar grupos con el conjunto de filtros restringido de la API.

get_group

Leer un único grupo (no devuelve los miembros; usa list_groups con un filtro de members.value).

create_group

Crear un grupo con POST. Configura mailEnabled, securityEnabled, mailNickname, description a través de la extensión de Entra.

update_group

Actualizar solo atributos de grupo con PATCH; aquí se rechazan operaciones de pertenencia.

add_group_members

Añade ≥1 usuarios a un grupo: segmenta automáticamente en bloques de máximo 20 miembros por PATCH (límite de la API), una sola operación por PATCH. Si falla a mediados de la secuencia, reporta addedMemberIds / failedMemberIds / notAttemptedMemberIds para que los escrituras parciales no pasen desapercibidas.

remove_group_member

Eliminar un usuario de un grupo con una sola eliminación por PATCH (la API solo permite una sola por PATCH, sin otras operaciones).

delete_group

Eliminar un grupo con DELETE.

Qué impone este servidor por ti

La API SCIM de Entra tiene restricciones que son fáciles de pasar de entrar. La capa de herramientas rechaza entradas incorrectas antes de enviar una solicitud:

  • Lista blanca de filtros: solo los atributos y operadores documentados por recurso; se rechaza or; externalId no se puede combinar con otra cláusula.

  • Cadenas de consulta: sin espacios en los lados de = (la API devuelve un 400 para cualquiera).

  • PATCH de usuario: se bloquea remove de mailNickname; el filtro de rutas de addresses debe ser exactamente [type eq "work"].

  • PATCH de grupo: las operaciones de pertenencia se deben hacer a través de herramientas dedicadas para garantizar el límite de 20 miembros de alta y la regla de eliminación individual.

  • Alta de miembros idempotente: la API considera exitosas las reintroducciones; add_group_members elimina duplicados de la entrada.

Los errores se devuelven al agente como un payload estructurado con status, scimType y detail.

Comportamientos de la API que conviene conocer

Cosas que hace la API en vivo que los documentos no dicen claramente o no dicen en absoluto. Cada una se encontró funcionando contra un tenant real o el Validador SCIM de Microsoft, y cada una está resuelta por ti; se listan en este apartado de cómunicación de comprender la respuesta.

**DELETE no debe llevar una cabecera Accept. ** La API responde 400 Accept header application/json is invalid. Se probaron las cuatro variantes en vivo: sin cabe cera → 204, */* → 204, application/json → 400, application/scim+json → 400. Only DELETE invertor la regla — cada otro método requiere un Accept JSON, y omitirlo es un 400 documentado. Esto romió silenciosaente deprovision_user y delete_group hasta la primea ejecución en vivo.

CustomSecurity Attributes nunca vuelven a devolver en una lectura simple. En/SChems el atributo a nivel de conjunto de atributos es retured: "request", as so get_ser no inclúira CSA sin que impórt lo que se pid. Solo apaarrect when se nomb r explíci, and la proyección es por conjunto: urn:...:CustomSecurityAttributes:<Set>. La URN de extrema desstandard rejected directly (400 ... not supported in the "attributes" or "excludedAttributes" query parameter), and que por eso attributeSets ent us required input and not one opcionales.

CSA values are tipoized, and the type is enforced. Boolean, Integer, String and multi-valued String all round-trip both in tact when enviano as the only commonation; is type JSON, and one PATCH may can carry many attrs at once. Two removal behaviors, no one documented by Microsoft:

  • op: "remove" on a CSA road causes that one cluster or other intact.

  • replace with [] on an a multi-vaqued attribute removes the quote — a data.

password is required on create but can be never read. It is writeOnly / returneds, and no response eechoes it. The full required set at create is userName, password, displayName, name.givenName, name.familyName and mailNickname, considerably stronger that RFC 7643, which only demands userName.

Group displayName is not unique. Enta is acceptable duplicado and returns 201. The tool that has a RFC orientation often assumes 409 here, so don’t trusts of create to fail to detect an existing group — first filter it.

Remove a group member is a question of membership, not of user. A remove on members[value eq "<id>"] that matches nothing is a 404 because the id of person; a live user simply never in grupo is rejected exactly like GUID who never was a user. It has been checked against a real tenant with a mobile member present throughout, so no is it. is not another artifact of the emptied group:

Case

Result

A real member

204

A live user who never was a member

404

A well-formed GUID that was never a user

404

A member whose user was fired first

404

Two consequences you should know. When a user is removed, his memberships disappear, so the ordering of "delete then check delete memberships" ends up in the same situation than "any others not member"; this was confirmed by looking at list_groups with a filter on members.value just before and after the delete. And the error names the group, not the member (Resource '<groupId>' does not exist or one of its queried reference-property objects are not present), which is weird because the group actually remains. A probe with a deliberately invalid group id also threw the same sentence naming that id, so the text simply echoes the PATCH targeted. The mock reproduces all of this rather than improve it. Run it again with npx tsx scripts/probe-member-removal.ts --confirm (~17 billable calls).

Reads of a group never include members. get_group does not return in any member anywhere, whatever the size of page. To find groups of a user, filter to the reverse: list_groups con members.value eq "<userId>".

The errors have structure and are worth displaying verbatim. A failure carries status, scimType and detail; and the detail text is unusually specific (names the offending operation index and the constraint). The tools pass this unchanged instead of flattening it into a message.

All calls are billed. There is no batching beyond what the API itself requires, so a wordy agent is really expensive. add_group_members makes batches with the API's cap at 20 members, and this is the only point where batching happens.

Test against a real tenant

The test suite never hits a real tenant. To validate the tools against a live Entra, put the credentials in a .env ignored by git and run the smoke script.

cd node
cp .env.example .env      # then fill in tenant id, client id, and the secret VALUE

The .env are read only by the scripts in scripts/. The published server always reads process.env, so it can never pick up a .env inserted by any folder where an MCP client starts it. A variable published in environment always wins over a file.

Variable

Purpose

ENTRA_SCIM_SMOKE_DOMAIN

Verified setting the temporary scim-smoke-* identities are created in

ENTRA_SCIM_SMOKE_CSA_SET

Set attribute name. Set it so to cover the two Custom Security Attributes tools

ENTRA_SCIM_SMOKE_CSA_ATTR

Attribute name inside that set

ENTRA_SCIM_SMOKE_CSA_VALUE

What value twice. CSAs are typed and the API rejects a type mismatch: true/false are converted into a JSON boolean, a bare integer has the number, everything else as a string. Defaults to a string, so set this for Boolean/Integer attributes

The smoke script

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

To pass flags, call the script directly – npm run x -- --flag not robust forwarding on Windows:

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

One orderly pass over all 18 tools in around 21 billed calls. It creates two users and one group, does all reads, PATCHes and deletes on them, and then deletes them again. Characteristics:

  • It refuses to be run by accident. If there isn’t ENTRA_SCIM_LIVE=1 or --confirm, it prints the tenant, endpoint and cost, and then stops. If ENTRA_SCIM_DRY_RUN or ENTRA_SCIM_STATIC_TOKEN is set, it explicitly refuses unless --rehearse is passed, because such run does not prove anything for the live API.

  • It doesn't stop when one fails. A failed step marks its dependencies as skip, and everything independent still runs, so a single run lets you know which tools the live API actually supports. Exit code is nonzero if anything fails.

  • Test identity are obvious scim-smoke-<runId>-1@<domain> and a group SCIM Smoke <runId>.

  • Cleanup is always performed. Whatever is created is gone in a finally block, the leftovers are displayed with their ids. Recover from shed back a process with npx tsx scripts/live-smoke.ts --sweep that lists the missing scim-smoke-* users; then add --confirm to remove them. The sweep will never modify an account not in scim- smoke- prefix.

While until a attribute set exists in the tenant (“Entra portal -> Protection -> Custom security attributes”) and ENTRA_SCIM_SMOKE_CSA_SET / ENTRA_SCIM_SMOKE_CSA_ATTR name it, those two Custom Security Attributes produce skip. Everything other runs without assistance.

Once the set is present, validate only these two tools for ~9 calls instead of 21. Validated reads every value back (a PATCH that the API accepts but does not store would otherwise look like a success), covers each type already declared, and does a removal:

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

Set the set form once and derive a test value for each type:

ENTRA_SCIM_SMOKE_CSA_ATTRS=isManaged:bool,accountType:string,trustLevel:int,locations:string[]

Confirmed live for all four types: values round-trip intact, op: "remove" clears one entry and leaves the rest, and replace of a multi-valued → [] deletes it. The later read omits the attribute entirely, instead of giving empty array.

For zero cost before spending, rehearsals. Validates the script, not the 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

The mock rehearsal is worth doing: real HTTP, real id and the full ordering create / PATCH / delete are exercised, so catches ordering and cleanup failures before you pay. It does not replace the real run (first live initialization found two bugs no mock run had caught; see what the testing legs really caught).

What the testing legs really exposed

Three independent legs, each has found things the other could not. That's the reason all three exist:

Leg

Cost

Found

Mock + unit tests

free

Sequencing, validation and cleanup errors. Fast, but shares its own hypothesis, so it cannot catch a wrong hypothesis.

Live tenant (smoke: live)

Side: ~21 billed event

The DELETE error Accept (two never executed tool), the invalid bare CSA URN, and the types/removes.

SCIM Validator ([node/docs/scim-validator.md)

free

Seven gaps in mock fidelity: places the mock was more likely than a real SCIM client expects; each one had hidden a real behavior.

What worth preserving take away: the mock leniency hides actual behavior of the API..: How mock leniency hides the real behavior. Every deficiency that a live execution found before had passed a full mock suite, because the mock had been written using the same reading of the documentation as the client. A third-party client (the validator) and a living tenant were the only factors that could break this circularity.

Driving the live tenant conversationally

The .mcp.json at root registers the server in Claude Code using scripts/dev-server.mjs; that script loads node/.env and starts the built server, so no secret is put into a committed configuration file.

It executes the built server, so node/dist must exist before your MCP client can launch it. A fresh clone is reached by:

cd node && npm install     # the "prepare" script builds as part of install

After a source modification, rebuild and restart the client so it relaunches the command:

cd node && npm run build

Development

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 and test are the four gates CI runs on every push/pull request, along with npm audit --audit-level=high.

The server has no test dependency on a real tenant. Unit testing covers the filter, patch, query and the client layer; integration tests start the in-process mock server and point all MCP tools end-to-end over real HTTP (node/test/integration/). Captured SCIM Validator sessions can be turned into replay fixtures using npm run fixtures:convert. For the one thing all of this can't guarantee – that live API accepts those payloads – see Testing against a real tenant.

Releasing

The version physically occupies four places: node/package.json, node/package-lock.json (twice) and server.json (twice: once for registry record, and once for the npm Package it points). A single command writes all of them.

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

El -a importa: --follow-tags solo envía etiquetas anotadas, así que una etiqueta ligera como git tag v0.2.0 se queda en tu máquina y el push informa de éxito sin haber enviado ninguna etiqueta — el lanzamiento simplemente nunca se ejecuta.

npm version incrementa la versión en package.json y en el lockfile; luego el script del ciclo de vida version lo propaga a server.json y prepara el resultado para el commit. No hace commit ni etiqueta, aunque npm version normalmente hace ambas cosas: npm busca .git junto al paquete que está versionando, este paquete vive en node/, y el .git del repositorio está un nivel más arriba — así que npm decides que no está en un repo git y se salta esos pasos sin decirlo. De ahí el commit y la etiqueta explícitos de arriba. Si te equívocas en esto, git push --follow-tags no envía nada en silencio, porque la etiqueta a seguir nunca se creó.

npm run check:version comprueba que los cuatro coinciden, CI lo ejecuuta en every push y the workflow lanzamiento lo vuelve a ejecutar contra la propia etiqueta — así que una etiqueta que no cocuerde con package.json falla antes de publicar any cos a. La versión que el servidor reporta en su handshake de MCP se lee de package.json en tiempo de ejecución, así que sigue automáticaamente that valor.

Al hacer push de una etiqueta v* se ejecuta [`.github/workflows/releas¿. ---

  1. verify: lint, format, typoformat, typecheck, cobertura tests, version/tag check, and mcp-publisher validate contra el registro vivo. ...

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