Tenable OT Security MCP Server
This server provides a comprehensive set of tools for interacting with a Tenable OT Security deployment, covering read‑only exploration of OT/ICS assets, vulnerabilities, events, policies, and network configuration, plus optional, dry‑run‑by‑default write operations for asset management, policy editing, and finding resolution.
Connectivity & Orientation
Check server status and authentication.
Summarize the environment (assets, vulnerabilities, events, policies) in one call.
Introspect the GraphQL schema to discover available filters.
Asset Inventory
Query assets by kind, category, criticality, tags, vendor, and more.
Retrieve full asset details (IPs, MACs, Purdue level, risk, backplane, segments).
List open vulnerabilities per asset with CVEs, CVSS scores, KEV flags, and vendor solutions.
View and manage custom field schemas.
Check the status of manual asset file uploads.
Vulnerability Management
Search fleet‑wide vulnerabilities by severity, CVE, family, and exploitability.
Get full vulnerability details and all affected assets.
Query plugin definitions (vulnerability catalog) and individual plugin details.
Bulk export assets, plugin definitions, and findings.
Events & Detection Policies
Query OT events with filters for time, severity, source/destination IP, policy, and event type.
Retrieve full event records.
List detection policies, see their status (enabled/paused), and get full policy configurations.
Query per‑asset policy findings with MITRE ATT&CK technique mapping.
Network Topology & Configuration
List network segments and zones.
View communication paths for any asset.
Read appliance network configuration (monitored subnets, firewall rules, interfaces, blind spots).
List deployed sensors and OT agents.
Cross‑Domain Analysis
Explore attack pathways (1‑hop network expansion).
Identify vulnerability clusters across multiple assets.
Analyze chronological event sequences for patterns.
Retrieve a full per‑asset intelligence bundle (asset + vulns + events + communication peers).
Group Management (Read)
List and inspect asset, email, schedule, tag, rule, port, protocol, and user groups (both ICP and Enterprise Manager levels).
View archived groups.
Discover tags eligible for tag groups and email groups per SMTP server.
Scan Definitions (Read)
List and inspect active scan definitions.
View past scan execution history (note: the server does not initiate scans).
Impact Analysis
List policy relationships to evaluate what references an object before making changes.
Write Operations (opt‑in, dry‑run by default)
Enabled only when TENABLE_OT_WRITE_TOOLS_ENABLED=1 is set. All destructive actions require explicit confirmation and are audited.
Asset Lifecycle: hide/restore, bulk hide/restore, merge, recalculate risk, upload manual asset file.
Asset Properties: update name/kind/location/Purdue level/criticality/custom fields, bulk edit, reset metadata, rename backplane.
Custom Fields: create, rename, or delete custom field schemas.
Detection Policies: enable/disable/archive policies, update policy configurations.
Findings: resolve findings.
Groups: create, update, archive asset, email, schedule, tag, rule, port, protocol, and user groups.
Scan Definitions: define, edit, enable, disable, delete active scans, port scans, SNMP scans, etc.
Sensors: pause or remove sensors (with impact warnings).
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., "@Tenable OT Security MCP Server list all assets with critical vulnerabilities"
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.
Tenable OT Security MCP Server
An MCP (Model Context Protocol) server that connects AI clients — Claude Desktop, Claude Code, or any other MCP client — to a Tenable OT Security deployment. It exposes OT/ICS asset inventory, vulnerabilities, events, detection policies, and policy findings as read tools, plus an opt-in, dry-run-by-default write layer for asset edits, policy management, and finding resolution.
Not an official Tenable product. Copyright (c) 2026 Tenable, Inc., released under Apache 2.0. It is not a supported Tenable product: it ships with no SLA, no warranty, and no support commitment, and it is not covered by Tenable's product security or vulnerability-disclosure processes. Nothing here should be taken as a statement of Tenable's product roadmap. "Tenable" and "Tenable OT Security" are trademarks of Tenable, Inc.; Apache 2.0 explicitly does not grant trademark rights (§6), and no trademark licence is granted by this repository. Use at your own risk against your own deployment.
Safety model
This server talks to industrial control systems. Four rules are structural, not advisory:
1. Write tools are invisible unless you opt in.
Without TENABLE_OT_WRITE_TOOLS_ENABLED=1, the write modules are never imported
and no write tool name appears in the MCP tools/list response at all. An AI
client cannot call — or even discover — an operation you haven't enabled. This is
deliberately not a permission error at call time.
2. Every write defaults to dry_run=True.
A dry-run call returns the exact payload that would be sent, without sending
it. The AI is expected to show you that preview and get your approval before
re-calling with dry_run=false.
3. Destructive operations need a second, explicit confirm flag.
dry_run=false alone is not enough. delete_custom_field requires
confirm_wipes_values=True on top of it, and is rejected without it.
4. Every write call is audited — dry-run, real, or failed.
One JSON line per call to /data/audit.jsonl: timestamp, tool, parameters,
dry-run flag, outcome, and any error. Mount that volume so the record outlives
the container. The audit writer fails closed: if it can't record, the call
raises rather than proceeding silently.
Two further guarantees:
No active scanning. Nothing in this codebase triggers, launches, or schedules a scan against live OT/ICS equipment. Scan definitions can be created and edited (behind the write opt-in, dry-run by default), and
enable_active_scansets a definition'senabledflag so it can participate in runs — but only a human, from the Tenable OT UI, ever starts a scan. There is no run/start/execute mutation anywhere in this codebase.Credentials from the environment only. The API key is read from
TOT_API_KEY, held privately by the client, and never logged, never written to disk by the server, and never included in an audit entry. Uploaded file contents are never audited either — only filename and byte size.
Related MCP server: Tenable Identity Exposure MCP Server
Prerequisites
A reachable Tenable OT Security deployment
A Tenable OT service-account API key (how to generate one)
Docker (for the container deployment), or Python 3.12+ to run it directly
Network reachability from wherever this runs to the appliance
Deploy
# on Ubuntu, if Docker isn't installed:
curl -fsSL https://get.docker.com | sudo sh
sudo usermod -aG docker "$USER"
# log out/in or `newgrp docker`, then confirm:
docker ps
# from the dev machine:
scp -r "./tenable-ot-mcp" <vm-user>@<vm-ip>:~/tenable-mcp-server
# on the VM:
cd ~/tenable-mcp-server
docker compose build
docker compose up -dBefore docker compose up, create the .env file the compose file reads:
cp .env.example .envThen edit .env and set TOT_URL and TOT_API_KEY. Keep the key out of shell
history and out of version control — .env is gitignored.
Check it came up:
docker compose psdocker compose logs -fIf docker compose isn't available
Ubuntu's docker.io package ships the Docker CLI without the Compose v2
plugin, so docker compose build fails with unknown command. Either install
the plugin:
sudo apt install docker-compose-v2…or skip Compose entirely — it isn't required:
docker build -t tenable-ot-mcp:0.1.0 .docker run -d --name tenable-ot-mcp --restart unless-stopped --env-file .env -e TENABLE_OT_HOST=0.0.0.0 -e TENABLE_OT_PORT=8000 -e TENABLE_OT_AUDIT_DIR=/data -p 8000:8000 -v "$PWD/data:/data" --security-opt no-new-privileges:true tenable-ot-mcp:0.1.0Audit-directory ownership (bind mounts)
The server process runs as non-root uid 10001, but a bind-mounted ./data
arrives owned by your host uid (typically 1000) — so without help, the
audit writer can't create audit.jsonl, and because it fails closed,
every write call would then fail too. This is the single most common way this
deployment breaks, and it breaks late: the container starts, reports
healthy, serves reads fine, and only fails the first time something writes.
This is now fixed automatically, in the image itself, not by a manual step
you have to remember to run. docker-entrypoint.sh starts as root for
exactly long enough to chown/chmod /data if its ownership is wrong,
proves the target uid can actually write there (a real write-then-delete
probe, not just a permissions check), and then permanently drops to uid
10001 via setpriv before the server process ever starts — root is never
carried into the running server. If the probe fails (e.g. a mount that
ignores chown, such as some NFS/CIFS configurations), the container refuses
to start and logs exactly what to run on the host, rather than starting a
server whose every write will fail later. setpriv (from util-linux) is
used instead of the more commonly documented gosu/su-exec because it's
already present in the python:3.12-slim base image — no extra dependency.
You do not need to docker run --user 0:0 ... chown by hand anymore; a
plain docker compose up -d (or docker run without a --user override)
is enough on a fresh checkout, a fresh VM, or after the ./data directory
gets recreated. The manual command below still works as a diagnostic if you
ever want to fix ownership outside the container, e.g. before the image has
even been built:
docker run --rm -v "$PWD/data:/data" --user 0:0 tenable-ot-mcp:0.1.0 sh -c "chown -R 10001:$(id -g) /data && chmod -R 770 /data"Deploying without a deploy key
scripts/deploy_to_mcp_server.ps1 uses a dedicated SSH key for repeat
deployments. It's a convenience, not a requirement — a plain scp -r of the
project directory works fine. Copy into a path that doesn't already exist:
scp -r into an existing directory nests the source inside it rather than
replacing it, and will overwrite same-named files of anything already there.
Running without Docker
pip install .
export TOT_URL=https://tenable-ot.example.local
export TOT_API_KEY=...
python -m tenable_ot_mcp.serverConfirm what's exposed — especially whether the write layer is on:
python -m tenable_ot_mcp.server --list-toolsConfiguration
All configuration is environment variables. There is no config file and no way to pass the API key on the command line.
Variable | Default | Purpose |
| (required) | Appliance base URL. |
| (required) | Service-account API key. |
|
|
|
|
|
|
|
| Bind address for HTTP transports. |
|
| Directory holding |
|
| Verify the appliance certificate. OT appliances are commonly self-signed — matching pyTenable's own default — so the compose file turns this off. Turn it on for any deployment with a trusted certificate. |
|
| GraphQL request timeout (seconds). |
|
| File-upload timeout; uploads are parsed server-side and take far longer. |
| (unset) | Optional label stamped on every audit entry to identify which appliance/credential this instance runs as. A label — never the key. |
|
| Log level. Logs go to stderr, keeping stdout clear for the stdio transport. |
Connecting an MCP client
Option A — direct HTTP over the LAN (simplest)
Set TENABLE_OT_BIND=0.0.0.0 in .env and recreate the container, so it
publishes on 0.0.0.0:8000->8000/tcp. Claude Desktop has no native plain-HTTP
remote transport, so bridge it with mcp-remote. Because the connection is
http:// rather than https://, --allow-http is required:
{
"mcpServers": {
"tenable-ot": {
"command": "C:\\Program Files\\nodejs\\npx.cmd",
"args": [
"-y",
"mcp-remote@0.1.37",
"http://192.168.1.79:8000/mcp",
"--allow-http",
"--transport",
"http-only"
]
}
}
}A working bridge logs Connected to remote server, Local STDIO server running, Proxy established successfully.
Understand the trade-off. With
TENABLE_OT_BIND=0.0.0.0the MCP endpoint is reachable by anything on the LAN, the transport has no authentication, and--allow-httpmeans the traffic — including every asset, vulnerability and policy record — is unencrypted on the wire. That is acceptable on an isolated lab segment and not much else. For anything beyond that, use Option B or put an authenticating TLS reverse proxy in front.
Option B — SSH tunnel (keeps it off the network)
Leave TENABLE_OT_BIND=127.0.0.1 (the default) so the port is bound to
loopback on the server only, and forward it:
ssh -N -L 8000:127.0.0.1:8000 <vm-user>@<vm-ip>Then point the client at http://localhost:8000/mcp — same mcp-remote
config as above with the host changed, or for Claude Code:
claude mcp add --transport http tenable-ot http://localhost:8000/mcpOption C — stdio (server run locally, no network at all)
{
"mcpServers": {
"tenable-ot": {
"command": "python",
"args": ["-m", "tenable_ot_mcp.server"],
"env": {
"TOT_URL": "https://tenable-ot.example.local",
"TOT_API_KEY": "your-key-here"
}
}
}
}Tool catalog
130 tools: 59 read (always registered) and 71 write (only when
TENABLE_OT_WRITE_TOOLS_ENABLED=1). Every write tool defaults to
dry_run=True and is audited on every call. Confirm what your instance
actually exposes with:
python -m tenable_ot_mcp.server --list-toolsRead tools (always available)
Orientation and connectivity
Tool | Description |
| Connectivity/auth probe with latency; first stop when other tools fail. |
| One-call orientation: asset/vulnerability/event/policy rollups for a new deployment. |
| Introspect this deployment's own GraphQL schema. Run this to unblock the gated filters. |
Assets
Tool | Description |
| Filter OT assets; returns identity, IPs/MACs, Purdue level, segments, backplane, risk. Paginated. |
| One asset's full bundle by id, including tags and backplane id. |
| Open vulnerabilities for one asset, with CVEs, CVSS, exploit/KEV flags, vendor solution. |
| The tenant's custom-field slot/label/type schema. |
| Processing status of the last manual upload (CSV-type imports only — see the tool description). |
Vulnerabilities and plugins
Tool | Description |
| Fleet-wide vulnerability search with severity / family / exploitability filters — "what do we patch first". |
| One vulnerability's full record. |
| Plugin definitions — the vulnerability catalog, not per-asset findings. |
| One plugin definition by numeric id. |
Events and detection policies
Tool | Description |
| OT events — individual detections, newest first (via pyTenable). |
| One OT event's full record. |
| Detection policies with severity, enabled/paused state, and fired-event counts. |
| One policy's complete configuration. Required reading before |
| Per-asset detection findings (policy × asset × hit counts). |
Cross-domain joins
These return joined relational data, not computed analysis — the tool titles say so explicitly, so a model doesn't present a join as inference.
Tool | Description |
| Attack-pathway data (relational, not computed). |
| Vulnerability clusters (relational join, not computed). |
| Temporal patterns (event sequence, not motif analysis). |
| Asset intelligence bundle — joined data, not narrative. |
Topology, network configuration, sensors
Tool | Description |
| Network segments and zones as the appliance has observed them. |
| Observed communication paths for one asset. |
| What the appliance is configured to watch. |
| Sensors and OT agents feeding this appliance. |
Impact analysis (run before writes)
Tool | Description |
| "What breaks if I change this?" |
Groups — read
Tool | Description |
| Asset groups a policy can scope to. Call before |
| Email groups available as policy notification targets. |
| Email groups routing through a given SMTP server. |
| Schedule groups (time windows) a policy can use. |
| Tag groups (PLC controller-tag rollups). |
| Tags eligible for inclusion in a tag group. |
| Rule groups (IDS rule bundles). |
| Port groups (port-range bundles for |
| Protocol groups (protocol + port-range bundles). |
| User groups, ICP level. |
| User groups, Enterprise Manager level. |
Scan definitions — read
Definitions only. Nothing here runs a scan.
Tool | Description |
| Active-scan definitions. |
| One active-scan definition. |
| Past executions of an active scan. |
Bulk exports (via pyTenable)
High latency — prefer the interactive query tools. Skipped entirely if no pyTenable bridge is supplied.
Tool | Description |
| Bulk asset export. |
| Bulk plugin-definition export. |
| Bulk export of finding instances (vulnerability/violation × asset). |
Write tools (only when TENABLE_OT_WRITE_TOOLS_ENABLED=1)
All default to dry_run=True and are audited on every call.
Asset lifecycle
Tool | Risk |
| WRITE — reversible. |
| WRITE — affects many assets. |
| WRITE — destructive; queues entries for deletion. Re-discovery creates fresh records. |
| WRITE — DESTRUCTIVE AND IRREVERSIBLE. Source asset is permanently deleted. Polls the async merge job to a terminal state. |
| WRITE — deployment-wide variant can be expensive. |
| WRITE — creates/updates assets from file contents; |
Asset properties
Tool | Risk |
| WRITE — edits name/kind/location/description/Purdue/criticality/custom fields. |
| WRITE — same edits across a filter. Untargeted calls rejected. |
| WRITE — reverts every operator-set field to as-discovered. |
| WRITE — renames a chassis/rack (distinct object from an asset). |
Custom-field schema
Tool | Risk |
| WRITE. |
| WRITE — destructive. Wipes the stored value on every asset. Requires |
Detection policies
Tool | Risk |
| WRITE. |
| WRITE — DISABLES DETECTION. Review blast radius. |
| WRITE — irreversible. |
| WRITE — edits title/scope/schedule/severity/notifications. Always re-reads current state first, because Tenable's policy mutations are full replaces, not patches. Renaming ( |
Findings
Tool | Risk |
| WRITE — marks findings resolved. Bare resolve-all is rejected. |
Groups — write
Archive is Tenable OT's soft-delete. Run list_policy_relationships first:
these are shared objects, and policy mutations are full replaces.
Tool | Risk |
| WRITE — asset groups are policy scope targets. |
| WRITE — bulk display-tag flag toggle. |
| WRITE — alert recipient lists; archiving can silence notifications. |
| WRITE — policy time windows. |
| WRITE — PLC controller-tag rollups. |
| WRITE — IDS rule bundles. |
| WRITE — port-range bundles. |
| WRITE — protocol + port-range bundles. |
| WRITE — ACCESS CONTROL (ICP level). |
| WRITE — ACCESS CONTROL (Enterprise Manager level). |
Scan definitions — write
Define/edit only. enable_active_scan sets the enabled flag so a definition
can participate in runs initiated from the Tenable OT UI — it does not run
the scan. No tool in this codebase starts a scan.
Tool | Risk |
| WRITE — creates/edits a definition; does not run it. |
| WRITE — toggles whether a definition participates in UI-initiated runs. |
| WRITE — irreversible; removes the definition. |
| WRITE — port-scan job definition. |
| WRITE — SNMP scan job definition. |
| WRITE — controller-discovery job definition. |
| WRITE — asset-discovery job definition. |
| WRITE — inactive-probing job definition. |
| WRITE — edits the subnets-discovery scan. |
Sensors
Tool | Risk |
| WRITE — CHANGES WHAT THE APPLIANCE CAN SEE. Pausing or removing a sensor creates a monitoring blind spot. |
Current limitations
Filtering works, except where it needs an unconfirmed enum value.
The filter-expression input shape is confirmed — taken from pyTenable's own
source (tenable/ot/exports/api.py, tenable/ot/assets.py), which builds the
same AssetExpressionsParams inputs:
{"field": "id", "op": "Equal", "values": asset_id} # leaf
{"op": "And", "expressions": [...]} # conjunctionSo these filters work today: vendor, name_contains, hidden, tags, every
query_policy_findings filter (including severity_at_least, since
PolicyLevel is confirmed), and the equivalent targeting on bulk_edit_assets
and resolve_findings.
Still gated: filters that map a natural word onto a Tenable enum value —
kind (AssetType), category (AssetCategory), criticality_at_least
(UserDefinedCriticality), and the purdue_level / kind write-side setters.
Those enum values aren't published anywhere, and this server refuses to guess
them: a wrong enum doesn't fail loudly, it silently filters or writes the wrong
thing. Calling one raises SchemaNotConfirmedError naming the exact type to
introspect.
To close the gap, run the introspection query in
src/tenable_ot_mcp/tools/_enums.py (_INTROSPECTION_QUERY) against your
deployment and fill in the UNCONFIRMED section — a single-block edit. The types
still needed are listed there as _NEEDED_TYPES.
Coverage note. Group management (asset / email / schedule / tag / rule /
port / protocol / user), network topology, network configuration, sensors, and
scan definitions are all implemented against custom GraphQL, because pyTenable
does not cover them — tenable.ot.TenableOT wraps only .assets,
.events, .plugins, .exports and a generic .graphql() passthrough
(verified against pyTenable 26.6.1). pyTenable is used only for the bulk-export
surface (export_assets, export_findings, export_plugin_definitions) and
the event/plugin queries; if no pyTenable bridge is supplied those tools are
skipped and the server still starts.
Queries marked unconfirmed in the source return a clear error envelope
naming what to introspect, rather than raising, when a given appliance doesn't
expose them.
Development
pip install -e ".[dev]"
python -m pytestThe suite runs entirely against a mocked client — no network, no appliance, no credentials. It covers dry-run paths, live paths, audit-entry assertions, and confirm-flag rejection paths for destructive tools.
For a real deployment check, scripts/live_smoke_test.py exercises read-only
tools against a live appliance using env-var credentials. It never registers the
write modules. It is deliberately not part of the test suite and must not run in
CI.
scripts/bulk_import_projects.py batch-imports Rockwell .L5X/.ACD project
files by calling the real upload_manual_asset_file tool in-process, so batch
behaviour can't drift from single-file behaviour. Defaults to a dry run; pass
--live to actually upload.
Licence
Apache License 2.0 — see LICENSE. Every source file carries an
SPDX-License-Identifier: Apache-2.0 header.
Available Tools
59 toolsexport_assetsBulk export assetsA
Bulk asset export via Tenable OT's export API. Returns full asset records for offline analysis, inventory reconciliation, or feeding another system.
PREFER query_assets for interactive questions — it is far lower latency, supports the natural-vocabulary filters, and returns a trimmed projection that is cheaper to read. Use this tool when you genuinely need whole records in bulk.
search is a single-term substring. Structured export filters take a filter shape pyTenable's own documentation does not specify, so they are not exposed rather than guessed.
READ-ONLY. High latency on large deployments.
Inputs: limit, search, sort_by, start_at, +1 more. Returns: object
export_findingsBulk export findingsA
Bulk export of FINDING INSTANCES — one record per (vulnerability or policy violation x affected asset) — via Tenable OT's official export API.
Choosing between the finding tools:
• get_asset_vulnerabilities — the open vulnerabilities on ONE asset. Lowest latency; start here for a specific asset.
• query_policy_findings — detection-policy findings, with severity/status/MITRE filters.
• this tool — every finding instance in bulk, for offline analysis or reporting across the whole deployment.
search is a single-term substring. Structured export filters take a filter shape pyTenable's documentation does not specify, so they are not exposed rather than guessed.
READ-ONLY. High latency on large deployments.
Inputs: limit, search, start_at. Returns: object
export_plugin_definitionsBulk export plugin definitionsA
Bulk plugin-definition export via Tenable OT's export API. Returns full plugin records in bulk.
PREFER query_plugin_definitions for interactive lookups. Use this when you need the whole catalog.
READ-ONLY. High latency.
Inputs: limit, search, start_at. Returns: object
find_email_groups_using_smtp_serverFind email groups that route through a given SMTP serverA
Given an SMTP-server id, return the email groups bound to it. Useful before retiring an SMTP server: any group returned here will lose its delivery path if the server is removed.
Inputs: smtp_server_id (required), limit, after_cursor. Returns: object
get_active_scanGet one active scanA
Returns the full specification for one active-scan job by id. Use after list_active_scans returns a job of interest, or when reading parameters before suggesting modifications.
Inputs: scan_id (required). Returns: object
get_active_scan_executionsGet past executions of an active scanA
Returns past execution records for one active scan: start/end time, elapsed time, status (Completed / Failed / Ongoing), who initiated, source (UI / API / system), and any failure explanation. Use this to audit when a scan was last run and whether it succeeded — but the underlying execution is triggered by humans, not this server.
Inputs: scan_id (required), limit. Returns: object
get_assetGet one OT assetA
Returns the per-asset bundle for one OT asset by id: identity, classification, IPs / MACs, Purdue level, segment membership, criticality, run status, backplane (rack) membership, tags, and aggregate risk. backplane is null for assets not mounted in a chassis; when present, its id is what rename_backplane needs (a backplane's id is NOT the same as any asset's id). tags includes any Tenable-assigned project-file-import tag ('Project (SN: )') — pass one back into query_assets' tags filter to pull every asset that same import produced. Call after query_assets returns an id of interest. For the asset's open vulnerabilities call get_asset_vulnerabilities separately.
Inputs: asset_id (required). Returns: object
get_asset_groupGet an asset group by idA
Fetch one asset group's full record by its id. The shape varies by group subtype: an AssetList returns an assets_sample preview, an IpRange returns start_ip / end_ip, a FilterGroup returns its filter expression, and so on.
Inputs: group_id (required). Returns: object
get_asset_intelligenceGet asset intelligence bundle (joined data, not narrative)A
Returns one asset's full relational bundle in a single shot: asset core + open vulnerabilities + recent events where the asset is source or destination + 1-hop comms peers. The AI uses this bundle to write a per-asset intelligence narrative if asked. The server does NOT generate the narrative itself.
Inputs: asset_id (required), max_peers, max_vulns, max_events, +1 more. Returns: object
get_asset_vulnerabilitiesGet vulnerabilities for one assetA
Returns the open vulnerabilities (Tenable plugins) affecting one OT asset. Each vulnerability includes CVEs, CVSS v3 score and vector, exploit availability flags (CISA KEV, exploit-available, exploited-by-malware), age, public disclosure date, and the official vendor solution. Use query_vulnerabilities for global vuln filtering (KEV-only, exploit-available, severity floor, etc.); this tool is asset-scoped.
Inputs: asset_id (required), limit. Returns: object
get_communication_pathsGet communication paths for an assetA
Returns observed L2 communication links involving one OT asset, with peer asset id, protocols seen, traffic / conversation count, and first/last-conversation times. Peer assets are returned as IDs only — call get_asset on each peer id to enrich with name / vendor / type. The consuming AI uses this as the graph adjacency for attack-path reasoning; call again on a peer's id to expand further.
Inputs: asset_id (required), limit, since. Returns: object
get_email_groupGet an email group by idA
Fetch one email group with its recipient list and bound SMTP server details.
Inputs: group_id (required). Returns: object
get_em_user_groupGet a user group by id (Enterprise Manager level)C
One EM-level user group with its roles and member preview.
Inputs: group_id (required). Returns: object
get_eventGet one OT eventA
Returns full detail for a single OT event by id. Use after query_events returns an id of interest, or when a finding or case references a specific event.
Inputs: event_id (required). Returns: object
get_manual_asset_upload_statusGet manual asset-upload processing statusA
Check the processing status of the most recent file uploaded via Tenable OT's Manual Uploads data source (upload_manual_asset_file). Processing is asynchronous: poll this a few times after uploading if it hasn't updated yet. Returns null fields if no manual upload has ever been processed on this deployment.
CAVEAT (confirmed empirically): this field tracks literal CSV-type imports specifically — it stayed null through a live Rockwell .L5X project-file upload that DID create an asset. For project-file / OTD-scan-result uploads, don't treat a null result here as failure — confirm outcome with query_assets (by the controller/device name you expect, or by IP) instead of waiting on this to populate.
Inputs: no named parameters. Returns: object
get_plugin_definitionGet one plugin definitionA
Returns one plugin definition by its NUMERIC plugin id, including the assets it affects. Note the id is an integer (e.g. 500123), not the UUID used for assets.
READ-ONLY.
Inputs: plugin_id (required). Returns: object
get_policyGet one policy's full configurationA
Fetch one policy's COMPLETE configuration by id — source/destination asset-group scope (as AND'd rows of OR'd include/exclude conditions), schedule, severity, notification actions (syslog server / email group), and (for Communication/Protocol-schema policies) the protocol/port group. Unlike list_detection_policies's summary, this returns every field the policy's edit surface exposes.
Call this BEFORE update_policy — Tenable OT's policy mutations are full replaces, not patches, so you need to see the current state of every field before changing just the ones you actually want to change.
Inputs: policy_id (required). Returns: object
get_port_groupGet a port group by idC
One port group with up to 100 of its port-range items.
Inputs: group_id (required). Returns: object
get_protocol_groupGet a protocol group by idC
One protocol group with up to 100 of its items.
Inputs: group_id (required). Returns: object
get_rule_groupGet a rule group by idC
One rule group with the first 25 included rules.
Inputs: group_id (required). Returns: object
get_schedule_groupGet a schedule group by idA
Fetch one schedule group. Shape varies by kind: TimeInterval returns start_time/end_time; RecurringGroup returns a list of weekly windows under schedules.
Inputs: group_id (required). Returns: object
get_schema_enumsIntrospect the Tenable OT GraphQL schemaA
Read this deployment's own GraphQL schema — enum values, input-object shapes, and the available root query/mutation fields.
WHY THIS MATTERS: some filters on this server refuse to run with a SchemaNotConfirmedError because their Tenable-side enum values were never confirmed (this server does not guess enum values — a wrong one silently filters or writes the wrong thing instead of failing). Call this tool with no arguments to dump exactly the types those mappings need, then hand the output to whoever maintains tools/_enums.py; filling them in is a single edit and unblocks the gated filters.
It also answers 'does this appliance actually expose query X?' — useful when a topology tool reports that a top-level query was rejected.
Arguments: • type_names: specific GraphQL types to dump. Omit to get the types this server still needs. • include_inputs: also dump input-object field shapes (for filter/mutation argument structures). • list_root_fields: also list every root query and mutation name the appliance exposes.
READ-ONLY. Touches no OT data — GraphQL reflection only.
Inputs: type_names, include_inputs, list_root_fields. Returns: object
get_tag_groupGet a tag group by idB
One tag group with up to 100 member items.
Inputs: group_id (required). Returns: object
get_user_groupGet a user group by id (ICP-level)B
One ICP-level user group with its roles and member preview.
Inputs: group_id (required). Returns: object
get_vulnerabilityGet one vulnerabilityA
Returns one Tenable plugin (vulnerability) by id, plus the full list of OT assets currently affected. Use this to reason about exposure breadth ('which assets are affected by this exploited-in-the-wild vuln?'). To look up by CVE id, call query_vulnerabilities(cve='CVE-...') first and then fetch by the returned plugin_id.
Inputs: plugin_id (required). Returns: object
list_active_scansList active scansA
Returns Tenable OT active-scan job specifications: name, description, scan operation type (PortScan, AssetDiscovery, SnmpType, etc.), category (IT / OT / Discovery), trigger (Manual / Periodic / System), enabled flag, status, and the asset group the job targets. Use this to audit what scans are configured. Predefined system scans appear with predefined: true. Note: this server does not expose any tool that runs a scan — that's a human-only action via the Tenable OT UI.
Inputs: limit. Returns: object
list_archived_asset_groupsList archived asset groupsA
Page through archived asset groups — those that were soft-deleted via archive_asset_group. Group definitions and historical membership are preserved by Tenable; they just stop appearing in active views.
Inputs: limit, after_cursor. Returns: object
list_archived_port_groupsList archived port groupsC
Soft-deleted port groups, paginated.
Inputs: limit, after_cursor. Returns: object
list_archived_protocol_groupsList archived protocol groupsC
Soft-deleted protocol groups, paginated.
Inputs: limit, after_cursor. Returns: object
list_archived_rule_groupsList archived rule groupsC
Soft-deleted rule groups, paginated.
Inputs: limit, after_cursor. Returns: object
list_archived_schedule_groupsList archived schedule groupsC
Soft-deleted schedule groups, paginated.
Inputs: limit, after_cursor. Returns: object
list_archived_user_groupsList archived user groups (ICP-level)B
Soft-deleted user groups at the ICP level.
Inputs: limit, after_cursor. Returns: object
list_asset_groupsList asset groups (active)A
Page through every active (non-archived) asset group in the deployment. Each entry includes its membership shape — IP list, IP range, asset-id list, filter expression, etc. — and whether it surfaces as a UI tag (display_tag). Use this before creating to avoid duplicating an existing group.
Inputs: limit, after_cursor. Returns: object
list_custom_fieldsList configured custom fieldsA
Returns the asset custom-field schema for this Tenable OT tenant. Custom fields are 10 fixed slots ('customField1'..'customField10'); operators map each slot to a human label (e.g. 'Plant ID', 'CDA Type') and a value type ('PlainText' or 'HyperLink').
Call this before reading or writing custom-field values so the AI knows which labels are configured. Read tools (get_asset, query_assets) already surface values keyed by label automatically; write tools (update_asset, bulk_edit_assets) accept custom_fields keyed by label and translate to slots internally.
Inputs: no named parameters. Returns: object
list_detection_policiesList detection policiesA
Returns OT detection policies — the rules that fire events. Each policy has a level (severity), enabled / paused / archived flags, an event-type classification, and aggregate fired-event counts. Use this to audit which policies are configured, which are noisy, or which are paused. Call query_policy_findings for the per-asset hits one policy is producing.
Note: Tenable OT's policies query supports pagination only — the category / enabled / search filters below are applied client-side after fetching a page. For large deployments, increase limit to capture more before filtering.
Inputs: limit, paused, search, enabled, +1 more. Returns: object
list_eligible_tagsDiscover tags eligible for a tag groupA
List controller tags that could be added to a tag group. Filterable by asset (asset_id) and tag-value type (tag_type: one of Unknown, Int, Bool, Short, DInt, Long, Float, MultipleTagTypes). Use this before create_tag_group to discover what's available without guessing tag ids.
Inputs: limit, asset_id, tag_type, after_cursor. Returns: object
list_email_groupsList email groupsA
Page through every email group in the deployment. Each entry includes its recipients, the SMTP server it routes through, and last-modified metadata. Email groups are referenced by detection policies' actions to send alert emails.
Inputs: limit, after_cursor. Returns: object
list_em_archived_user_groupsList archived user groups (Enterprise Manager level)D
Soft-deleted EM-level user groups.
Inputs: limit, after_cursor. Returns: object
list_em_user_groupsList user groups (Enterprise Manager level)A
Page through every active EM-level user group. Each entry exposes em_level (whether the group is EM-only) plus roles and member preview.
Inputs: limit, after_cursor. Returns: object
list_network_configRead what the appliance is configured to monitorA
Returns the appliance's network CONFIGURATION — what it is set up to watch, as opposed to list_segments_and_zones and get_communication_paths, which report what it has actually observed.
Reach for this when inventory looks wrong. If an asset is missing from query_assets, the usual cause is that its subnet isn't an enabled monitored network — nothing was ever captured, so the asset cannot exist in the data.
kind selects what to read:
• monitored_networks — subnets the appliance captures. Adds a coverage block naming any DISABLED subnets, which are blind spots
• network_areas — named groupings, incl. whether each supports active queries
• zones — the coarsest grouping, above segments
• firewall_rules — appliance firewall operations/targets
• tcp_port_pinnings — IP/port pinning rules that force protocol interpretation
• network_interfaces — interfaces seen, by MAC and family
READ-ONLY.
Inputs: kind (required), after, limit, target_id. Returns: object
list_policy_relationshipsCheck what references an object before changing itA
Answers 'what breaks if I change this?' — the impact check to run BEFORE any write that touches a shared object (asset group, schedule group, port/protocol group, baseline).
Tenable OT's policy mutations are FULL REPLACES, so an edit lands completely and silently. If a live detection policy scopes to the group you're about to archive or re-scope, that policy's coverage changes with no warning. This tool surfaces those dependants first.
kind selects the relationship:
• policies_containing — policies that CONTAIN the given object id (needs target_id)
• used_in_policies — policies that USE the given object id (needs target_id). Tenable exposes both directions; check both when the answer matters.
• baseline_policies — policies bound to the network baseline (no id needed)
• ids_rules — the IDS rule catalogue with SIDs, classifications and enabled state (no id needed)
The impact block gives the verdict: how many referencing policies are actually LIVE (not disabled/archived/paused), and their titles. An archived policy referencing the object is not a reason to hold back; a live one is.
READ-ONLY.
Inputs: kind (required), after, limit, target_id. Returns: object
list_port_groupsList port groupsA
Page through every active port group. Port groups are reusable port-range bundles consumed by PortPolicy definitions.
Inputs: limit, after_cursor. Returns: object
list_protocol_groupsList protocol groupsA
Page through every active protocol group. Each item carries a protocol (TCP/UDP/MODBUS/S7/IEC104/DNP3/etc.) and optional port range.
Inputs: limit, after_cursor. Returns: object
list_rule_groupsList rule groupsA
Page through every active rule group. A rule group is a bundle of IDS rule SIDs referenced by IntrusionPolicy. Each entry includes a preview of up to 25 included rules.
Inputs: limit, after_cursor. Returns: object
list_schedule_groupsList schedule groupsA
Page through every active schedule group. Each entry surfaces its kind (one-shot TimeInterval, weekly RecurringGroup, or system ScheduleFunction) and the windows it defines. Every policy mutation's schedule argument resolves against one of these.
Inputs: limit, after_cursor. Returns: object
list_segments_and_zonesList network segments and zonesA
Returns Tenable OT's segmentation: every segment (with VLAN, subnet, asset-type filter, system flag, archived flag) and every zone (a higher-level grouping of asset groups). Use this to answer compliance questions about the IEC 62443 Zone & Conduit model, NERC CIP Electronic Security Perimeters, or NEI 08-09 defense-in-depth.
Inputs: no named parameters. Returns: object
list_sensorsList sensorsA
Returns every Tenable OT sensor in the deployment with its current status, connection / tunnel status, version, addressing, error state, and whether updates are pending. Use this to verify visibility coverage before drawing conclusions from query_assets / query_events — an offline sensor means absence of evidence, not evidence of absence.
The status filter applies client-side after fetch.
Inputs: status, connection_status. Returns: object
list_tag_groupsList tag groupsA
Page through every tag group. Tag groups bundle controller tags (by asset id + tag id) so a TagValuePolicy can fire against all members. tag_type indicates the scalar type Tenable evaluates the tag values as.
Inputs: limit, after_cursor. Returns: object
list_user_groupsList user groups (ICP-level)A
Page through every active user group at the ICP level. Each entry exposes its assigned roles and a sample of member users.
Inputs: limit, after_cursor. Returns: object
query_assetsQuery OT assetsA
Returns a list of OT assets in the Tenable OT deployment, filtered by the provided criteria. Returns up to limit (max 500) assets with each asset's identity, classification, IPs / MACs, Purdue level, segment membership, backplane (rack) membership, and aggregate risk. Call get_asset on a returned id for the per-asset bundle, or get_asset_vulnerabilities for that asset's open vulnerabilities.
total_count is the full number of assets matching the filter, independent of the page size — use it to answer 'how many' questions directly. When the match exceeds one page the response sets has_more: true and returns an end_cursor; pass that as after to fetch the next page, repeating until has_more is false to walk the entire matched set.
Filter values use natural OT vocabulary:
• kind: one of ['accesscontrolsystem', 'accesspoint', 'actuator', 'backplanemodule', 'barcodescanner', 'bms', 'camera', 'cnc', 'controller', 'converter', 'cp', 'datalogger', 'dcs', 'domaincontroller', 'drive', 'eng', 'fielddevice', 'fileserver', 'firewall', 'gateway', 'historian', 'hmi', 'hub', 'hvacmodule', 'ied', 'industrialgateway', 'industrialnetworkdevice', 'industrialprinter', 'industrialrouter', 'industrialsensor', 'industrialswitch', 'inverter', 'io', 'iot', 'lightingcontrol', 'medicaldevice', 'mobile', 'networkdevice', 'otdevice', 'otserver', 'otworkstation', 'panel', 'plc', 'powermeter', 'powersupply', 'printer', 'projector', 'radio', 'relay', 'remoteio', 'repeater', 'robot', 'router', 'rtu', 'securityappliance', 'sensor', 'serialethernetbridge', 'server', 'smarthub', 'smartsensor', 'smarttv', 'storagedevice', 'switch', 'tablet', 'tenableem', 'tenableicp', 'tenablesensor', 'threedprinter', 'unknown', 'ups', 'videomanagementsystem', 'virtualserver', 'virtualworkstation', 'voipdevice', 'webserver', 'workstation']
• category: one of 'controller', 'network', 'iot'
• criticality_at_least: one of ['none', 'low', 'medium', 'high']
• vendor: equal-match on the vendor name
• name_contains: substring match on the asset name
• tags: list of exact tag strings; matches an asset carrying ANY of them. Project-file uploads (upload_manual_asset_file) are auto-tagged by Tenable as 'Project (SN: )' — call get_asset on one known asset from an import to read its exact tag, then use it here to pull every asset that same import produced. This is the reliable way to disambiguate duplicate-named modules from different imports (e.g. a redundant PLC pair, each with its own project file, producing two identically-named 'EN01' assets that are only distinguishable by which import's tag they carry) — module name and description alone can't tell them apart.
Inputs: kind, tags, after, limit, +6 more. Returns: object
query_attack_pathwaysQuery attack-pathway data (relational, not computed)A
Returns the asset's 1-hop network neighborhood: the asset itself plus a list of peer-asset IDs it has communicated with, with the protocols and conversation count of each link. Use this AS THE GRAPH the AI walks to reason about attack paths — call again on each peer's id to expand further. The server does NOT compute paths, pick highest-risk routes, or score compromise time. That's the AI's job. Peer assets are returned as IDs only — call get_asset on each to enrich with name / vendor / type.
Inputs: entry_asset_id (required), max_peers. Returns: object
query_eventsQuery OT eventsA
Returns OT detection events matching the filter criteria, newest first. Use this to investigate alert windows, policy-firing patterns, source/dest IP context, or events in a specific time window. Each event includes the time, classification, severity, source/dest assets, the firing detection policy, and protocol/IP/MAC context. Call get_event on a returned id for full event detail.
total_count is the full number of events matching the filter, independent of the page size — use it to answer 'how many' questions directly. When the match exceeds one page the response sets has_more: true and returns an end_cursor; pass that as after to fetch the next page, repeating until has_more is false to walk the entire matched set. Event totals can be very large, so narrow with the filters (severity, time window, policy) before paging.
Filter values use natural OT vocabulary: • severity_at_least: one of 'none', 'low', 'medium', 'high' • event_type: a PolicyEventType name like 'FirmwareVersionChange', 'ConfigurationDownload', 'ProgrammingUpload', 'OperatingMode' • since / until: ISO-8601 timestamps for time-window scope
Inputs: after, limit, since, until, +7 more. Returns: object
query_plugin_definitionsQuery plugin (vulnerability) definitionsA
Returns Tenable plugin DEFINITIONS — the vulnerability checks themselves (id, name, family, severity, VPR score, and how many assets each affects). This is the catalog, NOT per-asset findings: a plugin appears here whether or not anything in your environment is affected.
For the vulnerabilities on a specific asset use get_asset_vulnerabilities. For a bulk dump of every finding instance use export_findings.
search is a single-term substring match. Structured field filters need a filter-expression shape not yet confirmed against a live deployment, so they are not exposed rather than guessed.
READ-ONLY.
Inputs: limit, search, sort_by, start_at, +1 more. Returns: object
query_policy_findingsQuery policy findingsA
Returns per-asset findings for one or more detection policies — i.e. the rows of (policy × asset × hit count) that the policies have produced. Use this to see which assets keep tripping a policy (often a tuning gap), or which assets are MITRE-mapped to a specific technique. Each finding has firstHitTime / lastHitTime, activeHits / resolvedHits, status, and joined source / destination assets.
Filter values use natural OT vocabulary: • severity_at_least: one of 'none', 'low', 'medium', 'high' • status: a FindingStatus value (e.g. 'Open', 'Resolved') • mitre_technique: a MITRE ATT&CK id (e.g. 'T1565.001') • since: ISO-8601 timestamp; findings last-seen at or after
Inputs: limit, since, search, status, +4 more. Returns: object
query_temporal_patternsQuery temporal patterns (event sequence, not motif analysis)A
Returns events in a time window, ordered chronologically, with their classification, firing policy, and source/dest IPs joined. The AI uses this raw sequence to detect patterns (e.g. config-download + firmware-change + operating-mode-change within minutes = high-priority investigation). The server does NOT detect motifs, score patterns, or label sequences.
Inputs: since (required), limit, until, event_types. Returns: object
query_vulnerabilitiesQuery OT vulnerabilitiesA
Returns Tenable plugins (vulnerabilities) matching the filter criteria. Each result includes CVEs, CVSS v3 score and vector, exploit availability flags (CISA KEV, exploit-available, exploited-by-malware), age, public disclosure date, and the official vendor solution. Call get_vulnerability on a returned plugin id to see every affected asset.
total_count is the full number of plugins matching the filter, independent of the page size — use it to answer 'how many' questions directly. When the match exceeds one page the response sets has_more: true and returns an end_cursor; pass that as after to fetch the next page, repeating until has_more is false to walk the entire matched set.
Filter values use natural OT vocabulary: • severity_at_least: one of 'info', 'low', 'medium', 'high', 'critical' • cve: a CVE substring (e.g. 'CVE-2023-25619' or 'CVE-2023' for a year-bucket) • family / source: exact-match plugin metadata
For KEV-only or exploit-available filtering, inspect the projected flags in the response — those live on plugin details and aren't filterable server-side.
Inputs: cve, after, limit, family, +3 more. Returns: object
query_vulnerability_clustersQuery vulnerability clusters (relational join, not computed)A
Returns the per-asset → vulnerabilities join the consuming AI uses to spot common CVEs across multiple assets, exploit chains (KEV + exploit-available + high criticality), or single-patch leverage points (one CVE fixing many). Two modes:
• Pass asset_ids: parallel per-asset traversal of asset.plugins, returning each asset's vulns with the same schema. The AI walks the result to find shared CVEs.
• Pass cve_substring only: global plugin search (e.g. 'CVE-2023' for a year-bucket), each plugin coming with its full affectedAssets list joined.
Both args may be combined for a per-asset CVE-filtered view. The server does NOT cluster server-side.
Inputs: asset_ids, global_limit, cve_substring, per_asset_limit, +1 more. Returns: object
summarize_environmentSummarize the OT environmentA
Returns a one-shot snapshot of the operator's OT environment — total counts and useful subtotals across assets, events, vulnerabilities, sensors, topology, and policies. Ideal as the first call when the AI doesn't yet know the deployment's shape and scale. Each section is a compact dict; subtotals are best-effort (only those Tenable's filters support are populated).
Asset criticality buckets: none / low / medium / high. Plugin severity buckets: info / low / medium / high / critical. Event subtotals split by resolved flag.
Inputs: no named parameters. Returns: object
tenable_ot_statusCheck Tenable OT connectivityA
Report whether this server can reach and authenticate to the configured Tenable OT deployment, with round-trip latency and, on failure, the underlying error. Call this first when other tools are failing, to tell an appliance/credential problem apart from a query problem.
READ-ONLY. Never returns the API key.
Inputs: no named parameters. Returns: object
Tool Schema Changelog
Recent tool additions, removals, and schema changes observed during successful MCP inspections.
Loading tool schema changelog…
TDQS
Scored across 59 tools
Most tools have clearly distinct purposes, but the large number of query/list variants (e.g., query_attack_pathways vs get_communication_paths, query_vulnerabilities vs query_vulnerability_clusters) creates some potential for misselection. Descriptions are detailed enough to resolve most ambiguity.
Tool names follow a consistent snake_case verb_noun pattern (list_, get_, query_, export_, find_), with only a few harmless outliers like summarize_environment and tenable_ot_status. The naming convention is highly predictable across the entire set.
At 59 tools, the set is far beyond the recommended 3-15 range and feels heavy, with many archived variants and overlapping query tools. While the OT security domain is broad, the count is excessive and could overwhelm an agent.
The read-only surface is broad, but the server references write tools like update_policy and create_tag_group that are not actually provided, leaving lifecycle gaps. There is also no tool to execute scans, only to view scan configurations. The missing write operations and scan execution are significant gaps.
Maintenance
Related MCP Connectors
Query your team's drift, vulnerability, and upgrade data from any AI assistant. OAuth 2.1, 51 tools.
Connect AI clients to biomedical data and tools.
OT/ICS threat intel for AI agents: 40 tools, pay per call in USDC on Base via x402.
Protocol-native energy infrastructure orchestration for AI data centers. Provides 46 MCP tools across 8 grid protocols (IEC-61850, DNP3, Modbus, OCPP, OpenADR, IEEE 2030.5, IEC 60870-5-104, ICCP) with 5 core API primitives: connect, dispatch, settle, comply, and intel. Enables AI agents to programmatically interact with substations, grid interfaces, and energy assets for real-time workload-grid coordination.
Related MCP Servers
- AlicenseAqualityDmaintenanceProvides AI agents with read access to Trane Tracer SC+ building automation systems for inventory and telemetry retrieval.81MIT
- AlicenseAqualityDmaintenanceExposes the Tenable Identity Exposure (TIE) REST API as tools for LLM clients, enabling queries on security profiles, activity timelines, AD objects, and more.155MIT
- AlicenseAqualityCmaintenanceExposes Tenable security operations as MCP tools for AI-powered security workflows, enabling asset, vulnerability, scan, plugin, and tag management via natural language.17MIT
- AlicenseNot gradedqualityBmaintenanceEnables AI assistants to query and manage OpenCTI threat intelligence data, including indicators, observables, reports, malware, and more, with read-only and optional write operations.1MIT