Same auth as ndcli
Reads the authentication tokens you already issued via ndcli auth login. No
separate login, no extra credentials.
The NetDefense MCP server (netdefense-mcp) is a Model Context Protocol
server that exposes the same operations as ndcli to AI assistants. Once
configured, an LLM-powered client like Claude Code or Claude Desktop can
list devices, sync configuration, manage VPN networks, push tasks, and
inspect snippet hierarchies — using the same authentication, the same
organization, and the same permissions as your terminal.
Same auth as ndcli
Reads the authentication tokens you already issued via ndcli auth login. No
separate login, no extra credentials.
Feature parity
Full coverage across devices, OUs, organizations, snippets, templates, sync, tasks, VPN networks, variables, and backups.
Confirm-gated mutations
Every destructive CRUD-style operation — delete, rename, remove — requires
confirm: true. Without it, the tool returns a preview describing exactly
what would happen. The console_exec tool is the exception: it runs a
command immediately, gated only by the caller’s role. See
Device console.
Read-through, no caching
Tools call the NetDefense Control Plane directly; results reflect the live state of your organization at the moment of the call.
The MCP server is a single binary, distributed alongside ndcli. It speaks
the standard MCP stdio transport: a host process (Claude Code, Claude
Desktop, etc.) launches netdefense-mcp as a subprocess, sends JSON-RPC
requests over its stdin, and reads responses from its stdout.
It is not:
ndcli.ndcli — interactive flows like auth login and
backup encryption-key set remain CLI-only by design. The interactive
device connect shell session is also CLI-only, but shell command
execution is not: the device console tools below let
an MCP client run commands on a device with administrative privilege, one
at a time, without an interactive PTY.┌──────────────────────────┐ JSON-RPC over stdio ┌──────────────────────────┐│ MCP host │ ◀──────────────────────── │ netdefense-mcp ││ (Claude Code, Desktop, │ │ (single Go binary) ││ any MCP client) │ ────────────────────────▶ │ │└──────────────────────────┘ │ internal/service layer │ │ (shared with ndcli) │ └────────────┬─────────────┘ │ HTTPS + Bearer JWT ▼ ┌────────────┴─────────────┐ │ Control Plane REST API │ └──────────────────────────┘When the host process starts the MCP server, the binary loads the same
config.yaml ndcli does, finds your stored authentication tokens (system keyring
when available, file fallback otherwise), and registers ~80 tools grouped
by domain. From that point forward, every tool call:
organization
parameter, falling back to your configured default.{ "success": true, "data": ..., "pagination": ... }
on success, or { "success": false, "error": { "code": ..., "message": ... } } on failure.Tool names follow the ndcli.<domain>.<verb> convention (snake_case for
compound verbs), so ndcli device list becomes ndcli.device.list and
ndcli device approve-all becomes ndcli.device.approve_all. Every CLI
verb that has an MCP equivalent uses this mapping.
netdefense-mcp ships in the same release as ndcli. If you already have
ndcli installed, you may already have the MCP binary too — check with:
which netdefense-mcpIf it isn’t present, install it from the same channels:
brew tap netdefense-io/tapbrew trust netdefense-io/tapbrew install ndcli# ndcli + netdefense-mcp are bundled in the same formulaHomebrew 6.0+ requires you to explicitly trust a third-party tap before installing packages from it.
curl -L https://github.com/netdefense-io/NDCLI/releases/latest/download/ndcli_linux_amd64.tar.gz | tar xzsudo mv ndcli netdefense-mcp /usr/local/bin/scoop bucket add netdefense https://github.com/netdefense-io/scoop-bucketscoop install ndcli# netdefense-mcp.exe lands in the same directorynetdefense-mcp is also listed on Smithery, which packages it as a prebuilt bundle you can install without working through the channels above.
netdefense-mcp supports two ways to authenticate, and picks between them
using the same precedence ndcli does:
NDCLI_TOKEN — a Personal Access Token, if set and valid. Requires
ndcli/netdefense-mcp 1.26.0 or later.ndcli auth login on
the same host (keyring or file store). Works on any version.NOT_AUTHENTICATED.Run ndcli auth login once on the same machine and the MCP server picks up
the resulting tokens automatically. This is the right choice on a developer
workstation where you’re already logged in interactively.
ndcli auth loginndcli auth show # confirm: Status: ValidNDCLI_TOKEN (1.26.0+)For a container, CI job, or any host with no keyring and no TTY to complete
a browser login, set NDCLI_TOKEN to a Personal Access
Token instead. Mint the token
first with ndcli auth token create (this step still requires an
interactive session — see the caveat below), then hand it to the MCP server
either as a plain environment variable:
export NDCLI_TOKEN=ndpat_your_token_herenetdefense-mcpor in your MCP client’s env config block:
{ "mcpServers": { "netdefense": { "command": "netdefense-mcp", "env": { "NDCLI_TOKEN": "ndpat_your_token_here" } } }}Add the server with the claude mcp CLI:
claude mcp add netdefense netdefense-mcpThat’s it. Open Claude Code and the NetDefense tools appear in the tool picker. To verify:
claude mcp list# netdefense netdefense-mcp (active)To remove later: claude mcp remove netdefense.
If you prefer manual configuration, edit your Claude Code settings file and add:
{ "mcpServers": { "netdefense": { "command": "netdefense-mcp" } }}Edit your Claude Desktop config file:
~/Library/Application Support/Claude/claude_desktop_config.json%APPDATA%\Claude\claude_desktop_config.jsonAdd a netdefense entry to mcpServers:
{ "mcpServers": { "netdefense": { "command": "netdefense-mcp" } }}Restart Claude Desktop. The NetDefense tools become available in any new conversation.
You can add the server using the Gemini CLI command:
gemini mcp add netdefense netdefense-mcpThen restart Gemini CLI — it auto-starts configured servers on the next launch.
For manual configuration, edit your Gemini CLI settings file:
~/.gemini/settings.json.gemini/settings.jsonAdd a netdefense entry under mcpServers:
{ "mcpServers": { "netdefense": { "command": "netdefense-mcp" } }}netdefense-mcp takes no command-line arguments and reads all configuration
from the shared ndcli config file and credentials store. No environment
variables are required.
You can add the server using the Codex CLI command:
codex mcp add netdefense -- netdefense-mcpVerify with codex mcp list.
For manual configuration, edit your Codex config file:
~/.codex/config.toml.codex/config.tomlAdd a netdefense section:
[mcp_servers.netdefense]command = "netdefense-mcp"startup_timeout_sec = 10tool_timeout_sec = 60netdefense-mcp takes no command-line arguments and reads all configuration
from the shared ndcli config file and credentials store. No environment
variables are required.
Any client that supports the standard MCP stdio transport works the same
way: launch netdefense-mcp as a subprocess and speak JSON-RPC over its
stdin/stdout. The binary takes no command-line arguments and no
environment variables — all configuration comes from the shared ndcli
config file and credentials store.
A simple smoke test:
You: List the devices in the ocp-detroit organization.Claude will pick ndcli.device.list, call it with organization: "ocp-detroit", and return a tabular summary of the devices, each with its
status, OU, version, and heartbeat.
If something is off, the tool response carries a structured error code:
| Code | Meaning |
|---|---|
NOT_AUTHENTICATED | No ndcli tokens found. Run ndcli auth login. |
AUTH_FAILED | Tokens exist but couldn’t be refreshed. Run ndcli auth login again. |
ORG_REQUIRED | The tool needs an organization field and your config has no default. Set one with ndcli config set org <name>. |
INVALID_INPUT | One of the tool’s parameters is malformed (bad enum value, missing required field). |
API_ERROR | The NetDefense Control Plane returned a non-2xx response. The message field carries the server error. |
Every CRUD-style operation that mutates organization state — rename,
delete, remove, disable — requires an explicit confirm: true in the tool
input. Without it, the tool returns a preview that describes exactly what
would happen — but does not execute.
The device console tools are a deliberate exception to
this pattern. console_exec runs a shell command on the device immediately,
gated only by the caller’s role, not by confirm. Treat an MCP client with
console access the same way you’d treat one holding an SSH key.
Example: renaming a device.
Without confirm (preview only):
{ "name": "ndcli.device.rename", "arguments": { "organization": "ocp-detroit", "device": "clarence", "new_name": "clarence-2" }}Response:
{ "success": true, "data": { "preview": true, "action": "rename", "target": "clarence → clarence-2" }, "message": "Preview: Would rename 'clarence → clarence-2'. Set confirm=true to execute."}With confirm (actually runs):
{ "name": "ndcli.device.rename", "arguments": { "organization": "ocp-detroit", "device": "clarence", "new_name": "clarence-2", "confirm": true }}In practice, Claude will surface the preview to you, ask you to confirm,
and then re-issue the call with confirm: true. You can also tell it
upfront (“yes, go ahead and rename it”) and it will skip the preview step.
The MCP server exposes a persistent device console that lets an AI agent open one connection to a device and run many diagnostic commands over it — without reconnecting for each command. This is useful when you want the AI agent to investigate a problem methodically: check interface status, read firewall counters, probe connectivity, and correlate the results, all within a single authenticated session.
A device must be enabled and registered before a console session can be opened — the same requirement as interactive device connect. Attempting to open a console to a disabled or pending device returns an error.
console_exec while another command is still running in the same
session, the call waits for the prior command to finish.timeout_seconds (maximum 3600 seconds)."truncated": true.Console sessions persist across tool calls. They auto-expire when idle
— you do not need to close them explicitly, though doing so with
console_close is good practice to release resources on the device.
ndcli.device.console_openOpens a console session to a device. Returns a session_id that you pass
to subsequent console_exec and console_close calls.
{ "name": "ndcli.device.console_open", "arguments": { "organization": "ocp-detroit", "device": "fw-hq-primary" }}Optional parameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
organization | string | config default | Organization name. Falls back to your configured default if omitted. |
connect_timeout | integer | 300 | Seconds to wait for the device to accept the connection. |
Response:
{ "success": true, "data": { "session_id": "cs_a1b2c3d4e5f6", "device": "fw-hq-primary", "org": "ocp-detroit", "expires_at": "2025-06-18T14:30:00Z" }}ndcli.device.console_execRuns a single shell command in an open session. Returns the command’s
stdout, stderr, exit_code, and a truncated flag.
{ "name": "ndcli.device.console_exec", "arguments": { "session_id": "cs_a1b2c3d4e5f6", "command": "ifconfig" }}Response:
{ "success": true, "data": { "stdout": "em0: flags=8843<UP,BROADCAST,RUNNING,SIMPLEX,MULTICAST> ...", "stderr": "", "exit_code": 0, "truncated": false }}Optional parameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
timeout_seconds | integer | 60 (max 3600) | Per-command timeout in seconds. |
binary | boolean | false | When true, returns stdout and stderr as base64-encoded bytes and adds an "encoding": "base64" field. Use this for commands that emit non-text or binary output. |
Example using binary: true to capture raw bytes:
{ "name": "ndcli.device.console_exec", "arguments": { "session_id": "cs_a1b2c3d4e5f6", "command": "cat /var/db/pkg/local.sqlite", "binary": true }}Response:
{ "success": true, "data": { "stdout": "U1FMaXRlIGZvcm1hdCAz...", "stderr": "", "exit_code": 0, "truncated": false, "encoding": "base64" }}ndcli.device.console_closeCloses the session and tears down the device-side console.
{ "name": "ndcli.device.console_close", "arguments": { "session_id": "cs_a1b2c3d4e5f6" }}Response:
{ "success": true, "data": { "session_id": "cs_a1b2c3d4e5f6", "closed": true }}ndcli.device.console_listLists all currently open console sessions. Takes no input parameters.
{ "name": "ndcli.device.console_list", "arguments": {}}Response:
{ "success": true, "data": { "sessions": [ { "session_id": "cs_a1b2c3d4e5f6", "device": "fw-hq-primary", "org": "ocp-detroit", "opened_at": "2025-06-18T14:00:00Z", "last_activity": "2025-06-18T14:04:30Z" } ], "total": 1 }}“Check the interface status and firewall packet counters on fw-hq-primary, then confirm connectivity to 8.8.8.8.”
The AI agent:
console_open for fw-hq-primary — receives session_id.console_exec with ifconfig — reads interface status and addresses.console_exec with pfctl -s info — reads firewall counters.console_exec with ping -c 3 8.8.8.8 — confirms outbound connectivity.console_close to release the session.All five steps happen over a single console session, with clean per-command exit codes so the agent can detect and report failures individually.
The catalogue covers every domain ndcli exposes, with parity tracked in
the NDCLI command surface table. At a glance:
| Domain | Tools |
|---|---|
device | list, describe, approve, approve_all, rename, remove, rebind_token, snippets |
org | list, describe, create, delete, quota, set_default_ou, invite_send/list/accept/decline/revoke, account_list/disable/enable/set_role |
ou | list, describe, create, delete, rename, device_list, add_device, remove_device, template_list/add/remove |
sync | status, apply |
task | list, describe, cancel, create (PING / SHUTDOWN / REBOOT / RESTART / PLUGIN_INSTALL) |
snippet | list, describe, create, update_content, rename, set_priority, delete, pull |
template | list, describe, create, update, delete, add_snippet, remove_snippet |
network | network/member/link/prefix CRUD (19 tools) |
variable | list, describe, create, set, delete, overview — single tool per verb with a scope enum (org/ou/template/device) |
backup | config show/create/update/delete/enable/disable/test, status, show, enable, disable |
auth | status, me, refresh |
config | show |
console | open, exec, close, list |
Some operations stay CLI-only by design:
| Excluded | Why |
|---|---|
auth login / logout / migrate | Browser-based interactive login, local keyring mutation. Run ndcli auth login once; the MCP picks up the result. |
auth delete | Account deletion behind an LLM-driven tool needs stronger out-of-band confirmation than confirm: true. |
device connect | Interactive shell session for human use. For programmatic command execution, use the console_* tools instead. |
backup encryption-key set / remove | Sensitive secret material we don’t want flowing through an LLM tool surface. |
config set / reset | Mutating local CLI configuration via a remote MCP host has awkward semantics. Run these from the terminal. |
“Show me every device that hasn’t synced in the last week, grouped by OU. If any of them are still online, queue a sync for them.”
Claude calls ndcli.device.list with a synced_before: 7d filter,
groups the result by organizational_units, cross-references
ndcli.sync.status to find which are actually out of sync, and uses
ndcli.sync.apply (after surfacing the affected device list as a preview)
to push fresh configuration. You see the preview, approve, and get a
report of what completed.
“Run a full diagnostic on fw-hq-primary: check interfaces, firewall counters, routing table, and outbound connectivity. Summarize anything that looks wrong.”
Claude opens a console session with console_open, then runs each
diagnostic command in sequence with console_exec — reading the exit
code after each one to detect failures — and closes the session with
console_close when done. The result is a structured report with
observations per check. Because the session is persistent, all commands
run over a single authenticated connection to the device without
reconnecting between checks.
“What snippets actually apply to clarence in ocp-detroit? Walk me through how they’re wired together.”
Claude invokes ndcli.device.snippets, which traverses
device → OUs → templates → snippets and dedupes by priority. The result
is a single JSON document describing every snippet that will be installed
on that device, why, and which template it came from.
“Add murphy01 as a hub in the net-test-1 network and publish its network_lan_users prefix.”
Claude chains ndcli.network.member_add with role: HUB and
ndcli.network.prefix_add for the appropriate variable. Each destructive
step pauses for confirmation, and Claude shows you the resulting
connectivity from ndcli.network.link_list’s effective-connection view
(automatic + override + manual links, classified per pair).
“A new device just came online called rc-3000. Approve it, attach it to the policedept OU, and trigger an initial sync.”
Claude runs ndcli.device.approve → ndcli.ou.add_device →
ndcli.sync.apply, surfacing the configuration that will be sent before
the final sync executes.
“List every variable used across our org and show me which devices override the default for
network_lan_users.”
ndcli.variable.overview returns one entry per variable name with all
its definitions across scopes (org/ou/template/device), so Claude can
filter for network_lan_users and report exactly which devices/OUs have
overrides and what values they use.
The host process can’t find netdefense-mcp. Use the absolute path
in the configuration. which netdefense-mcp prints it.
Tools return NOT_AUTHENTICATED even though ndcli auth show works.
The MCP host might launch from a context that can’t reach your keyring
(e.g. some Linux desktop session managers). Run
ndcli auth migrate and switch storage to the file backend, or launch
the host process from a terminal session that has keyring access.
Tools return NOT_AUTHENTICATED even though NDCLI_TOKEN is set.
NDCLI_TOKEN support requires netdefense-mcp 1.26.0 or later — check
your version with netdefense-mcp --version (or ndcli version, they ship
together). On older builds the variable is silently ignored and the server
falls back to the cached ndcli auth login session; if none exists, every
call fails NOT_AUTHENTICATED. On 1.26.0+, also confirm the token itself is
still valid — an expired or malformed NDCLI_TOKEN fails the server at
startup rather than falling back (see the caution above), so check the MCP
host’s stderr/logs for a startup error rather than assuming it’s a
config-detection problem.
Token create/revoke tools fail while authenticated via NDCLI_TOKEN.
This is expected, not a bug: minting or revoking a PAT requires a real
interactive session. Run ndcli auth token create / revoke from a
terminal where you can ndcli auth login, then update the secret the
headless host reads NDCLI_TOKEN from. token_list is unaffected and
works under either auth mode.
A tool succeeded but the side effect didn’t appear. Most likely the
underlying agent on the device is offline. Check ndcli device list --heartbeat-after 5m for fresh connections.
An operation reports a 5xx with a pydantic validation error. That’s
a Control Plane-side bug — the operation may have actually succeeded.
Verify state via the matching *.list or *.describe tool, and report
the specific endpoint so we can fix the response schema.
console_exec returns "truncated": true. The command produced more
than ~1 MiB of output. Narrow the command (e.g. pipe through grep or
head) or use binary: true and decode the result client-side.
console_open returns an error for a device that appears online. The
device must be in the ENABLED state. Check with ndcli device describe <device> and approve it first if it is still PENDING.
ndcli device connect for human shell sessions