# Tenable OT Security MCP Server

> Tenable OT Security asset, vulnerability, event and policy tools, with an opt-in dry-run-by-default write layer.

- Type: MCP server
- Author: aqamahn
- License: Apache-2.0
- Trust tier: contributed (passed automated validation and a baseline maintainer review)
- Last reviewed: 2026-08-04
- Source repository: https://github.com/aqamahn/MCP-server-Tenable-OT-V2
- Repo state: not archived
- Last push: 2026-09-06T15:02:11Z
- Stars: 0 (a popularity signal, not a safety signal)
- Forks: 0 (a popularity signal, not a safety signal)
- Open issues: 0
- Language: Python
- Health checked at: 2026-11-05T20:35:15.821Z
- Integrations: Tenable
- Domains: ot-iot-security
- Tags: mcp-server, ot-security, ics, scada, asset-inventory, vulnerability-management, audit-logging
- Date added: 2026-08-01
- Transport: both
- Runtime: python
- Auth method: api-key
- Compatible clients: Claude Desktop, Claude Code
- Tools:
  - `tenable_ot_status`: Check Tenable OT connectivity
  - `query_assets`: Query OT assets
  - `get_asset`: Get one OT asset
  - `get_asset_vulnerabilities`: Get vulnerabilities for one asset
  - `list_custom_fields`: List configured custom fields
  - `get_manual_asset_upload_status`: Get manual asset-upload processing status
  - `query_attack_pathways`: Query attack-pathway data (relational, not computed)
  - `query_vulnerability_clusters`: Query vulnerability clusters (relational join, not computed)
  - `query_temporal_patterns`: Query temporal patterns (event sequence, not motif analysis)
  - `get_asset_intelligence`: Get asset intelligence bundle (joined data, not narrative)
  - `query_events`: Query OT events
  - `get_event`: Get one OT event
  - `export_assets`: Bulk export assets
  - `export_plugin_definitions`: Bulk export plugin definitions
  - `export_findings`: Bulk export findings
  - `query_plugin_definitions`: Query plugin (vulnerability) definitions
  - `get_plugin_definition`: Get one plugin definition
  - `list_detection_policies`: List detection policies
  - `get_policy`: Get one policy's full configuration
  - `query_policy_findings`: Query policy findings
  - `get_schema_enums`: Introspect the Tenable OT GraphQL schema
  - `list_sensors`: List sensors
  - `summarize_environment`: Summarize the OT environment
  - `list_segments_and_zones`: List network segments and zones
  - `get_communication_paths`: Get communication paths for an asset
  - `query_vulnerabilities`: Query OT vulnerabilities
  - `get_vulnerability`: Get one vulnerability
  - `list_asset_groups`: List asset groups (active)
  - `list_archived_asset_groups`: List archived asset groups
  - `get_asset_group`: Get an asset group by id
  - `list_email_groups`: List email groups
  - `get_email_group`: Get an email group by id
  - `find_email_groups_using_smtp_server`: Find email groups that route through a given SMTP server
  - `list_schedule_groups`: List schedule groups
  - `list_archived_schedule_groups`: List archived schedule groups
  - `get_schedule_group`: Get a schedule group by id
  - `list_tag_groups`: List tag groups
  - `get_tag_group`: Get a tag group by id
  - `list_eligible_tags`: Discover tags eligible for a tag group
  - `list_rule_groups`: List rule groups
  - `list_archived_rule_groups`: List archived rule groups
  - `get_rule_group`: Get a rule group by id
  - `list_port_groups`: List port groups
  - `list_archived_port_groups`: List archived port groups
  - `get_port_group`: Get a port group by id
  - `list_protocol_groups`: List protocol groups
  - `list_archived_protocol_groups`: List archived protocol groups
  - `get_protocol_group`: Get a protocol group by id
  - `list_user_groups`: List user groups (ICP-level)
  - `list_archived_user_groups`: List archived user groups (ICP-level)
  - `get_user_group`: Get a user group by id (ICP-level)
  - `list_em_user_groups`: List user groups (Enterprise Manager level)
  - `list_em_archived_user_groups`: List archived user groups (Enterprise Manager level)
  - `get_em_user_group`: Get a user group by id (Enterprise Manager level)
  - `list_active_scans`: List active scans
  - `get_active_scan`: Get one active scan
  - `get_active_scan_executions`: Get past executions of an active scan
  - `list_network_config`: Read what the appliance is configured to monitor
  - `list_policy_relationships`: Check what references an object before changing it
- Listing page: https://exchange.tenable.com/mcp-servers/tenable-ot-security-mcp-server
- Source file: https://github.com/tenable/cyberagents-exchange/blob/main/mcp-servers/tenable-ot-security-mcp-server.md

An MCP server that connects any MCP client — Claude Desktop, Claude Code, or
another — to a Tenable OT Security deployment, exposing OT/ICS asset inventory,
vulnerabilities, events, detection policies and policy findings as read tools,
plus an opt-in write layer for asset edits, group and policy management, scan
definitions and finding resolution.

Because this server talks to industrial control systems, its safety model is
structural rather than advisory: writes are invisible until explicitly enabled,
every write previews before it acts, destructive operations need a second
confirm flag, and every write attempt is recorded to an append-only audit log.

> Not an official Tenable product. Copyright (c) 2026 Tenable, Inc., released
> under Apache 2.0, but shipped with no SLA, no warranty and no support
> commitment, and not covered by Tenable's product security or
> vulnerability-disclosure processes.

## What it does

**59 read tools, always available.** Asset inventory with identity, IPs/MACs,
Purdue level, segment and backplane membership and aggregate risk; fleet-wide
and per-asset vulnerability search with CVE, CVSS and exploit/KEV flags; OT
events; detection policies and their per-asset findings; the plugin
(vulnerability) definition catalog; and bulk exports of assets, findings and
plugin definitions via pyTenable.

Beyond raw inventory it covers the environment's shape and its dependencies:
network segments and zones as observed, per-asset communication paths, sensor
inventory, and `list_network_config` — what the appliance is *configured* to
watch, which flags disabled subnets as monitoring blind spots and is the usual
answer to "why is this asset missing". `list_policy_relationships` answers
"what breaks if I change this?" before a write lands, which matters because
Tenable OT's policy mutations are full replaces, not patches.
`summarize_environment` gives a one-call orientation on an unfamiliar
deployment, and `get_schema_enums` introspects the appliance's own GraphQL
schema.

**71 write tools, only when `TENABLE_OT_WRITE_TOOLS_ENABLED=1`.** Asset
lifecycle (hide/restore, bulk variants, removal by address, merge, risk
recalculation, manual file upload), asset properties and custom-field schema,
detection policy enable/disable/archive and full policy editing, finding
resolution, group management across asset / email / schedule / tag / rule /
port / protocol / user groups, sensor management, and scan *definition*
create/edit.

Nothing in the codebase runs a scan. There is no run, start, or execute
mutation anywhere in it — definitions can be written, and `enable_active_scan`
sets whether a definition participates in runs, but only a human starting a
scan from the Tenable OT UI ever launches one against live equipment.

## How it works

A Python 3.12+ server on the MCP SDK, wrapping Tenable OT's GraphQL API
directly with `httpx` for everything Tenable doesn't officially cover, and
bridging to pyTenable for the bulk-export surface. Most paginated read tools
are declared as data — a `ConnectionSpec` record naming a query and a field
projection — and one registrar turns specs into tools, so the pagination and
projection plumbing exists and is tested once rather than being re-implemented
per tool. Composite tools that fan out or join across queries stay
hand-written. Queries whose availability varies by appliance are marked
unconfirmed and return an actionable `error` envelope naming what to
introspect, instead of raising.

The four safety properties are enforced in the architecture, not by
convention:

1. **Write tools are absent, not merely refused.** Without the opt-in
   environment variable the write modules are never imported, so no write tool
   name appears in the MCP `tools/list` response. A client cannot call — or
   discover — an operation that wasn't enabled.
2. **Every write defaults to `dry_run=True`,** returning the exact payload that
   *would* be sent without sending it, so the operator approves a concrete
   preview before anything changes.
3. **Destructive operations require a second explicit flag.** `dry_run=false`
   alone is insufficient; `delete_custom_field`, for instance, is rejected
   without `confirm_wipes_values=True`.
4. **Every write call is audited** — dry-run, live, or failed — as one JSON
   line recording timestamp, tool, parameters, dry-run flag, outcome and error.
   The audit writer fails closed: if it cannot record, the call raises rather
   than proceeding silently.

Credentials come from the environment only. The API key is read from
`TOT_API_KEY`, held privately by the client, and is 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.

Ships with a Dockerfile and compose file. The container entrypoint fixes
bind-mount ownership for the audit directory and proves the target uid can
actually write there before permanently dropping from root via `setpriv` — a
failure mode that otherwise surfaces late, only on the first write. The test
suite runs entirely against a mocked client with no network, appliance or
credentials, covering dry-run paths, live paths, audit-entry assertions and
confirm-flag rejection paths.

This is the Markdown version of one of 140 listings on CyberAgents Exchange. The same listing as JSON: https://exchange.tenable.com/api/mcp-servers/tenable-ot-security-mcp-server.json. The full index and how to read it: https://exchange.tenable.com/llms.txt
