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

  • 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 (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:

  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 trust netdefense-io/tap
brew install ndcli
# ndcli + netdefense-mcp are bundled in the same formula

Homebrew 6.0+ requires you to explicitly trust a third-party tap before installing packages from it.

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.

You can add the server using the Gemini CLI command:

Terminal window
gemini mcp add netdefense netdefense-mcp

Then restart Gemini CLI — it auto-starts configured servers on the next launch.

For manual configuration, edit your Gemini CLI settings file:

  • User-level (all projects): ~/.gemini/settings.json
  • Project-level (current directory): .gemini/settings.json

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

Terminal window
codex mcp add netdefense -- netdefense-mcp

Verify with codex mcp list.

For manual configuration, edit your Codex config file:

  • User-level: ~/.codex/config.toml
  • Project-level: .codex/config.toml

Add a netdefense section:

[mcp_servers.netdefense]
command = "netdefense-mcp"
startup_timeout_sec = 10
tool_timeout_sec = 60

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.

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

  • Commands run with administrative privilege on the device.
  • Only one command runs at a time within a session. If you issue console_exec while another command is still running in the same session, the call waits for the prior command to finish.
  • Each command has a default timeout of 60 seconds, configurable per call via timeout_seconds (maximum 3600 seconds).
  • Output is capped at approximately 1 MiB per command. If the output exceeds this limit, the response includes "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.

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

ParameterTypeDefaultDescription
organizationstringconfig defaultOrganization name. Falls back to your configured default if omitted.
connect_timeoutinteger300Seconds 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"
}
}

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

ParameterTypeDefaultDescription
timeout_secondsinteger60 (max 3600)Per-command timeout in seconds.
binarybooleanfalseWhen 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"
}
}

Closes 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
}
}

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

  1. Calls console_open for fw-hq-primary — receives session_id.
  2. Calls console_exec with ifconfig — reads interface status and addresses.
  3. Calls console_exec with pfctl -s info — reads firewall counters.
  4. Calls console_exec with ping -c 3 8.8.8.8 — confirms outbound connectivity.
  5. Calls console_close to release the session.
  6. Summarizes findings: which interfaces are up, whether packet counts look normal, and whether the ping succeeded.

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:

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
consoleopen, exec, close, list

Some operations stay CLI-only by design:

ExcludedWhy
auth login / logout / migrateBrowser-based interactive login, 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 connectInteractive shell session for human use. For programmatic command execution, use the console_* tools instead.
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.

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