tie-mcp-server
The tie-mcp-server provides a comprehensive MCP interface to the Tenable Identity Exposure (TIE) API, enabling management and monitoring of Active Directory security through 130+ tools.
Identity & Access Management
Manage AD objects, users, roles, profiles, and authentication (API keys, LDAP configuration, lockout policies)
Handle user creation defaults and role permissions
Infrastructure & Directory Management
Full CRUD for infrastructures and directories
View topology via the
get_topologyconvenience tool (infrastructure→directory hierarchy)
Attack & Deviance Monitoring
List, search, export, and update attacks and deviances
Manage attack types, checker options, categories, and reasons
Export checker data as CSV
Alert & Notification Management
View and bulk-update security alerts per profile
Full CRUD for email notifiers; send test notifications
Event Management
Retrieve and search events by directory or infrastructure
Get AD object changes and deviance changes between events
Configuration & Settings
Manage global application settings (SMTP, TLS, telemetry, user registration)
Configure LDAP integration, lockout policies, license, and dashboards (full CRUD)
Get and update user preferences; retrieve preferred profile via
get_preferred_profile
Reporting & Metrics
Export attack data, collect health metrics, and get cloud statistics
Get and refresh Tenable Cloud report access tokens
Security & Multi-Tenancy
Apply granular tool-level filtering based on operation safety (read, write, destructive)
Support multi-tenant deployments with distinct API credentials and base URLs per environment
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@tie-mcp-serverlist all active directory users"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
TIE MCP Server
Model Context Protocol (MCP) server for Tenable Identity Exposure API.
Features
Complete coverage of all 131 TIE API operations (one MCP tool per endpoint)
Additional convenience tools for discovery, navigation, in-memory AD object search, permission decoding, control-graph attack-path analysis, and a per-identity deviance 360 view (9 custom tools)
Client-side credential management for security
Multi-tenant support (multiple TIE environments)
Granular tool-level security controls
Auto-generated tool definitions from the OpenAPI specification
Related MCP server: cipp-mcp
Why an MCP server instead of raw API calls?
A thin "call the endpoint" wrapper would just hand the raw TIE API to the model, inheriting the API's shape and its rough edges. This server adds a layer that turns the API into something an LLM can use effectively:
In-memory AD object search over a cached snapshot. The TIE API has no server-side filter on
/api/ad-objects— the only way to find an object by attribute is to page through the entire directory (tens of thousands of objects, dozens of requests). Done naively through the model, that means fetching megabytes of JSON into the context window and paging again on every question. Instead,query_ad_objectsscans the directory once, builds a typed in-memory index, and answers expression queries against it in a few milliseconds — reused for ~10 minutes so follow-up questions are effectively free. A lookup that otherwise takes ~50 sequential API calls (and can't even be expressed as a filter) becomes one tool call. See AD object search.A real query language. Attributes are decoded into typed values (numbers, booleans, arrays) so
admincount>0,badpwdcount>=5, andmember:"dcadmin"mean what you'd expect, and can be combined withAND/OR/NOT. The API returns everything as strings and offers no way to combine conditions at all.Security-descriptor decoding. AD permissions live in the
ntSecurityDescriptoras a dense SDDL string (O:...G:...D:(A;;CCDC...;;;S-1-...)) that is unreadable to a model and useless for reasoning.get_ad_objectcan decode it on request into structured ACEs with trustee SIDs resolved to names (from the resident snapshot), rights named (GenericAll,WriteDacl,ForceChangePassword…), and object-types resolved via the live schema — turning "who can control this object?" into a readable answer instead of a 15 KB blob. See Permission decoding.Cross-object attack-path analysis. Because the whole directory is resident, the server can build a control graph and answer questions no per-object API can: blast radius ("what can this account reach?"), shortest control paths ("how does X become Domain Admin?"), and asset exposure ("who can reach my Tier-0?"). Edges come from group membership, ACL rights, delegation, SID history and more. See Attack-path analysis.
Per-identity deviance 360. Tenable files each Indicator-of-Exposure deviance against one object, so its own per-object view misses the cases where an identity is the risky trustee buried inside another object's finding, or inherits exposure from a container it sits under.
get_identity_360resolves all three layers (target / trustee / inherited), joins in severity and remediation bands, and deep-links back into the Tenable UI — the exposure picture the console can't assemble on its own. See Identity 360.Composed discovery tools.
get_topologyandget_preferred_profileanswer "what forests/domains exist?" and "which profile should I use?" in one call each, instead of the model having to stitch together/infrastructures,/directories, and/preferencesand infer how they relate.Context-window economy. The model queries and gets back only matching objects (with a reported total and an optional
limit), rather than ingesting the whole directory to filter it itself.Guardrails. Per-endpoint tools mean the client can allow/deny by name, and
TIE_ALLOWED_SAFETYcan advertise only read (or read+write) tools — a granularity you don't get by exposing one generic "call any endpoint" tool.
Example use cases
Because search runs in memory, questions that would be impractical as ad-hoc API
paging become single natural-language asks. A few that map directly onto
query_ad_objects expressions:
Privileged accounts with weak hygiene — "admins that have had bad password attempts":
admincount>0 AND badpwdcount>0.Kerberoast exposure — "user accounts that have an SPN set":
serviceprincipalname:"/" AND type=LDAP(any SPN contains a/).Weak / breached passwords among the privileged — "privileged accounts with a breached password":
admincount>0 AND isbreached=true, or a weak password under any profile's policy:admincount>0 AND isweak=true. (These come from TIE's password-hash analysis, joined onto the principal — see Credential weakness.)Stale but enabled accounts — combine
enabled=truewith alastlogontimestampbound to surface dormant-yet-active identities.Delegation risk sweep — "accounts trusted for delegation":
useraccountcontrol:"TRUSTED_FOR_DELEGATION".Fast pivot on one object — "show me everything about Domain Admins":
get_ad_object({ samAccountName: "Domain Admins" }), then follow itsmemberlist into further queries — all served from the same cached snapshot.
Prompts that combine capabilities
The real leverage is chaining search, permission decoding, the control graph, and credential weakness in one line of questioning. Natural-language prompts an orchestrator can answer by composing these tools:
"Which non-privileged users can reach Domain Admins, and how?" —
get_asset_exposurefrom the Tier-0 preset (reverse), each with its shortest control path."If
bobis phished, what's the blast radius?" —get_blast_radius({ principal: "bob" }); follows group membership, ACL control, delegation, SID history, GPO, and password reuse."What's our true Tier-0 — everyone who can become admin, not just group members?" —
get_tier0, which returns built-in privileged groups plus de-facto Tier-0 with the escalation path for each."Who can DCSync the domain, and does any of them have a weak password?" —
query_ad_objects("isweak=true")intersected with the DCSync-capable principals surfaced byget_tier0/ a path to the domain node."Show me the shortest way
helpdeskcould take over theAdministratoraccount." —get_control_paths({ from: "helpdesk", to: "Administrator" }); paths run through domain compromise when that's the real route."Who has dangerous rights on the Domain Admins object?" —
get_ad_object({ samAccountName: "Domain Admins", decodeSecurityDescriptor: true })to read the ACEs with trustees and rights resolved to names."Which admins have a breached OR reused password AND a path to a domain controller?" —
query_ad_objects("admincount>0 AND isbreached=true"), thenget_blast_radiusfrom each — reused-password clusters and the domainControlsedge do the reachability."Find accounts that are kerberoastable, privileged, and weak all at once." —
query_ad_objects('admincount>0 AND serviceprincipalname:"/" AND isweak=true')— the drop-everything findings, in one expression."What's the full exposure picture for
jaime.lannister— everything Tenable flags on him, everywhere he's the risky party, sorted worst-first?" —get_identity_360({ samAccountName: "jaime.lannister" }); returns his own deviances plus the ones where he holds dangerous rights over other objects plus what he inherits from his containers, each with a Tenable deeplink."For these 5 users on an attack path, show me each one's Critical/High/Medium/ Low deviance counts." —
get_identity_360_summary({ identities: [...] })in a single call for the badges, then drill into any one withget_identity_360.
Permission decoding
ntSecurityDescriptor is where AD stores who-can-do-what, but it arrives as raw
SDDL — a wall of ACE tokens and SIDs that no model can reason over. Pass
decodeSecurityDescriptor: true to get_ad_object to get it back as structured,
resolved facts:
get_ad_object({ samAccountName: "Domain Admins", decodeSecurityDescriptor: true })
// -> securityDescriptor:
{
"owner": { "sid": "S-1-5-...-512", "name": "Domain Admins" },
"aces": [
{ "effect": "Allow", "trustee": { "sid": "S-1-1-0", "name": "Everyone", "broad": true },
"rights": ["ReadProperty"], "appliesTo": null, "inherited": true },
{ "effect": "Allow", "trustee": { "sid": "S-1-5-...-1105", "name": "bob.shaft", "broad": false },
"rights": ["ControlAccess"], "appliesTo": "DS-Replication-Get-Changes-All" }
]
}What the decoder does that a raw string can't:
Resolves trustee SIDs to names using the in-memory snapshot (every object's SID is indexed), and flags broad principals (
Everyone,Authenticated Users,Anonymous) withbroad: true.Names the rights —
GenericAll,WriteDacl,WriteOwner,ForceChangePassword,AddMember, etc. — and collapses the full-control token run toGenericAll.Resolves object-type GUIDs via the live schema already in the store (e.g. the two replication rights that together make up DCSync), so extended rights and attributes show up by name.
Distinguishes Allow / Deny, marks inherited ACEs, and never throws on a malformed descriptor.
This is deliberately facts, not verdicts — it reports who has which right, and leaves severity to you or to Tenable's Indicators of Exposure. It is also the foundation for the control-graph analysis below; see docs/CONTROL_GRAPH_DESIGN.md.
Credential weakness
TIE runs a password-hash analysis and emits a passwordHashScan companion object
per analyzed principal. The server joins that signal back onto the principal (by
distinguished name), so credential weakness is directly queryable:
isbreached— password appears in a breached-password set.islmblank/isntblank— blank LM / NT hash.isweak— derived boolean: weak under at least one profile's policy. The underlying signal is per-profile (a TIE profile is a configuration lens), and a password is "weak" when it matches a configured weak/dictionary password or is empty / equals the samAccountName. The raw per-profile breakdown is preserved asisweakByProfile({ profileId: bool }) for finer questions.
query_ad_objects({ expression: "isweak=true AND admincount>0" }) // weak-password admins
query_ad_objects({ expression: "isbreached=true" }) // breached passwords
// weak specifically under profile 2 (fields are case-insensitive; the raw map
// is matched as a substring):
query_ad_objects({ expression: 'isweakByProfile:"\\"2\\":true"' })Facts, not verdicts — TIE's Indicators of Exposure already score these findings; this just makes them queryable alongside every other attribute (and, in a planned step, usable as attack-path entry points).
Attack-path analysis (control graph)
A single per-object question ("is this misconfigured?") is what Indicators of Exposure answer. The harder, cross-object question — "by chaining permissions and group memberships, what can this account ultimately reach, and who can reach my crown jewels?" — is a graph problem. From the resident snapshot the server builds a directed control graph whose edges come from both plain attributes and decoded SDDL:
membership (
MemberOf, includingprimaryGroupIDwhichmemberomits),GenericAll/GenericWrite/WriteDacl/WriteOwner,AddMember,ForceChangePassword,AddKeyCredentialLink(shadow creds),DCSync(both replication rights, scoped to the domain head), constrained delegation & RBCD,SIDHistory,Contains(container → child) andGpoAppliesTo(GPO → linked OU), so GPO-based control chains down to affected objects, andControls(domain → in-domain principal): the "domain compromise owns everything in the domain" primitive, synthesized at query time so paths continue through a domain takeover to a specific target. Example:unpriv → owns GPO → GpoAppliesTo OU → Contains user → DCSync → domain → Controls → Administrator, andReusedPassword(principal ↔ shared-password hub): principals sharing a password hash (from TIE's reuse analysis) cluster on a hub node, so compromising one reaches everyone who shares the credential.
Three tools traverse it — all shortest-path (BFS), depth/breadth-capped, and cycle-safe:
// Forward: if this account is compromised, what can it reach?
get_blast_radius({ principal: "bob", maxDepth: 6 })
// Targeted: how can X reach Y? Returns the edge chain.
get_control_paths({ from: "bob", to: "Domain Admins" })
// -> bob -MemberOf-> Helpdesk -GenericAll-> dcadmin -MemberOf-> Domain Admins
// Reverse: who can reach this asset / the Tier-0 set?
get_asset_exposure({}) // Tier-0 preset (privileged groups)
get_asset_exposure({ targets: ["CN=FileServer01,..."] })
// Derived Tier-0: privileged groups PLUS everyone who can become privileged,
// each with its escalation path. "What is my true Tier-0 attack surface?"
get_tier0({})Notes and honest limits:
Directory-control edges only. This is the credential-less slice — it does not include logon sessions or local-admin (those need live host collectors, à la BloodHound). A path here is a directory-control path, so a 0-path result means "no control path", not "no attack path of any kind".
Facts, not verdicts. Results report reachability and the exact edge chain, never a severity score.
Guardrails.
maxDepth(default 6) andmaxNodesbound traversal, and a hit cap is reported astruncated: "depth" | "nodes"— never silently, since under-reporting in a security tool is dangerous.On-demand build. The graph is built from the in-memory snapshot with no extra API calls. Set
TIE_BUILD_GRAPH=trueto build it in the background at startup; otherwise the first graph query builds it (and may take tens of seconds on a large tenant). See docs/CONTROL_GRAPH_DESIGN.md.
Identity 360 (deviance view)
Indicators of Exposure are how Tenable flags misconfigurations, but each deviance
is filed against exactly one object — usually the victim. That makes a
"what is this identity exposed to?" question surprisingly hard to answer from the
console, because the same identity can be dangerous or exposed in three different
ways that the per-object view never unifies. get_identity_360 resolves all
three layers for one object:
target— deviances Tenable filed directly on the object (its own view).trustee— deviances where the object is the risky principal embedded in another object's finding (e.g. the trustee inside a Dangerous-ACE list on a victim, or on a root partition). These never appear under the object in Tenable, because the deviance belongs to the victim.inherited— deviances on a container / partition the object sits under, whose exposure inherits down; resolved by walking up the control graph'sContainsedges (builds the graph on first use).
Each returned deviance is self-contained and enriched: checker + reason
metadata, the severity (raw O-CRITICITY and the Critical/High/Medium/Low
band), the remediation cost (raw and Low/Medium/High band), the related
counterpart object (for trustee/inherited layers), granted rights, resolved/
ignored state, and a deeplink into the Tenable IOE page plus a
deeplinkFilterHint (id:"<adObjectId>") to narrow the on-page filter to the
exact object. Results are sorted worst-first (severity band, then raw criticity,
then remediation cost).
// Full, sorted, deep-linked findings for one identity (all three layers).
get_identity_360({ samAccountName: "jaime.lannister" })
// Batch roll-up: per-identity counts by severity band (for report badges),
// NO full lists. Mix DN / SID / samAccountName / objectId; unresolved inputs
// come back flagged rather than failing the batch.
get_identity_360_summary({ identities: ["jaime.lannister", "S-1-5-...", 44656] })Notes:
Severity is profile-specific.
O-CRITICITYandO-ENABLEDare per-profile (and can be overridden per-directory); omitprofileIdto use your preferred profile. Deviances from checkers disabled in the profile/directory are excluded by default and reported in a separatesummary.suppressedtally — so nothing is silently dropped.Cached deviance index. All deviances are scanned once (profile-scoped) and cached behind the same TTL as the object snapshot;
refresh: trueforces a rescan. The summary and detail tools share the index, so a badge fromget_identity_360_summaryalways matches the expandedget_identity_360view.Facts, not verdicts. The tools translate Tenable's own numbers into bands and surface the relationships; they don't re-score risk.
Freshness and caching
The snapshot is cached for 1 day by default (TIE_CACHE_TTL_MS to change
it) and is not live — it reflects the directory as of the last scan. This is
deliberate: a full scan is expensive, and AD/TIE state changes slowly relative to
a working session, so cheap reuse is the right default. Consequences to know:
Every query/lookup response includes a
snapshotblock reporting the cache's objectcount,ageMs, andttlMs, and both tool descriptions tell the model the data may be stale — so it can decide when currency matters.To force current data, pass
refresh: trueonquery_ad_objectsorget_ad_object. The classic trap: run a query, fix something in TIE, re-query, and see the old data because you didn't refresh.When the TTL lapses, the next query transparently rescans (no action needed).
Startup warming (on by default): the full directory scan can take tens of seconds to ~100s on a large tenant. To keep that off the critical path, the server warms the snapshot in the background at startup — after
connect(), so it never delays startup, and a query arriving mid-scan simply joins the in-flight build (no double scan). By the time you run your first search the cache is usually already warm. Two further points:
Progress notifications. If the MCP client attaches a
progressTokento a call that does trigger a scan, the server emitsnotifications/progressonce per fetched page (e.g. "Scanning AD objects: 12000 loaded (12 pages)"), so a long scan isn't silent. Clients that don't request progress simply see one longer tool call.Disabling warming. Set
TIE_WARM_CACHE=falseto skip the startup scan — useful on a tenant you never search, or to reduce load when running many server instances. The snapshot then builds lazily on the first query instead. A failed background warm (e.g. TIE unreachable at startup) is caught and logged, and also falls back to lazy build — it never crashes the server.
Installation
The server runs as a local subprocess of your MCP client and communicates over stdio. Choose one of the following.
Option A — npx (recommended, once published)
No local clone or build. Reference it directly from your MCP client config (see Configuration):
{ "command": "npx", "args": ["-y", "tie-mcp-server"] }Option B — from source
git clone <repo-url> tie-mcp-server
cd tie-mcp-server
npm install # also builds via the `prepare` script
npm run build # (re-run after any source change)Then point your client at the built entry point, e.g.
node /absolute/path/to/tie-mcp-server/build/index.js.
Option C — Docker
A multi-stage Dockerfile is provided for users who prefer not to install
Node locally. Build the image:
docker build -t tie-mcp-server .Because the server speaks MCP over stdio, the container must be run
interactively (-i) with credentials passed as environment variables:
{
"command": "docker",
"args": [
"run", "-i", "--rm",
"-e", "TIE_BASE_URL",
"-e", "TIE_API_KEY",
"tie-mcp-server"
],
"env": {
"TIE_BASE_URL": "https://customer.tenable.ad",
"TIE_API_KEY": "your-api-key-here"
}
}Note: This is a per-user local tool using stdio transport, so Docker Compose is not applicable (there is no long-running network service to orchestrate). Docker is offered only to bundle the Node runtime. If you need a centrally-hosted, multi-client gateway, that requires switching to MCP's HTTP/SSE transport first — see Hosting as a shared service.
Configuration
The MCP server requires two environment variables:
TIE_BASE_URL- Your TIE instance URL (e.g.,https://customer.tenable.ad)TIE_API_KEY- Your TIE API key
Optional environment variables:
TIE_ALLOWED_SAFETY- Comma-separated safety tiers to advertise (read,read,write); see Server-side safety filter.TIE_WARM_CACHE- Build the AD-object search snapshot at startup instead of on first query. On by default; set tofalseto disable (see AD object search).TIE_CACHE_TTL_MS- How long the AD-object snapshot stays fresh, in ms (default86400000, i.e. 1 day). Lower it to trade scan cost for freshness.TIE_BUILD_GRAPH-trueto build the control graph (attack-path / blast- radius / asset-exposure edges) in the background after the snapshot warms. Off by default; adds CPU + memory. Query tools land in a later release.TIE_TIMEOUT- Per-request timeout in ms (default30000).TIE_MAX_RETRIES- Max request retries (default3).
Single Environment Setup
Add to your MCP client configuration (e.g., ~/.claude/settings.json):
{
"mcpServers": {
"tie": {
"command": "node",
"args": ["/path/to/tie-mcp-server/build/index.js"],
"env": {
"TIE_BASE_URL": "https://customer.tenable.ad",
"TIE_API_KEY": "your-api-key-here"
}
}
}
}Multiple Environments Setup
For multiple TIE environments, add multiple server instances:
{
"mcpServers": {
"tie-prod": {
"command": "node",
"args": ["/path/to/tie-mcp-server/build/index.js"],
"env": {
"TIE_BASE_URL": "https://prod.tenable.ad",
"TIE_API_KEY": "prod-key"
}
},
"tie-staging": {
"command": "node",
"args": ["/path/to/tie-mcp-server/build/index.js"],
"env": {
"TIE_BASE_URL": "https://staging.tenable.ad",
"TIE_API_KEY": "staging-key"
}
}
}
}Development
# Install dependencies
npm install
# Regenerate tool definitions from the OpenAPI spec (writes src/generated/tools.ts)
npm run generate:tools
# (Optional) Generate TypeScript API types from the OpenAPI spec
npm run generate:client
# Build the project
npm run build
# Watch mode for development
npm run watch
# Run without building (development)
npm run dev
# Type checking
npm run typecheck
# Linting
npm run lintSecurity
Client-Side Credentials
Credentials are never stored in the MCP server code. They must be configured on the client side (MCP client config) and passed as environment variables to the server process.
Tool-Level Filtering
Organizations can filter tools by operation type for granular security control:
{
"mcpServers": {
"tie": {
"command": "node",
"args": ["/path/to/tie-mcp-server/build/index.js"],
"env": {
"TIE_BASE_URL": "https://customer.tenable.ad",
"TIE_API_KEY": "key"
},
"allowedTools": ["get_*", "list_*", "search_*"],
"deniedTools": ["delete_*"]
}
}
}Tool categories by risk level (the safety field on each generated descriptor):
🟢 read (70 tools):
get_*,list_*,search_*,export_*🟡 write (51 tools):
create_*,update_*,set_*, plus actions likecommit_*,login🔴 destructive (10 tools):
delete_*,unstage_*
Server-side safety filter
Beyond the client's allowedTools/deniedTools, the server itself honors a
TIE_ALLOWED_SAFETY environment variable. Set it to a comma-separated list of
tiers to advertise only those tools — e.g. read for a strictly read-only
deployment, or read,write to disable destructive operations entirely:
{
"mcpServers": {
"tie-readonly": {
"command": "node",
"args": ["/path/to/tie-mcp-server/build/index.js"],
"env": {
"TIE_BASE_URL": "https://customer.tenable.ad",
"TIE_API_KEY": "key",
"TIE_ALLOWED_SAFETY": "read"
}
}
}
}Architecture
Rather than hand-writing a handler per endpoint, tool definitions are generated from the OpenAPI spec into a single data file, and one generic dispatcher turns any descriptor + arguments into an HTTP request.
scripts/
└── generate-tools.mjs # Parses the OpenAPI spec -> src/generated/tools.ts
src/
├── index.ts # MCP server: registers tools, routes calls
├── config.ts # Environment configuration
├── client.ts # HTTP client for TIE API (axios)
├── dispatch.ts # Generic descriptor -> HTTP request dispatcher
├── custom-tools.ts # Hand-written convenience/discovery tools
└── generated/ # Auto-generated — do not edit by hand
└── tools.ts # 131 ToolDescriptor entries (name, method, path, schema)Generated vs Custom Tools
Generated tools (src/generated/tools.ts): One-to-one mappings of TIE API
endpoints. Regenerate whenever the TIE API spec changes:
npm run generate:toolsCustom tools (src/custom-tools.ts): Hand-written convenience tools that
compose multiple API calls or provide discovery/navigation helpers. These survive
regeneration and are merged with generated tools at server startup.
Current custom tools:
get_topology- Returns Infrastructure→Directory hierarchy treeget_preferred_profile- Returns user's default profile from preferencesquery_ad_objects- Search all AD objects with a filter expression, run in memory over a cached snapshot (see AD object search)get_ad_object- Look up a single AD object by DN, SID, or SAM account name
Custom tools follow the same CustomTool interface ({name, description, category, safety, inputSchema, handler}) and are dispatched alongside generated tools.
Available Tools
The server exposes 139 tools total:
131 generated tools from
src/generated/tools.ts(one per TIE API endpoint)8 custom tools from
src/custom-tools.ts(get_topology,get_preferred_profile,query_ad_objects,get_ad_object,get_blast_radius,get_control_paths,get_asset_exposure,get_tier0)
See TOOL_NAMING_CONVENTION.md for the naming scheme and the (historical) 88-endpoint reference list.
Discovery Tools
// Get user's preferred profile (from preferences)
get_preferred_profile()
// Returns: { preferredProfileId: 2, preferredProfileName: "Contoso" }
// Get infrastructure→directory topology tree
get_topology()
// Returns: [{ id, name, directories: [{id, name, status}] }]AD object search
query_ad_objects and get_ad_object solve a real limitation: the TIE API has
no server-side filter on /api/ad-objects, so the only way to find objects by
attribute is to page through the entire directory (tens of thousands of objects).
These tools do that scan once, build a typed in-memory snapshot, and reuse it
(TTL-cached, default 10 minutes) so subsequent searches run in a few milliseconds
without re-paging.
// Filter expression: FIELD OP VALUE, combined with AND / OR / NOT and parentheses.
query_ad_objects({
expression: '(admincount>0 AND useraccountcontrol:"NORMAL") OR badpwdcount>=5',
limit: 50, // optional; 0 = all. Total match count is always reported.
})
// Single-object lookup by DN, SID, or SAM account name.
get_ad_object({ distinguishedName: "CN=Domain Admins,CN=Users,DC=alsid,DC=corp" })
get_ad_object({ sid: "S-1-5-21-...-512" })
get_ad_object({ samAccountName: "Domain Admins" })Operators
Operator | Meaning |
| Equality / inequality — numeric when both sides are numbers, else case-insensitive string |
| Ordering — numeric when both numeric, else lexical (case-insensitive) |
| Contains — substring for strings, membership for multi-valued attributes |
| Numeric bitwise test: |
| Boolean combinators; precedence |
Fields are attribute names (case-insensitive), e.g. admincount, cn,
member, useraccountcontrol, isbreached, plus the identity fields type
(LDAP/SYSVOL), directoryId, objectId, id. Quote values containing
spaces: cn:"Domain Admins". Multi-valued attributes match if any value
matches; a missing attribute never matches. Pass refresh: true to force a fresh
scan.
Note:
useraccountcontrolis exposed as decoded flag names ("NORMAL DONT_EXPIRE"), not the raw integer bitmask — test flags with the:contains operator (e.g.useraccountcontrol:"DONT_EXPIRE"), not&.
Generated Tool Examples
// Get system information
get_about()
// List attacks for a profile
list_attacks({ profileId: "profile-123" })
// Search events
search_events({
query: { /* search criteria */ }
})
// Create infrastructure
create_infrastructure({
name: "Production",
description: "Production environment"
})
// Update deviance
update_deviance({
infrastructureId: "infra-1",
directoryId: "dir-1",
devianceId: "dev-1",
data: { status: "resolved" }
})Hosting as a shared service
This server currently uses stdio transport: the MCP client spawns it as a local child process. That model is correct for a per-user desktop tool and is why distribution is via npm/npx (or a plain Docker image), not Docker Compose.
To instead run one centrally-hosted instance that many remote clients
connect to, the server would need to switch to MCP's streamable HTTP/SSE
transport (replacing StdioServerTransport in src/index.ts with the HTTP
server transport and adding a listening port). At that point it becomes a
standing network service, and Docker — optionally with Compose behind a reverse
proxy for TLS and authentication — becomes the appropriate deployment. This is
a deliberate, separate change and is not implemented today.
API Documentation
License
MIT — see LICENSE.
Available Tools
133 toolschange_passwordC
Update a user's password
Inputs: body (required).
clone_profileB
Creates a new profile from another one
Inputs: body (required), fromId (required).
clone_roleC
Creates a new role from another one
Inputs: body (required), fromId (required).
commit_profileD
Commits change of the related profile
Inputs: id (required).
create_api_keyB
Create or renew an API key for the current user
Inputs: no named parameters.
create_attack_type_optionA
Create attack type options related to a profile and attack type. Note: profileId selects a configuration profile (a lens, not a container). If the user has not specified a profile, call get_preferred_profile (or get_preferences) and use preferredProfileId — do not assume profile 1.
Inputs: body (required), profileId (required), attackTypeId (required).
create_checker_optionA
Create checker options related to a profile and checker. Note: profileId selects a configuration profile (a lens, not a container). If the user has not specified a profile, call get_preferred_profile (or get_preferences) and use preferredProfileId — do not assume profile 1.
Inputs: body (required), checkerId (required), profileId (required).
create_dashboardC
Create dashboard instance.
Inputs: body (required).
create_directoryC
Create directory instance.
Inputs: body (required).
create_email_notifierC
Create email-notifier instance.
Inputs: body (required).
create_infrastructureC
Create infrastructure instance.
Inputs: body (required).
create_profileC
Create profile instance.
Inputs: body (required).
create_roleC
Create role instance.
Inputs: body (required).
create_syslogC
Create syslog instance.
Inputs: body (required).
create_userC
Create user instance.
Inputs: body (required).
create_widgetB
Create a new widget in dashboard by dashboard id
Inputs: body (required), dashboardId (required).
delete_dashboardA
Delete dashboard instance with it's associated widgets
Inputs: id (required).
delete_directoryC
Delete directory instance.
Inputs: id (required), infrastructureId (required).
delete_email_notifierB
Delete email-notifier instance.
Inputs: id (required).
delete_infrastructureB
Delete infrastructure instance.
Inputs: id (required).
delete_profileC
Delete profile instance.
Inputs: id (required).
delete_roleC
Delete role instance.
Inputs: id (required).
delete_syslogB
Delete syslog instance.
Inputs: id (required).
delete_userC
Delete user instance.
Inputs: id (required).
delete_widgetC
Delete widget instance.
Inputs: id (required), dashboardId (required).
export_attacksA
Get all attacks in CSV rows format Note: profileId selects a configuration profile (a lens, not a container). If the user has not specified a profile, call get_preferred_profile (or get_preferences) and use preferredProfileId — do not assume profile 1.
Inputs: profileId (required), resourceType (required), resourceValue (required), limit, +6 more.
export_checker_dataA
Get all ad objects of a deviance in CSV rows format Note: profileId selects a configuration profile (a lens, not a container). If the user has not specified a profile, call get_preferred_profile (or get_preferences) and use preferredProfileId — do not assume profile 1.
Inputs: checkerId (required), profileId (required), language, parameters.
generate_saml_certificateC
Generates SAML certificate
Inputs: no named parameters.
get_aboutC
Get about singleton.
Inputs: no named parameters.
get_ad_object_by_checkerA
Retrieve an AD object by id that have deviances for a specific profile and checker Note: profileId selects a configuration profile (a lens, not a container). If the user has not specified a profile, call get_preferred_profile (or get_preferences) and use preferredProfileId — do not assume profile 1.
Inputs: id (required), checkerId (required), profileId (required), showIgnored.
get_ad_object_by_directoryC
Get ad-object instance by id.
Inputs: id (required), directoryId (required).
get_ad_object_by_infrastructureC
Get ad-object instance by id.
Inputs: id (required), directoryId (required), infrastructureId (required).
get_ad_object_changesB
Get one ad-object changes between a given event and the event which precedes it
Inputs: id (required), eventId (required), directoryId (required), infrastructureId (required), +1 more.
get_ad_object_in_eventC
Get one ad-object by event and by id
Inputs: id (required), eventId (required), directoryId (required), infrastructureId (required).
get_alertB
Get alert instance by id.
Inputs: id (required).
get_api_keyA
Get the API key of the current user
Inputs: no named parameters.
get_application_settingsB
Get the application settings
Inputs: no named parameters.
get_attack_type_configurationB
Get the attack type configuration
Inputs: no named parameters.
get_categoryB
Get category instance by id.
Inputs: id (required).
get_checkerB
Get checker instance by id.
Inputs: id (required).
get_cloud_statisticsC
Get user info needed to send cloud statistics.
Inputs: no named parameters.
get_current_userB
Get a user's information
Inputs: no named parameters.
get_dashboardB
Get dashboard instance by id.
Inputs: id (required).
get_devianceB
Get ad-object-deviance-history instance by id.
Inputs: id (required), directoryId (required).
get_deviance_by_directoryC
Get ad-object-deviance-history instance by id.
Inputs: id (required), directoryId (required), infrastructureId (required).
get_directoryB
Get directory instance by id.
Inputs: id (required).
get_directory_by_infrastructureC
Get directory instance by id.
Inputs: id (required), infrastructureId (required).
get_email_notifierB
Get email-notifier instance by id.
Inputs: id (required).
get_eventB
Get event instance by id.
Inputs: id (required), directoryId (required).
get_event_by_directoryC
Get event instance by id.
Inputs: id (required), directoryId (required), infrastructureId (required).
get_infrastructureA
Get infrastructure instance by id.
Inputs: id (required).
get_ldap_configurationB
Get ldap-configuration singleton.
Inputs: no named parameters.
get_licenseC
Get license singleton.
Inputs: no named parameters.
get_lockout_policyA
Get the lockout policy
Inputs: no named parameters.
get_metricsA
Collect Eridanis and SQL Server health metrics.
Inputs: no named parameters.
get_preferencesB
Get a user's preferences
Inputs: no named parameters.
get_preferred_profileA
Return the user's preferred (default) profile — its id and name — from GET /api/preferences. TIE profiles are configuration lenses, not containers; one is marked preferred. Use this profileId by default in profile-scoped tools unless the user explicitly names a different profile. Prevents querying the wrong profile and getting empty results.
Inputs: no named parameters.
get_product_associationC
Get product association in the License
Inputs: no named parameters.
get_profileA
Get profile instance by id.
Inputs: id (required).
get_profile_scoresA
Get the directories score by profile Note: profileId selects a configuration profile (a lens, not a container). If the user has not specified a profile, call get_preferred_profile (or get_preferences) and use preferredProfileId — do not assume profile 1.
Inputs: profileId (required), reasonIds, checkerIds, directoryIds.
get_profile_topologyA
Get a representation of the AD topology. Note: profileId selects a configuration profile (a lens, not a container). If the user has not specified a profile, call get_preferred_profile (or get_preferences) and use preferredProfileId — do not assume profile 1.
Inputs: profileId (required).
get_reasonB
Get reason instance by id.
Inputs: id (required).
get_relay_linking_keyA
Get current linking key for relay setup
Inputs: no named parameters.
get_report_access_tokenA
Returns the authentication token required to pull data from Tenable Cloud
Inputs: no named parameters.
get_roleC
Get role instance by id.
Inputs: id (required).
get_saml_configurationA
Get saml-configuration singleton.
Inputs: no named parameters.
get_syslogB
Get syslog instance by id.
Inputs: id (required).
get_topologyA
Discover the Active Directory environment as a Forest -> Domain tree. Returns each infrastructure (forest) with its directories (domains) and their IDs, so you can obtain the infrastructureId / directoryId values required by other tools. This is the topology (containment) axis and is independent of profiles. Call this first when you do not already know the IDs of the forests or domains you need to query.
Inputs: no named parameters.
get_userB
Get user instance by id.
Inputs: id (required).
get_user_creation_defaultsB
Return the default roles for user creation
Inputs: no named parameters.
get_widgetC
Get widget instance by id.
Inputs: id (required), dashboardId (required).
get_widget_optionsC
Get a widget's options by id
Inputs: id (required), dashboardId (required).
list_ad_objectsC
Return the last state of every ad-object at the provided timestampBy default, the timestamp is now. Objects out of scope are included
Inputs: no named parameters.
list_attacksA
Get all attacks Note: profileId selects a configuration profile (a lens, not a container). If the user has not specified a profile, call get_preferred_profile (or get_preferences) and use preferredProfileId — do not assume profile 1.
Inputs: profileId (required), resourceType (required), resourceValue (required), limit, +6 more.
list_attack_type_optionsA
Get all the attack type options related to a profile and attack type. Note: profileId selects a configuration profile (a lens, not a container). If the user has not specified a profile, call get_preferred_profile (or get_preferences) and use preferredProfileId — do not assume profile 1.
Inputs: profileId (required), attackTypeId (required), staged.
list_attack_typesB
Get attack types
Inputs: no named parameters.
list_categoriesB
Retrieve all category instances.
Inputs: no named parameters.
list_changed_deviancesC
Get all deviances that has been created or resolved since an event
Inputs: no named parameters.
list_checker_optionsA
Get all the checker options related to a profile and checker. Note: profileId selects a configuration profile (a lens, not a container). If the user has not specified a profile, call get_preferred_profile (or get_preferences) and use preferredProfileId — do not assume profile 1.
Inputs: checkerId (required), profileId (required), staged.
list_checkersA
Retrieve all checker instances.
Inputs: no named parameters.
list_dashboardsA
Retrieve all dashboard instances.
Inputs: no named parameters.
list_deviances_by_checkerA
Get all deviances by checker. Note: profileId selects a configuration profile (a lens, not a container). If the user has not specified a profile, call get_preferred_profile (or get_preferences) and use preferredProfileId — do not assume profile 1.
Inputs: body (required), checkerId (required), profileId (required), page, +3 more.
list_deviances_by_directoryC
Get all deviances for a directory.
Inputs: directoryId (required), infrastructureId (required), page, perPage, +3 more.
list_deviances_by_directory_and_checkerA
Get all deviances related to a single directory and checker. Note: profileId selects a configuration profile (a lens, not a container). If the user has not specified a profile, call get_preferred_profile (or get_preferences) and use preferredProfileId — do not assume profile 1.
Inputs: checkerId (required), profileId (required), directoryId (required), infrastructureId (required), +2 more.
list_deviances_by_eventA
Get all deviances by eventId. Note: profileId selects a configuration profile (a lens, not a container). If the user has not specified a profile, call get_preferred_profile (or get_preferences) and use preferredProfileId — do not assume profile 1.
Inputs: body (required), eventId (required), profileId (required), directoryId (required), +3 more.
list_directoriesB
Retrieve all directory instances.
Inputs: no named parameters.
list_directories_by_infrastructureB
Get all directories for a given infrastructure
Inputs: infrastructureId (required).
list_email_notifiersA
Retrieve all email-notifier instances.
Inputs: no named parameters.
list_infrastructuresB
Retrieve all infrastructure instances.
Inputs: no named parameters.
list_profile_alertsA
Retrieve all alert instances. Note: profileId selects a configuration profile (a lens, not a container). If the user has not specified a profile, call get_preferred_profile (or get_preferences) and use preferredProfileId — do not assume profile 1.
Inputs: profileId (required), page, read, perPage, +1 more.
list_profilesB
Retrieve all profile instances.
Inputs: no named parameters.
list_reasonsA
Retrieve all reason instances.
Inputs: no named parameters.
list_reasons_by_checkerA
Retrieve all reason instances that have deviances for a specific profile and checker. Note: profileId selects a configuration profile (a lens, not a container). If the user has not specified a profile, call get_preferred_profile (or get_preferences) and use preferredProfileId — do not assume profile 1.
Inputs: checkerId (required), profileId (required).
list_reasons_by_eventA
Retrieve all reason instances for which we have deviances for a specific profile, directory and event. Note: profileId selects a configuration profile (a lens, not a container). If the user has not specified a profile, call get_preferred_profile (or get_preferences) and use preferredProfileId — do not assume profile 1.
Inputs: eventId (required), profileId (required), directoryId (required), infrastructureId (required).
list_rolesA
Retrieve all role instances.
Inputs: no named parameters.
list_syslogsB
Retrieve all syslog instances.
Inputs: no named parameters.
list_usersB
Get all users
Inputs: no named parameters.
list_widgetsB
Get all widgets by dashboard id
Inputs: dashboardId (required).
loginC
Logs in a user
Inputs: body (required).
logoutB
Logs out a user
Inputs: no named parameters.
refresh_report_access_tokenB
Updates the authentication token required to pull data from Tenable Cloud
Inputs: no named parameters.
search_ad_objectsA
Search all AD objects having deviances by profile by checker Note: profileId selects a configuration profile (a lens, not a container). If the user has not specified a profile, call get_preferred_profile (or get_preferences) and use preferredProfileId — do not assume profile 1.
Inputs: body (required), checkerId (required), profileId (required), page, +1 more.
search_deviances_by_ad_objectB
Search all deviances by profile by checker by AD object Note: profileId selects a configuration profile (a lens, not a container). If the user has not specified a profile, call get_preferred_profile (or get_preferences) and use preferredProfileId — do not assume profile 1.
Inputs: body (required), checkerId (required), profileId (required), adObjectId (required), +2 more.
search_eventsD
Search events instances
Inputs: body (required).
set_role_permissionsB
Replace permission list for a role
Inputs: id (required), body (required).
set_user_rolesA
Replace role list for a user
Inputs: id (required), body (required).
set_widget_optionsC
Define widget's options by id
Inputs: id (required), body (required), dashboardId (required).
test_email_notifierC
Send a test email notification by id
Inputs: id (required).
test_email_notifier_by_email_notifierC
Send a test email notification
Inputs: body (required).
test_syslogB
Send a test syslog notification by id
Inputs: id (required).
test_syslog_by_syslogC
Send a test syslog notification
Inputs: body (required).
unstage_profileC
Unstages change of the related profile
Inputs: id (required).
update_alertC
Update alert instance.
Inputs: id (required), body (required).
update_application_settingsC
Update the application settings
Inputs: body (required).
update_attack_type_configurationC
Update the attack type configuration
Inputs: body (required).
update_dashboardC
Update dashboard instance.
Inputs: id (required), body (required).
update_devianceC
Update ad-object-deviance-history instance.
Inputs: id (required), body (required), directoryId (required), infrastructureId (required).
update_deviances_by_ad_objectA
Update the deviances emitted on a specific AD object and for a specific checker. Note: profileId selects a configuration profile (a lens, not a container). If the user has not specified a profile, call get_preferred_profile (or get_preferences) and use preferredProfileId — do not assume profile 1.
Inputs: body (required), checkerId (required), profileId (required), adObjectId (required), +3 more.
update_deviances_by_checkerB
Update instances matching a checkerId. Note: profileId selects a configuration profile (a lens, not a container). If the user has not specified a profile, call get_preferred_profile (or get_preferences) and use preferredProfileId — do not assume profile 1.
Inputs: body (required), checkerId (required), profileId (required).
update_directoryC
Update directory instance.
Inputs: id (required), body (required), infrastructureId (required).
update_email_notifierC
Update email-notifier instance.
Inputs: id (required), body (required).
update_infrastructureC
Update infrastructure instance.
Inputs: id (required), body (required).
update_ldap_configurationC
Update ldap-configuration singleton.
Inputs: body (required).
update_licenseC
Create new license singleton
Inputs: body (required).
update_lockout_policyB
Update the lockout policy
Inputs: body (required).
update_preferencesC
Update a user's preferences
Inputs: body (required).
update_profileC
Update profile instance.
Inputs: id (required), body (required).
update_profile_alertsA
Update alerts for one profile Note: profileId selects a configuration profile (a lens, not a container). If the user has not specified a profile, call get_preferred_profile (or get_preferences) and use preferredProfileId — do not assume profile 1.
Inputs: body (required), profileId (required).
update_roleC
Update role instance.
Inputs: id (required), body (required).
update_saml_configurationC
Update saml-configuration singleton.
Inputs: body (required).
update_syslogC
Update syslog instance.
Inputs: id (required), body (required).
update_userC
Update user instance.
Inputs: id (required), body (required).
update_widgetC
Update widget instance.
Inputs: id (required), body (required), dashboardId (required).
Tool Schema Changelog
Recent tool additions, removals, and schema changes observed during successful MCP inspections.
TDQS
Scored across 133 tools
Many tools have near-identical names and descriptions, such as get_ad_object_by_directory vs. get_ad_object_by_infrastructure, get_event vs. get_event_by_directory, and test_email_notifier vs. test_email_notifier_by_email_notifier. The large number of overlapping resource-specific variants makes it difficult for an agent to reliably select the correct tool.
The tool set mostly follows a consistent verb_noun snake_case pattern like list/get/create/update/delete_X. Minor deviations such as clone_profile, unstage_profile, commit_profile, and test_X_by_X variants are noticeable but still predictable.
With 133 tools, the server is far beyond a well-scoped MCP surface. Even for a broad AD security management domain, the sheer number of tools—including many near-duplicate accessor variants—makes the server overwhelming and difficult to navigate.
The server provides extensive CRUD/lifecycle coverage for users, roles, profiles, infrastructures, directories, dashboards, widgets, syslogs, email notifiers, and configuration singletons. Minor gaps exist, such as no delete operation for API keys or checker/attack type options, but core workflows are largely covered.
Maintenance
Related MCP Connectors
isolved and ApplicantPro jobs, tenant discovery, and change detection as an MCP server.
One MCP endpoint for your company's tools, with per-person sign-in, policy and audit.
Single-connection MCP gateway discovering and executing 490+ verified MCP & A2A tools.
Related MCP Servers
- FlicenseBqualityNot gradedmaintenanceAn MCP server that enables interaction with SailPoint IdentityNow and Identity Security Cloud for identity governance and access management. It provides comprehensive tools to manage identities, accounts, roles, workflows, and certifications through API integration.636-
- AlicenseBqualityAmaintenanceAn MCP server for CIPP (Community IT Professionals Platform), enabling MSPs to manage Microsoft 365 tenants, users, policies, and security settings through CIPP's API.4913Apache 2.0
- AlicenseNot gradedqualityCmaintenanceMCP server that enables AI-powered assessment of Active Directory on-premises environments by exposing AD data as queryable tools for LLMs like Claude.MIT
- AlicenseCqualityBmaintenanceMCP server providing 100% coverage of the CISO Assistant GRC REST API through action-routed tools for compliance, risk management, and other security domains.500MIT