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.
Safe by default
Every destructive operation requires confirm: true. Without it, the
tool returns a preview describing exactly what would happen.
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,
device connect (interactive shell sessions), and backup encryption-key set
remain CLI-only by design.┌──────────────────────────┐ 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 directoryThe MCP server doesn’t have its own login flow. Run ndcli auth login
once on the same machine and the MCP will pick up the resulting tokens
automatically.
ndcli auth loginndcli auth show # confirm: Status: ValidAdd 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 operation that mutates production state 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. This is
the only way to mutate via the MCP.
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.
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