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 "Install 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 Serverlist 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.
Maintenance
Resources
Unclaimed servers have limited discoverability.
Looking for Admin?
If you are the server author, to access and configure the admin panel.
Related MCP Servers
- AlicenseAqualityCmaintenanceProvides AI agents with read access to Trane Tracer SC+ building automation systems for inventory and telemetry retrieval.Last updated8MIT
- AlicenseAqualityBmaintenanceExposes the Tenable Identity Exposure (TIE) REST API as tools for LLM clients, enabling queries on security profiles, activity timelines, AD objects, and more.Last updated153MIT
- AlicenseAqualityCmaintenanceExposes Tenable security operations as MCP tools for AI-powered security workflows, enabling asset, vulnerability, scan, plugin, and tag management via natural language.Last updated17MIT
- Alicense-qualityBmaintenanceEnables AI assistants to query and manage OpenCTI threat intelligence data, including indicators, observables, reports, malware, and more, with read-only and optional write operations.Last updatedMIT
Related MCP Connectors
Runtime permission, approval, and audit layer for AI agent tool execution.
CVE lookups (NVD) and dependency-manifest audits (OSV) for AI agents. No API keys.
CVE lookups (NVD) and dependency-manifest audits (OSV) for AI agents. No API keys.
Latest Blog Posts
- Who's Calling? MCP Hosts Are an Identity Blind Spot (And the Spec Knows It)By Om-Shree-0709 on .mcpAgent IdentityOAuth 2.1
- Your AI Chatbot Just Exposed Your CEO's Salary to an InternBy Om-Shree-0709 on .Agent IdentityMCP SecurityOAuth Delegation
- Why MCP Servers Need Execution Sandboxing (And Why Your Current Stack Isn't Enough)By Om-Shree-0709 on .Agentic AiPrompt InjectionWebAssembly
MCP directory API
We provide all the information about MCP servers via our MCP API.
curl -X GET 'https://glama.ai/api/mcp/v1/servers/aqamahn/MCP-server-Tenable-OT-V2'
If you have feedback or need assistance with the MCP directory API, please join our Discord server