Same auth as ndcli
Reads the OAuth2 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 OAuth2 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 (PathFinder tunnels), 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 OAuth2 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 install ndcli# ndcli + netdefense-mcp are bundled in the same formulacurl -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.
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 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 |
Some operations stay CLI-only by design:
| Excluded | Why |
|---|---|
auth login / logout / migrate | Browser-based device flow, 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 | Long-lived PathFinder tunnel + interactive shell. Wrong shape for request/response MCP tools. |
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.
“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 NDAgent 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.