Skip to main content
Glama

Run an Active Directory script

execute
Destructive

Run custom JavaScript against an Active Directory domain to query, filter, count, or change objects and Group Policy, returning only what your script returns.

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, remove, grant, revoke and setSecurity. 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 }>; // Security settings (GptTmpl.inf, ADR-0011). Each changes one thing and answers with what was there before. // A GPO that defines a user right REPLACES the whole list on the computers it applies to. So granting on a right // the GPO doesn't define yet (defined: "new") leaves only that principal: grant everyone who should keep it. // Revoking the last principal undefines the right (defined: false); computers keep their last list until another GPO sets it. grant(g: string, right: string, principal: string): Promise; // right e.g. "SeServiceLogonRight"; principal "CONTOSO\svc-web" or a SID revoke(g: string, right: string, principal: string): Promise; setSecurity(g: string, section: "System Access", key: string, value: number | string): Promise; // e.g. "MinimumPasswordLength", 14 setSecurity(g: string, section: "Registry Values", key: string, value: { type: 1 | 2 | 4 | 7; value: number | string | string[] }): Promise; // key MACHINE... }; 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). 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 } interface RightChange { id: string; right: string; principal: GpoPrincipal; changed: boolean; defined: boolean | "new"; before: GpoPrincipal[]; after: GpoPrincipal[] } interface SecurityChange { id: string; section: string; key: string; changed: boolean; before: unknown; after: unknown } // 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 };

// Let a service account log on as a service through a GPO. If the GPO doesn't define the right yet, granting // defines it with only this account on every computer it applies to, so read who holds it first. const before = (await gpo.get("Web Servers")).securitySettings.privilegeRights.SeServiceLogonRight; return await gpo.grant("Web Servers", "SeServiceLogonRight", "CONTOSO\svc-web"); // { changed, defined, before, after }

// 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

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

NameRequiredDescriptionDefault
okYes
logsYes
callsYes
errorNo
domainYes
resultNo
truncatedYes

Schema Changelog

Changes observed during successful MCP inspections.

  1. First observedv0.2.0

TDQS

A4.8/5.0
Behavior5/5

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

Annotations only declare non-readonly, destructive and open-world; the description goes far beyond that with execution context (runs as the server's Windows user over Kerberos with signing and sealing, against the PDC emulator), result throttling (~10,000 tokens output cap, 200 calls per run, max 1000/cap 20000 search results), default attribute behavior, and read-mode write refusals. It also documents side effects per operation (grant replacing a whole user-right list, delete semantics, Recycle Bin restore requiring showDeleted).

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?

The operative instruction is front-loaded in the first two sentences, and every block (API surface, GPO module, interfaces, examples) maps to something an agent must know to write correct code. It is nonetheless very long with eight worked examples, so it leans dense rather than waste-free.

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, the description supplies the full API surface, TypeScript types, module split (ad/gpo), write-refusal rules, and worked examples covering read, write, restore, ACL, and GPO cases. Nothing an agent needs to author a correct script is missing.

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%, so the baseline is 3, but the description adds real meaning: the code parameter must be an async function body whose return value (plus console.log) is the only thing returned, and results should be filtered inside the script. The domain parameter's alias lookup is restated rather than extended, which keeps this just short of a 5.

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 first sentence states a specific verb and resource — run a JavaScript script against one Active Directory domain — and immediately scopes what it is used for (any read, filter, join, count, or change). It also distinguishes itself from nearby siblings by pointing to connections_list for aliases and docs/search for other lookups, so an agent can route without opening schemas.

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

Usage Guidelines5/5

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

Explicit guidance on when to use it ('any read, filter, join, count, or change'), where to find the domain argument (connections_list), and a hard exclusion (a read-mode connection refuses add, modify, delete, move, addAce, removeAce). It even states the privilege model — AD's own permissions decide what succeeds — which tells the agent how to interpret failures.

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