Skip to main content
Glama

Run an Active Directory script

execute
Destructive

Run a JavaScript script against an Active Directory domain to query, filter, join, count, or change objects; AD permissions govern results.

Instructions

Run a JavaScript script against one Active Directory domain. Use this for any read, filter, join, count, or change. Write the body of an async function and "return" the value you want back. Only what you return (and console.log) comes back to you, so filter and pick attributes inside the script. Output is capped at about 10,000 tokens.

Every call runs as the Windows user the server runs as, over Kerberos with signing and sealing, against the domain's PDC emulator, so Active Directory's own permissions decide what succeeds. Use connections_list to find domain aliases. Name objects by DN. Pass your own attributes; without them you get name, objectClass and sAMAccountName. A connection added in read mode refuses add, modify, delete, move, addAce and removeAce.

Available in the script: declare const ad: { // One object by DN, or null if there is none. Default attributes: name, objectClass, sAMAccountName. ["*"] for all. get(dn: string, attributes?: string[], opts?: { controls?: Controls }): Promise<Entry | null>; // Objects matching an LDAP filter. base defaults to the domain head, scope to "sub", max to 1000 (cap 20000). // more is true when there were more than max. Multi-valued attributes like member come back whole. search(opts: { filter?: string; base?: string; scope?: "base" | "one" | "sub"; attributes?: string[]; max?: number; controls?: Controls }): Promise<{ entries: Entry[]; more: boolean }>; // The account the server runs as, and the PDC emulator it talks to. whoami(): Promise<{ user: string; pdc: string; defaultNamingContext: string }>; // Writes. A read-mode connection refuses these four before anything is sent. add(dn: string, attributes: Record<string, Value | Value[]>): Promise<{ dn: string }>; // include objectClass modify(dn: string, changes: Array<{ op: "add" | "replace" | "delete"; attribute: string; values?: Value | Value[] }>, opts?: { controls?: Controls }): Promise<{ dn: string }>; delete(dn: string, opts?: { tree?: boolean }): Promise<{ dn: string }>; move(dn: string, to: { newParent?: string; newName?: string }): Promise<{ dn: string }>; // newName is an RDN, e.g. "CN=New Name" // Permissions. getAcl reads the owner and DACL, which needs no admin rights. A read-mode connection refuses addAce and removeAce. getAcl(dn: string): Promise<{ dn: string; owner: Principal; protected: boolean; aces: Ace[] }>; // objectType: an attribute, class or extended right name (find it with search) or a GUID. inheritedObjectType: a class. addAce(dn: string, ace: NewAce): Promise<{ dn: string }>; // Removes the entry that matches exactly; an Ace from getAcl can be passed back as it is. removed is false if none matched. removeAce(dn: string, ace: NewAce | Ace): Promise<{ dn: string; removed: boolean }>; }; // Group Policy, through Microsoft's GroupPolicy module on the PDC emulator. g is a GPO's name or id. // A read-mode connection refuses create, delete, link, unlink, set and remove. declare const gpo: { list(): Promise<Gpo[]>; // With where it is linked and every registry policy value it sets. get(g: string): Promise<Gpo & { links: Link[]; computerSettings: Setting[]; userSettings: Setting[]; securitySettings: SecuritySettings }>; create(name: string, opts?: { comment?: string }): Promise; delete(g: string): Promise<{ id: string; name: string; deleted: true }>; // Creates the link, or changes it if the GPO is already linked there. target is an OU or the domain DN. link(g: string, target: string, opts?: { enabled?: boolean; enforced?: boolean; order?: number }): Promise<{ id: string; links: Link[] }>; unlink(g: string, target: string): Promise<{ id: string; links: Link[] }>; // key starts with HKLM\ (computer) or HKCU\ (user). Find keys with search over catalogue.policies. set(g: string, s: { key: string; valueName: string; type: "String" | "ExpandString" | "DWord" | "QWord" | "MultiString"; value: string | number | string[] }): Promise; remove(g: string, key: string, valueName?: string): Promise; // Backup-GPO to a folder on the machine adslayer runs on. Allowed on a read connection. backup(g: string, path: string): Promise<{ id: string; backupId: string; path: string; timestamp: string }>; }; interface Gpo { id: string; name: string; status: string; created: string; modified: string; computerVersion: number; userVersion: number; wmiFilter: string | null } interface Link { target: string; enabled: boolean; enforced: boolean; order: number } interface Setting { key: string; valueName: string; type: string; value: unknown } // From the GPO's security template (GptTmpl.inf). Read-only for now. Empty when the GPO sets none. interface SecuritySettings { systemAccess: Record<string, number | string>; // password and lockout policy, e.g. MinimumPasswordLength, LockoutBadCount eventAudit: Record<string, number | string>; privilegeRights: Record<string, GpoPrincipal[]>; // user rights, e.g. SeInteractiveLogonRight groupMembership: Array<{ group: GpoPrincipal; members?: GpoPrincipal[]; memberOf?: GpoPrincipal[] }>; // Restricted Groups registryValues: Record<string, { type: number; value: number | string }>; // Security Options, keyed MACHINE... other: Record<string, Record<string, string>>; } interface GpoPrincipal { sid: string | null; name: string | null } // Every attribute is an array. GUIDs and SIDs are strings; other binary values are { base64 }. interface Entry { dn: string; attributes: Record<string, Array<string | { base64: string }>> } type Value = string | number | boolean | { base64: string }; interface Principal { sid: string; name: string | null } // name e.g. "CONTOSO\Helpdesk" // rights: e.g. "GenericAll", "ReadProperty", "WriteProperty", "ExtendedRight", "CreateChild", "DeleteChild", "Delete", "DeleteTree", "WriteDacl" // inheritance: "All" = this object and everything below it; "Descendents" = only below it. type Inheritance = "None" | "All" | "Descendents" | "SelfAndChildren" | "Children"; interface Ace { principal: Principal; type: "allow" | "deny"; rights: string[]; objectType?: string; inheritedObjectType?: string; inheritance: Inheritance; inherited: boolean } interface NewAce { principal: string; type: "allow" | "deny"; rights: string[]; objectType?: string; inheritedObjectType?: string; inheritance?: Inheritance } // principal: "CONTOSO\Helpdesk" or a SID // showDeleted: see and restore objects in the Recycle Bin (CN=Deleted Objects). Restore = modify with it. interface Controls { showDeleted?: true } // Calls may run in parallel with Promise.all. Each run may make at most 200 calls.

Examples: // Enabled users in an OU, by name const r = await ad.search({ base: "OU=Sales,DC=contoso,DC=local", filter: "(&(objectCategory=person)(objectClass=user)(!(userAccountControl:1.2.840.113556.1.4.803:=2)))", attributes: ["sAMAccountName"] }); return r.entries.map(e => e.attributes.sAMAccountName[0]);

// How many members a group has, nested groups not expanded const g = await ad.get("CN=Helpdesk,OU=Groups,DC=contoso,DC=local", ["member"]); return g?.attributes.member?.length ?? 0;

// Disable a user (514 = normal account + disabled) return await ad.modify("CN=Jane Doe,OU=Sales,DC=contoso,DC=local", [{ op: "replace", attribute: "userAccountControl", values: "514" }]);

// Reset a password. unicodePwd takes the password in double quotes as UTF-16LE bytes. Add pwdLastSet "0" to force a change at next sign-in. const pw = '"' + newPassword + '"'; let b = ""; for (let i = 0; i < pw.length; i++) { const c = pw.charCodeAt(i); b += String.fromCharCode(c & 255, c >> 8); } return await ad.modify("CN=Jane Doe,OU=Sales,DC=contoso,DC=local", [{ op: "replace", attribute: "unicodePwd", values: { base64: btoa(b) } }, { op: "replace", attribute: "pwdLastSet", values: "0" }]);

// Restore a deleted user from the Recycle Bin to where it was. Delete isDeleted first, then set the new DN. const controls = { showDeleted: true }; const gone = (await ad.search({ base: "CN=Deleted Objects,DC=contoso,DC=local", filter: "(&(isDeleted=TRUE)(sAMAccountName=jdoe))", attributes: ["lastKnownParent", "msDS-LastKnownRDN"], controls })).entries[0]; const to = "CN=" + gone.attributes["msDS-LastKnownRDN"][0] + "," + gone.attributes.lastKnownParent[0]; return await ad.modify(gone.dn, [{ op: "delete", attribute: "isDeleted" }, { op: "replace", attribute: "distinguishedName", values: to }], { controls });

// Who can reset passwords in an OU: full control, all extended rights, or the Reset Password right const acl = await ad.getAcl("OU=Sales,DC=contoso,DC=local"); return acl.aces.filter(a => a.type === "allow" && (a.rights.includes("GenericAll") || (a.rights.includes("ExtendedRight") && (!a.objectType || a.objectType === "User-Force-Change-Password")))).map(a => a.principal.name ?? a.principal.sid);

// Password and lockout policy for the domain, from the Default Domain Policy's security settings const sa = (await gpo.get("Default Domain Policy")).securitySettings.systemAccess; return { minLength: sa.MinimumPasswordLength, lockoutAfter: sa.LockoutBadCount };

// Protect an OU from accidental deletion, as the admin tools do. AD allows a delete with Delete on the object or // DeleteChild on its parent, so deny both. To undo, removeAce the first; the parent's deny also protects its other children. await ad.addAce("OU=Sales,DC=contoso,DC=local", { principal: "S-1-1-0", type: "deny", rights: ["Delete", "DeleteTree"] }); return await ad.addAce("DC=contoso,DC=local", { principal: "S-1-1-0", type: "deny", rights: ["DeleteChild"] });

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
codeYesBody of an async JavaScript function. Use await ad.* and return a value.
domainYesConnection alias or the domain's DNS name. See connections_list.

Output Schema

TableJSON Schema
NameRequiredDescriptionDefault
okYes
logsYes
callsYes
errorNo
domainYes
resultNo
truncatedYes

Schema Changelog

Changes observed during successful MCP inspections.

  1. First observedv0.2.0

TDQS

A4.6/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Far exceeds the annotations: it discloses the auth model (runs as the server's Windows user over Kerberos with signing and sealing), the target endpoint (PDC emulator), that AD's own ACLs decide success, the default attribute set, read-mode write refusal, the ~10,000-token output cap, the 200-call-per-run limit, and that calls may run in parallel via Promise.all. Annotations (readOnly=false, destructive=true, openWorld=true, idempotent=false) are fully consistent with this and the description adds the operational context they omit.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

It is long, but front-loaded: purpose and scope first, then execution model, then the full `ad`/`gpo` API reference and worked examples. The length is defensible because the tool is an arbitrary-code surface with a large implicit API, yet some per-method commentary duplicates what the sibling `docs` tool likely holds.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

For a code-execution tool with an output schema and no nested params, nothing an agent needs is missing: it covers identity/permissions, targeting, defaults, limits, the full ad/gpo surface with types, and concrete examples including password reset, recycle-bin restore, ACL inspection and deny-ACE protection.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema coverage is 100% and both parameters are already documented there, so the baseline is 3. The description goes beyond it by defining the contract of `code` (body of an async function, only the returned value and console.log come back, filter and pick attributes inside the script) and by clarifying the `domain` value against connections_list, which meaningfully shapes how the parameter must be written.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The opening sentence pins a specific verb and resource ('Run a JavaScript script against one Active Directory domain') and then enumerates the scope of work it covers (read, filter, join, count, change), so an agent knows this is a general-purpose AD execution surface rather than a single operation. The title and body align, and no sibling could be mistaken for it once this is read.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines4/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

It gives clear when-to-use guidance ('Use this for any read, filter, join, count, or change') and routes the agent to connections_list for domain aliases, plus a meaningful exclusion ('A connection added in read mode refuses add, modify, delete, move, addAce and removeAce'). It never explicitly contrasts itself with the sibling `search` or `docs`, so an agent could still wonder whether a simple lookup belongs here or in `search`.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.