DEV Community

Cover image for Building Your First MCP Server for Network Devices - Part 2
Uzi Golan
Uzi Golan

Posted on

Building Your First MCP Server for Network Devices - Part 2

This is Part 2 of a 7-part series on AI-assisted network operations with MCP and Agent Skills. Part 1 explained why your network devices need an MCP server. This article builds the first useful version.


Table of Contents


In Part 1, we argued that the shift from personal AI to team AI requires shared infrastructure MCP servers and Skills that are company assets, not personal tools. We covered six principles: team AI, token efficiency, evolution, filtering, Lego blocks, and future-proofing.

Now we build the foundation. Not the full toolkit not yet. The smallest useful MCP server: device inventory, credential separation, a driver abstraction, safe read tools, a health check, and config backups. No writes, no knowledge catalog, no fancy retrieval. Just the read-only foundation that everything else builds on.

By the end of this article, you'll have a server that can manage your device inventory, connect to devices over SSH, run whitelisted read commands, check device health, and back up configurations all through typed, bounded MCP tools that any AI client can call.


The Smallest Useful MCP Server

Before writing any code, define what the first milestone is and what it isn't.

What it does:

  • Manage a device inventory (add, list, update, remove)
  • Connect to devices over SSH (or telnet where SSH isn't available)
  • Run whitelisted read commands (show commands, info dumps)
  • Check device health (identity, active alarms)
  • Back up configurations to a local archive

What it doesn't do yet:

  • No configuration writes (that's Article 3 the safety model)
  • No knowledge catalog (that's Article 5 CLI harvesting, manuals, MIBs)
  • No SNMP operations (that's also Article 5)
  • No multi-client deployment (that's Article 6)
  • No fusion prompts spanning every layer (that's Article 7)

This is the seed. It grows through usage each missing capability is discovered when an engineer asks a question the current tools can't answer.


Inventory Before Tools

The first tool isn't a CLI command it's the device inventory. Before the AI can reach a device, it needs to know the device exists.

The Six-Fact Intake Gate

Adding a device requires six facts: name, host, family, group, user, and password. If you omit any field, the agent asks for all missing ones in a single question not one at a time:

"rad agent, add my device: name lab-etx2, host 172.17.163.205, family etx2, group lab, user su, password 1234"

The intake gate enforces all six before anything is written. Why six?

  • Name the human handle the team uses ("lab-etx2", "marks-mp4", "sf-163-187")
  • Host the IP address or hostname
  • Family the device family identifier that selects the CLI dialect ("etx2", "secflow", "mp4100", "minid")
  • Group an organizational tag for filtering ("lab", "production", "site-A")
  • User the SSH username
  • Password the SSH password

Credential Separation

This is the first safety decision, and it's non-negotiable: credentials never live in the inventory file. The inventory stores name, host, family, and group. Credentials go to a gitignored .env file or OS keychain. The inventory file is safe to commit to Git, share with the team, or display in a tool response. Credentials are never exposed in tool arguments, responses, or audit logs.

inventory.yaml          ← name, host, family, group (safe to commit)
server/.env             ← user, password (gitignored, chmod 600)
Enter fullscreen mode Exit fullscreen mode

Listing Devices

Once devices are registered, listing them is credential-free:

"noam, show the list of devices"

Returns name, host, family, and groups no credentials. Filterable: "list only the mp1 family" narrows by family, "list the lab group" narrows by group.

Updating and Removing Devices

"abayev, marks-mp4 moved update its host to 172.17.161.95"

Partial update: only the named field changes. Changing family is treated as suspicious (usually a mis-registration) and asked back.

"rad agent, remove lab-etx2 from the list"

Requires explicit confirmation. Removing a device only forgets the inventory entry it never touches the device, its backups, or its audit history. Those persist.


The Driver Abstraction

This is the technical center of the server. The insight: transport and CLI dialect vary independently. SSH and telnet are transports. The CLI dialect (prompt format, context navigation, commit behavior, port naming) is per device family. These two dimensions must be separated.

Backend × Driver

Tools (product-agnostic)     run_show, health_check, get_config, backup_config
        │
Backends (transport)         ssh / telnet (Netmiko)
        │
Drivers (CLI dialect)        radcli.py  shared context-CLI dialect
        │                      ├── secflow   (SF-1p)
        │                      ├── etx1p     (ETX-1p)
        │                      ├── etx2      (ETX-203AX/205A/220A/ETX-2I)
        │                      ├── mp4100    (Megaplex-4100)
        │                      ├── mp1       (MP-1)
        │                      ├── minid     (MiNID)
        │                      └── etx2v     (ETX-2V uCPE-OS)
        │
Devices                      inventory.yaml
Enter fullscreen mode Exit fullscreen mode

Figure 1 Driver abstraction + seed that grows

Tools stay product-agnostic verbs. The device's family field picks the driver. The inventory's transport field picks the backend. An SSH session can drive a SecFlow. A telnet session can drive an ETX-2. The tool doesn't care it calls the driver, the driver calls the backend.

Why This Matters

Most network automation code couples transport and dialect: one script that SSHes to a specific device type and runs specific commands. That works for one family. When you add a second family, you copy the script and modify the dialect-specific parts. By the third family, you have three copies that drift.

The driver abstraction prevents this. The shared dialect lives in one base class. Family-specific differences live in subclasses that override only what's different:

class RadCLIDriver:
    """Shared context-CLI dialect  prompt format, context navigation,
    show-command whitelists, config export."""

    def connect(self, host, user, password): ...
    def run_show(self, context: str, command: str) -> str: ...
    def get_config(self) -> str: ...
    def health_check(self) -> HealthResult: ...

class SecFlowDriver(RadCLIDriver):
    """SF-1p: standard SSH, context CLI, direct-write save."""
    pass  # mostly inherited

class MP4100Driver(RadCLIDriver):
    """Megaplex-4100: candidate-DB commit model, mandatory recipe."""
    def commit_recipe(self): ...
    # discard-changes → configure → sanity-check → commit → save

class MiNIDDriver(RadCLIDriver):
    """MiNID: fragile/unique SSH, patient connect profile, bare 'more...' pager."""
    def connect(self, host, user, password):
        # patient per-family connect profile
        ...
Enter fullscreen mode Exit fullscreen mode

What Varies by Family

The differences are real and discovered through usage:

Aspect SecFlow (SF-1p) ETX-2 Megaplex-4100 MiNID
SSH behavior Standard Standard Standard Fragile patient profile needed
Port naming ethernet 3 ethernet 0/2 ethernet 1/1/1 ethernet 1
Commit model Direct write Direct write Candidate-DB + mandatory recipe Direct save
Pager None None None Bare more... (paginates)
Prompt SF-1p# ETX-2I# MP-4100# MiNID#

A tool that doesn't account for these differences will work on one family and fail silently on another. The driver abstraction makes the differences explicit and testable not hidden in if/else branches scattered across the codebase.


The Netmiko Backend

Netmiko handles SSH session management connection, authentication, command execution, output reading. It supports the rad_etx device type for RAD equipment, plus cisco_ios, junos, arista_eos, huawei_vrp, and dozens more. If you're already using Netmiko or NAPALM, the MCP server wraps your existing automation primitives behind typed, AI-callable tools.

But Netmiko is the transport, not the policy. The MCP server owns the policy: what commands are whitelisted, how output is bounded, what requires confirmation, what's logged. Netmiko gets you to the device; the server decides what's safe to do once you're there.

One Persistent Session Per Device

SSH connect costs 5-7 seconds, and many network devices refuse a new session while the old one tears down. So sessions are cached: one persistent CLI session per device, re-grounded with exit all before each call, liveness-probed after 60 seconds of idle, and replaced transparently when dead.

This is a performance decision, but it's also a filtering decision: the agent doesn't open a new SSH session for every tool call. It reuses the existing session, which means the device's session table isn't flooded, and the agent can chain multiple reads in a single conversation without reconnection overhead.

Prompt-Anchored Reads, Never Quiet Timers

Every read terminates the moment the device prompt reappears not after a quiet-gap timeout. This matters more than it sounds. The SecFlow-1p deterministically pauses more than 3 seconds mid-info dump. A short quiet threshold silently truncates the output this once hid an entire router 1 subtree from the CLI harvester because the read ended before the device finished sending.

Prompt-anchored reads are deterministic. Quiet-timer reads are probabilistic. In network operations, deterministic always wins.


The First Tools

Each tool follows the same pattern: the user asks in plain language, the agent calls the tool, the tool executes against the device (after confirmation for reads that contact the device), and the result is returned with provenance.

add_device Register a New Device

"rad agent, add my device: name lab-etx2, host 172.17.163.205, family etx2, group lab, user su, password 1234"

The six-fact intake gate. All facts required before anything is written. Credentials go to .env, inventory facts go to inventory.yaml. If you omit a field, the agent asks for all missing ones in a single question.

Guardrail: Credentials are never stored in the inventory file. The inventory is safe to share.

list_devices Show the Inventory

"noam, show the list of devices"

Returns name, host, family, and groups for every device. Credential-free output. Filterable by family or group.

Guardrail: No credentials in the response. Ever.

test_connectivity SSH Reachability Check

"rad agent, can you reach lab-etx2?"

SSH (or telnet, per the device's transport field) reachability and auth check. Doesn't run any commands just confirms the session can be established.

Guardrail: Read-only. No device state changed.

run_show Whitelisted Read Commands

"abayev, show the active alarms on sf-163-187"

"noam, show the ports status on ehud1p"

The agent navigates the right context (configure reportingshow active-alarms, or configure portshow summary) and runs the command. Output is interpreted: alarm severity (a major or critical alarm blocks config work by policy), port status (up/down/errors).

Port naming is family-specific the agent uses the target family's convention. A SecFlow uses ethernet 3, an ETX-1p uses ethernet lan1, an ETX-2 uses ethernet 0/2. The driver knows; the tool doesn't hardcode.

Guardrail: Only whitelisted read prefixes. No raw shell strings. Contexts are validated against known contexts. A strict token charset prevents command injection.

run_show_in_context Scoped Reads

Some device families scope show commands to specific contexts you can't run show active-alarms from the root prompt, you have to be in configure reporting first. run_show_in_context handles the navigation: enter the context, run the command, exit back to root.

Guardrail: Context path is validated against the driver's known-context list. Unknown contexts are rejected, not guessed.

cli_help Relay the ? Help Tree

"rad agent, what commands are available under configure protection erp on the ETX-2?"

cli_help types <prefix>? into the device's CLI and captures the help output, then clears the pending line with Ctrl-U. It never executes anything it's a read of the device's own help system.

Guardrail: Newlines and control characters in the prefix are refused. The tool types ? and clears the line nothing is ever executed. This is how the CLI harvester (Article 5) discovers the command tree.

get_config Full Config Export

"noam, back up the configuration of minid-1 and diff it against the previous backup"

Full info export to the local archive (server/backups/), with diffs against any earlier snapshot. The backup is timestamped, hashed, and stored. Diffs are line-by-line with context.

Guardrail: Read-only. No config changes. The backup archive is append-only old snapshots are never overwritten.

health_check Driver-Defined Health Sweep

"rad agent, run a health check on lab-sf1p"

The driver defines what "health" means for its family. Typically: device identity (serial, firmware, uptime), active alarms (interpreted with severity), and a basic connectivity sanity check. The result is structured not raw CLI output, but a HealthResult with fields the model can reason about.

Guardrail: Read-only. The health check is a composite of existing read tools, not a new device-side operation.


Bounded Data, Not Raw Dumps

The most important design principle for fetch tools: return the right amount of data in the right shape, not everything the device sends.

Figure 2 Bounded data (Scope / Shape / Granularity)

A show command that returns 20 lines on one device family returns 2,000 on another. A get_config export can be 15,000 lines. Dumping that into the model's context burns tokens, overwhelms the reasoning, and produces worse answers not better.

Three Filtering Directions

Every fetch tool needs filtering in three directions, and you discover the right filters through usage:

Scope filtering return less, but the right less. run_show doesn't accept any command string; it accepts only whitelisted prefixes. The whitelist is per-family what's safe on a SecFlow might not exist on an ETX-2. The scope narrows the data before it reaches the model.

Shape filtering return structured, not raw. Raw CLI output is verbose, repetitive, and hard for the model to parse. The health_check tool doesn't return raw show output it returns a HealthResult with identity, alarms, and severity as structured fields. The model reads structured data faster and more accurately than free-form CLI text.

Granularity filtering return the right level of detail. "Show me the active alarms" is one granularity. "Show me the full alarm dictionary with meanings and recommended actions" is another. "Show me the config hierarchy" is a third. One tool can't serve all three and you discover the granularities only when engineers ask questions the existing tool can't answer well.

The Search-Then-Read Pattern

For larger data sets, the pattern is: first search returns compact results with excerpts and source locators. Then a follow-up tool fetches one chunk by stable ID only if the answer isn't in the excerpt. This keeps the model's context small and the evidence traceable.

This is the same bounded-research principle from the cli-generator project: deterministic routing before AI reasoning, bounded research instead of open-ended tool access, and validation outside the model. The infrastructure does the heavy lifting so the model doesn't have to which means fewer tokens and the ability to run with smaller or self-hosted models.


Family-Specific Examples

Active alarms with interpretation:

"abayev, show the active alarms on sf-163-187"

The agent navigates configure reportingshow active-alarms and interprets severity. A major or critical alarm blocks config work by policy the agent states this explicitly, not just "you have alarms."

Ports status with family-specific naming:

"noam, show the ports status on ehud1p"

configure portshow summary. The agent uses the ETX-1p's port naming convention (ethernet lan1, ethernet lan2), not the SecFlow's (ethernet 3) or the ETX-2's (ethernet 0/2). The driver knows; the tool doesn't hardcode.

Config backup with diff:

"noam, back up the configuration of minid-1 and diff it against the previous backup"

Full info export to the local archive, with line-by-line diffs against any earlier snapshot. The backup is timestamped and hashed. The diff shows what changed since the last backup useful for change tracking and drift detection.

Health check with structured output:

"rad agent, run a health check on lab-sf1p"

Returns device identity (serial, firmware, uptime), active alarms (with severity and meaning), and a connectivity sanity check. Structured output, not raw CLI text the model can reason about the fields directly.


What This Server Does Not Do Yet

Being explicit about what's missing is as important as building what's there.

No configuration writes. The server has read tools only. No stage_config, no commit_config, no save_startup. Writes require the safety model staged commits, diff previews, explicit approval, audit trails. That's Article 3.

No knowledge catalog. The server can reach devices and read their output, but it doesn't know what the commands mean, what the alarm dictionary says, or which features exist on which firmware. The knowledge layers CLI harvesting, manual ingestion, SNMP MIBs, datasheets are Article 5.

No SNMP operations. The CLI is the only device access path. SNMP gives you a second window into device state identity, interface counters, alarm traps without opening an SSH session. That's also Article 5.

No multi-client deployment. The server runs locally over stdio. Sharing it across a team requires HTTP transport, token-based access control, and a collaboration hub. That's Article 6.

No claim that "having tools" means the solution is optimized. The tools you build in this article fetch data and return it. That's the starting point. Over time, every fetch tool will need scope filtering, shape filtering, and granularity filtering discovered when engineers ask questions your tools can't answer well. The server grows with your operations.


The First Version Is a Seed

The server described in this article is the weekend project from Part 1. It has inventory, driver abstractions, SSH backends, read tools, health checks, and config backups. It's enough to prove the concept. It's not enough for production.

Over the next articles, every layer we add was discovered through real usage, not planned upfront:

  • Safety model (Article 3) because writes without staging, backup, and approval are unacceptable
  • Agent Skills (Article 4) because tools alone don't know what to check, in what order, or what "good" looks like
  • Knowledge layers (Article 5) because the ? help tree, user manuals, datasheets, and SNMP MIBs are four separate knowledge domains the AI needs
  • Multi-client deployment (Article 6) because an MCP server on one laptop is a prototype, not infrastructure
  • Fusion prompts (Article 7) because the real value emerges when every layer works together in a single prompt

When you build your own, expect the same trajectory. Your first version will be wrong in ways you can't predict and that's normal. The CLI dialects you didn't account for, the port naming conventions that differ by family, the SSH session limits that force persistent connections, the show commands that paginate unexpectedly each is a gap discovered through usage, and each gap becomes a tool enhancement.

The server is not shipped; it's grown.


Series Roadmap

This article covered the foundation: inventory, drivers, transport, and safe read tools. The remaining articles:

  1. Why Your Network Devices Need an MCP Server (published)
  2. Building Your First MCP Server for Network Devices (this article)
  3. The Safety Model staged commits, read-only modes, audit trails, and why interlocks belong in code, not prompts.
  4. Agent Skills: Teaching AI to Think Like a Network Engineer how to author SKILL.md files that auto-load across Claude, Copilot, and Codex.
  5. Knowledge Layers: CLI Harvesting, Manual Ingestion, and SNMP MIBs the hard part nobody talks about.
  6. Deploying Across Claude, Copilot, and Codex one skill set, six AI clients, and what "verified" really means.
  7. Fusion Prompts: From Datasheet to Running Ring capstone workflows that span every layer in a single prompt.

The driver abstractions, tool definitions, and safety patterns described here are in rad-mcp-server/. Lab use only never connect to production devices.

Next in the series: The Safety Model Staged Commits and Audit Trails.

Top comments (0)