使用说明
Active Directory MCP
Multi-tenant Active Directory MCP Server — manage AD via AI assistants
About • Architecture • Quick Start • Tools • Configuration • Security
About
Active Directory MCP is an open-source Model Context Protocol server that lets AI assistants (Claude, Gemini CLI, ChatGPT via API, etc.) safely manage Active Directory environments.
Key features
- 10 tools covering users, groups, computers, OUs, security and the directory catalogue, plus 15 MSP prompt playbooks served through the MCP prompts protocol. One read-only
search_*and one writemanage_*per kind of object; the write tool takes anaction. Common requests are one call each: an account's details already carry its groups, primary group and OU; onboarding copies groups and OU from a model account (copy_from_user); offboarding disables, moves (move_to_ou) and optionally strips groups in one call; membership calls accept lists. - Two transports, one catalogue: stdio (
server.py) is a thin adapter over the same registry, sanitizer, name resolution and deadlines that serve Streamable HTTP via FastAPI (server_fastapi.py). Neither transport can drift from the other. An earlier FastMCP-based HTTP transport (server_http.py) has been removed. - Two deployment modes:
single(default) binds one process to one AD viaAD_MCP_CONFIG— run one process per tenant to serve several;multibinds one process to N ADs viaAD_MCP_SERVERS, adding a required/optionalad_serverparameter to every directory-bound tool. See Multi-tenant architecture. - Write-operation guard rails: every mutating tool requires either a per-tenant client confirmation string or an automation Bearer token before touching AD.
- Audit log on every operation: each call records operation name, target, mode (CONFIRMED / AUTOMATION / NO_CONFIRMATION_REQUIRED), and outcome.
Naming convention
All MCP tool names use the ad_* prefix with a descriptive suffix — e.g. ad_search_directory_users, ad_manage_directory_users, ad_search_security_reports. This avoids collisions when this MCP runs alongside other servers (GLPI, Hudu, etc.) connected to the same AI client.
Multi-tenant architecture
There are two ways to serve more than one AD with this codebase. Both read the same active_directory / organizational_units / security / client schema (see Configuration); pick one per deployment, not both.
Mode 1 — one process per tenant (AD_MCP_MODE=single, the default)
.base-code/ <- this repository (shared source of truth)
src/active_directory_mcp/
ad-config/
ad-config.example.json <- template only (real configs are .gitignored)
<deployment>/ <- one directory per tenant, OUTSIDE this repo
tenant-a/
ad-config/ad-config.json <- real credentials (NEVER committed)
start.sh <- exports AD_MCP_CONFIG and launches the server
tenant-b/
ad-config/ad-config.json
start.sh
Each start.sh exports AD_MCP_CONFIG pointing at that tenant's config and runs python -m active_directory_mcp.server_fastapi on a dedicated port. Update the shared .base-code/ once, restart every tenant process — same code, isolated state. Tools take no ad_server parameter in this mode; this is the original, unchanged behavior.
Mode 2 — one process serving several ADs (AD_MCP_MODE=multi)
A single server_fastapi.py process can instead serve N directories, selected via AD_MCP_SERVERS pointing at an ad-servers.json (schema: ad-config/ad-servers.example.json — each entry is an ad-config.json block plus label/aliases/enabled). Every directory-bound tool then gains an ad_server parameter:
- Write tools:
ad_serveris required and must name exactly one directory — there is no "all". - Read tools:
ad_serveris optional; omitted (orall/todos/*) runs the query against every configured directory and the response reports where each result was found. - Catalog tool:
ad_list_ad_servers,ad_check_ad_server_configured,ad_list_configured_clients,ad_check_client_configurationandad_get_client_tenant_infoare replaced by a singlead_search_directory_servers(same tool in both modes), whose optionalserver_nameresolves a name, alias, domain or client name to the exactad_server. Calling it first is not required:ad_serveritself accepts the client's name.
ad_server matching ignores case, accents and surrounding whitespace; a name that matches more than one directory is refused with the list of candidates rather than guessed. Write confirmation (client_confirmation, below) is validated against the same directory named by ad_server.
export AD_MCP_MODE=multi
export AD_MCP_SERVERS=/etc/ad-mcp/ad-servers.json
python -m active_directory_mcp.server_fastapi --host 0.0.0.0 --port 8820
The stdio transport (server.py) only supports single mode.
Quick Start
Prerequisites
- Python 3.11+
- LDAP/LDAPS reachable from the host
- An AD service account with the permissions required by the operations you plan to expose
1. Install
git clone https://github.com/DevSkillsIT/Skills-MCP-Active-Directory.git
cd Skills-MCP-Active-Directory
python -m venv .venv
source .venv/bin/activate # Linux/macOS
# .venv\Scripts\activate # Windows
pip install -e . # installs from pyproject.toml
2. Configure
mkdir -p /etc/ad-mcp
cp ad-config/ad-config.example.json /etc/ad-mcp/ad-config.json
$EDITOR /etc/ad-mcp/ad-config.json # set server, bind_dn, password, base_dn, OUs
chmod 600 /etc/ad-mcp/ad-config.json
The example file is the only template kept in git. Any real
ad-config.jsonis blocked by.gitignore(ad-config/*.json+!ad-config/*.example.json).
3. Run
export AD_MCP_CONFIG=/etc/ad-mcp/ad-config.json
# stdio transport (for direct Claude Desktop / mcp-cli use):
python -m active_directory_mcp.server
# HTTP transport (for Claude Code, Gemini CLI, n8n, etc.):
python -m active_directory_mcp.server_fastapi --host 0.0.0.0 --port 8820
# MCP endpoint: http://localhost:8820/mcp Health (no auth): http://localhost:8820/health
Set AD_MCP_API_TOKEN in the environment to require a Bearer token on the HTTP transport; without it the server accepts unauthenticated requests (development only — always set it in production). See HTTP_MCP_GUIDE.md for the full transport reference, including the multi mode.
4. Connect from Claude Code
claude mcp add --transport http ad http://localhost:8820/mcp \
--headers "Authorization: Bearer YOUR_AUTOMATION_TOKEN"
5. Connect from Gemini CLI
~/.gemini/settings.json:
{
"mcpServers": {
"ad": {
"httpUrl": "http://localhost:8820/mcp",
"headers": { "Authorization": "Bearer YOUR_AUTOMATION_TOKEN" },
"timeout": 30000
}
}
}
Tools
Ten tools: one read-only ad_search_* and one write ad_manage_* per kind of
object, plus the catalogue. This is the house pattern (search = discovery,
manage = action through an action parameter) and it replaced the 45 verb-per-
tool names each mode listed before: the catalogue a client has to carry on every call went
from ~60k to ~23k characters, and a model no longer has to pick between seven
near-identical user tools.
Every tool declares MCP annotations: ad_search_* is readOnlyHint: true,
ad_manage_* is destructiveHint: true (every one of them includes delete).
In multi mode each tool bound to a directory also takes ad_server
(required on writes, optional on reads — omitted means every directory).
| Tool | Type | What it does |
|---|---|---|
ad_search_directory_servers |
read | Configured ADs; server_name resolves a client name; test_connection runs a live LDAP test (one or all) |
ad_search_directory_users |
read | username returns the full account sheet (groups, primary group, OU, flags, manager); otherwise lists by free text, LDAP filter or OU; inactive_days lists accounts without logon |
ad_manage_directory_users |
write | action: create (onboarding; copy_from_user copies groups and OU), update, move, enable, disable (offboarding with move_to_ou and remove_from_groups), reset_password, delete |
ad_search_directory_groups |
read | group_name returns the group with its members (recursive expands nested groups); otherwise lists |
ad_manage_directory_groups |
write | action: create, update, move, add_members, remove_members (several groups and members per call), delete |
ad_search_directory_computers |
read | computer_name returns the machine; otherwise lists; inactive_days lists stale machines |
ad_manage_directory_computers |
write | action: create, update, move, enable, disable, reset_trust, delete (a domain controller is refused) |
ad_search_organizational_units |
read | ou_dn returns the OU with its GPOs and its contents (object_types filters); otherwise lists the hierarchy |
ad_manage_organizational_units |
write | action: create, update, move, delete (force deletes the contents too) |
ad_search_security_reports |
read | report: domain_policy, privileged_groups, admin_accounts, password_violations, user_permissions |
Every write answer is read back from the directory (DN, account state, groups actually joined), so a follow-up query to confirm it is unnecessary.
Old tool names
The 45 names this server listed before in each mode (47 distinct names across
single and multi mode) are not gone: a client connected directly (or a saved
prompt) can still call them. 45 of them are routed to the new tool with the
right action plus a tool_unificada note; ad_test_ldap_connection_status
and ad_move_organizational_unit_parent are still registered under their own
names, just no longer listed. A gateway that only
routes listed tools (mcphub) answers "tool not available" instead, and its
search then finds the new tool. The mapping lives in
ALIASES_DE_TOOLS (server_fastapi.py).
MSP prompts (15)
The 15 professional MSP playbooks (7 manager + 8 analyst) are not tools — they are served through the standard MCP prompts protocol, prompts/list and prompts/get, over both transports. See PROMPTS.md for the full catalog and usage.
Configuration
The runtime configuration file path is provided via the AD_MCP_CONFIG environment variable. Schema in ad-config/ad-config.example.json.
Key fields
| Field | Required | Description |
|---|---|---|
active_directory.server |
yes | Primary LDAP URL, e.g. ldaps://dc.example.com:636 |
active_directory.server_pool |
no | Additional LDAP URLs for failover |
active_directory.bind_dn |
yes | Full DN of the service account |
active_directory.password |
yes | Service account password (keep file at chmod 600) |
active_directory.base_dn |
yes | Base DN, e.g. DC=example,DC=com |
organizational_units.* |
yes | Default OUs for users/groups/computers/service accounts |
security.enable_tls |
no | Force StartTLS / LDAPS |
security.validate_certificate |
no | Verify server certificate against ca_cert_file |
security.require_secure_connection |
no | Refuse to bind over plaintext |
automation.token |
no | Bearer token for unattended write operations |
client.slug |
no | Tenant identifier reported by ad_search_directory_servers |
Service account permissions
Grant the bind account the minimum delegated rights required by the operations you intend to expose:
- Read-only deployments: "Read all properties" + "List contents" on the domain root is enough.
- User/group write: delegate "Create/Delete objects" + "Write all properties" on the target OUs.
- Password reset: delegate the "Reset password" extended right on the target OUs.
- Computer join/leave: delegate "Create/Delete computer objects" on the computers OU.
Always use a dedicated service account, LDAPS in production, and rotate the password regularly.
Security
Write protection model
Every write — each action of the ad_manage_* tools, and the old write names routed to them — calls check_write_permission() before reaching LDAP. It accepts the write if one of:
automation_tokenmatchesautomation.tokenin the config — intended for CI / scheduled jobs.client_confirmationmatches the tenant slug — the AI assistant must identify the tenant first (ad_search_directory_servers, in either mode), read the slug back to the user, and pass that exact string.- The tenant has
require_confirmation_for_writes: false(explicit opt-out, not recommended).
If none of the above is satisfied, the call short-circuits with a permitted: false message and the LDAP write is never attempted.
Audit logging
All operations write a structured log line including: timestamp, tool name, target, confirmation mode (AUTOMATION / CONFIRMED / WRONG_CONFIRMATION / NO_CONFIRMATION_REQUIRED), and success/failure. Logs go wherever logging.file points.
Secrets hygiene
- Real
ad-config.jsonfiles are git-ignored. Only*.example.jsonis tracked. - Never paste a config containing a real
passwordorautomation.tokeninto a chat that's logged or transcribed by a third party. - Rotate
automation.tokenwhenever you regenerate it; treat it as a privileged credential.
Testing
# Unit + integration tests
pytest tests/ -v
# Coverage
pytest --cov=src/active_directory_mcp --cov-report=term-missing
# Lint
ruff check .
A bundled docker-compose-ad.yml spins up a Samba AD container at 192.168.1.100 plus an MCP container so the integration tests can run against a real LDAP backend without touching production.
Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
LDAP bind failed |
bad bind_dn / password |
Verify against ldapsearch -H <server> -D '<bind_dn>' -W |
Insufficient permissions |
service account lacks delegated rights | Re-delegate on the target OU |
Certificate verification failed |
self-signed cert without trust | Set ca_cert_file or validate_certificate: false (test only) |
permitted: false on every write |
Missing confirmation/token | Identify the tenant first (ad_search_directory_servers), or pass automation_token |
Health degraded |
socket open but LDAP search failed | Check service-account lockout / replication / network ACLs |
Contributing
- Fork the repo.
- Create a feature branch:
git checkout -b feat/your-feature. - Run tests:
pytest. - Open a PR with a clear description and a link to the relevant issue.
Commits follow Conventional Commits.
License
MIT — see LICENSE.
Acknowledgments
- Based on the upstream
alpadalar/ActiveDirectoryMCPproject by Alperen Adalar. - Built on top of
mcp,fastmcp,ldap3, and FastAPI.
Support
- Bug reports: GitHub Issues
- Discussions: GitHub Discussions
