Skip to content

NetDefense MCP Server

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:

  • A daemon — there is no long-running service to install or supervise.
  • A separate authentication system — it shares storage with ndcli.
  • A web server — there is no HTTP listener and no port to expose.
  • A replacement for 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:

  1. Verifies the access token is valid (refreshes silently if needed).
  2. Resolves the target organization from the call’s organization parameter, falling back to your configured default.
  3. Calls the matching method on the shared service layer.
  4. Returns a JSON-shaped response: { "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:

Terminal window
which netdefense-mcp

If it isn’t present, install it from the same channels:

Terminal window
brew tap netdefense-io/tap
brew install ndcli
# ndcli + netdefense-mcp are bundled in the same formula

The 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.

Terminal window
ndcli auth login
ndcli auth show # confirm: Status: Valid

Add the server with the claude mcp CLI:

Terminal window
claude mcp add netdefense netdefense-mcp

That’s it. Open Claude Code and the NetDefense tools appear in the tool picker. To verify:

Terminal window
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

Add 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:

CodeMeaning
NOT_AUTHENTICATEDNo ndcli tokens found. Run ndcli auth login.
AUTH_FAILEDTokens exist but couldn’t be refreshed. Run ndcli auth login again.
ORG_REQUIREDThe tool needs an organization field and your config has no default. Set one with ndcli config set org <name>.
INVALID_INPUTOne of the tool’s parameters is malformed (bad enum value, missing required field).
API_ERRORThe 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:

DomainTools
devicelist, describe, approve, approve_all, rename, remove, rebind_token, snippets
orglist, describe, create, delete, quota, set_default_ou, invite_send/list/accept/decline/revoke, account_list/disable/enable/set_role
oulist, describe, create, delete, rename, device_list, add_device, remove_device, template_list/add/remove
syncstatus, apply
tasklist, describe, cancel, create (PING / SHUTDOWN / REBOOT / RESTART / PLUGIN_INSTALL)
snippetlist, describe, create, update_content, rename, set_priority, delete, pull
templatelist, describe, create, update, delete, add_snippet, remove_snippet
networknetwork/member/link/prefix CRUD (19 tools)
variablelist, describe, create, set, delete, overview — single tool per verb with a scope enum (org/ou/template/device)
backupconfig show/create/update/delete/enable/disable/test, status, show, enable, disable
authstatus, me, refresh
configshow

Some operations stay CLI-only by design:

ExcludedWhy
auth login / logout / migrateBrowser-based device flow, local keyring mutation. Run ndcli auth login once; the MCP picks up the result.
auth deleteAccount deletion behind an LLM-driven tool needs stronger out-of-band confirmation than confirm: true.
device connectLong-lived PathFinder tunnel + interactive shell. Wrong shape for request/response MCP tools.
backup encryption-key set / removeSensitive secret material we don’t want flowing through an LLM tool surface.
config set / resetMutating 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.approvendcli.ou.add_devicendcli.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.