I have been running a VyOS-based router at home for years. VyOS is incredibly powerful: VLANs, WireGuard tunnels, DNS forwarding, firewall rules, DHCP, BGP if you are into that, and pretty much anything else you can think of. But it has always been squarely aimed at networking enthusiasts. The configuration interface is a CLI modeled after Juniper’s JunOS with a candidate config, commits, rollbacks. If you are used to the web UI on a typical consumer router, VyOS is a different world entirely.

I have been thinking about how AI could change that, and specifically how the Model Context Protocol (MCP) could turn a VyOS router into something that anyone can manage through natural language. Instead of memorizing that the path to set a static route is:

set protocols static route 10.0.0.0/24 next-hop 192.168.1.1

you just say, “Route the 10.0.0.0/24 network through 192.168.1.1.” Or better yet, things like:

  • “How many phones are connected to my wifi right now?”
  • “Put all my Alexa devices on a separate VLAN”
  • “Configure WireGuard so I can access my network remotely”
  • “What is using all my bandwidth?”

Suddenly VyOS goes from a niche device for geeks like me to something that could genuinely replace consumer routers for normal people. You get all the power of a proper routing platform without that scary and intimidating CLI. The AI handles the translation.

Learning MCP by Building

I learn best by building things, and I wanted to understand the nuts and bolts of MCP: the protocol itself, how streaming HTTP transport works, the JSON-RPC framing, session management. Reading specs is one thing, implementing them is another. So I wrote two MCP servers for my VyOS router, one in PHP and one in Go, each taking a fundamentally different approach. The Go version I wrote almost entirely with Claude Code, which was a good test of how well AI-assisted development works when you understand the problem but not the language idioms. I could describe what the VyOS CLI tools expect and let Claude handle the Go specifics.

The PHP Version: Stdio + REST API

The PHP version was my first pass. It uses the PHP MCP SDK with stdio transport, meaning Claude Code or any MCP client spawns it as a subprocess on my workstation. It talks to VyOS via the router’s built-in REST API over HTTPS:

MCP Client → spawns → server.php (stdio) → HTTPS → VyOS REST API

The nice thing about this approach is simplicity. Nothing runs on the router beyond what VyOS already provides. You configure VyOS to expose its API, set an API key, and the PHP server wraps each API endpoint as an MCP tool. The entry point is about as minimal as it gets:

$server = Server::builder()
    ->setServerInfo('VyOS Router', '1.0.0')
    ->addRequestHandler(new NegotiatingInitializeHandler($serverInfo))
    ->setContainer($container)
    ->setDiscovery(__DIR__, ['src'])
    ->build();

$result = $server->run(new StdioTransport());

The tools themselves use PHP attributes for schema definitions which the SDK picks up via auto-discovery:

#[McpTool(name: 'vyos_show_config', description: 'Retrieve VyOS configuration at a path')]
public function showConfig(
    #[Schema(type: 'array', items: ['type' => 'string'], description: 'Configuration path components')]
    array $path = [],
    #[Schema(type: 'string', enum: ['json', 'raw'], description: 'Output format')]
    string $format = 'json',
): mixed {
    return $this->wrap(fn() => $this->client->showConfig($path, $format));
}

One thing I had to work around was MCP protocol version negotiation. The PHP SDK’s built-in InitializeHandler ignored the client’s requested protocol version entirely, so I wrote a NegotiatingInitializeHandler that actually checks what the client asks for and responds accordingly.

The downside of the REST API approach: you need to configure HTTPS and an API key on the router, and the responses can be chatty.

The Go Version: On-Router with Streamable HTTP

The Go version takes a different approach. Instead of calling the REST API over the network, it runs directly on the router as a systemd service and executes VyOS CLI tools like /bin/cli-shell-api, /opt/vyatta/sbin/my_set, and /opt/vyatta/sbin/my_commit directly. No API layer, no HTTPS configuration, no API key.

MCP Client → SSH tunnel → vyos-mcp-go (on router) → VyOS CLI tools

The entire server compiles to a single 8MB static binary with zero runtime dependencies. Cross-compile it on your workstation, scp it to the router, done:

build:
	CGO_ENABLED=0 GOOS=linux GOARCH=amd64 $(GO) build -ldflags="-s -w" -o $(BINARY) .

deploy: build
	scp $(BINARY) mcp-server.service $(ROUTER):/config/user-data/

It lives in /config/user-data/ which persists across VyOS image upgrades, so you don’t lose it when you update your router.

This version uses the Go MCP SDK’s streamable HTTP transport rather than stdio. The server listens on 127.0.0.1 (localhost only, no exposure to the network). You reach it through an SSH tunnel:

ssh -L 8384:localhost:8384 router

Then configure your MCP client. For Claude Code it is:

{
  "mcpServers": {
    "vyos": {
      "type": "http",
      "url": "http://localhost:8384/mcp"
    }
  }
}

The main.go is pretty compact. Set up the VyOS config session, register the tools, serve streamable HTTP on /mcp:

client, err := NewVyosClient()
if err != nil {
    log.Fatal(err)
}
defer client.Close()

mcpServer := mcp.NewServer(&mcp.Implementation{
    Name:    "VyOS Router",
    Version: "2.0.0",
}, nil)
registerTools(mcpServer, client)

handler := mcp.NewStreamableHTTPHandler(
    func(*http.Request) *mcp.Server { return mcpServer },
    nil,
)

The interesting part is the VyOS client. VyOS has a concept of config sessions where you set up a session, make changes in a candidate config, then commit atomically. The Go client initializes a session at startup by calling cli-shell-api getSessionEnv and injecting the resulting environment variables into all subsequent command executions. Config-modifying operations are serialized with a mutex so concurrent MCP calls don’t stomp on each other.

The Tools

Both versions expose the same set of tools. The configuration tools follow the VyOS workflow (show, set, delete, commit, save):

Tool What it does
vyos_show_config Retrieve config at a path (JSON or raw)
vyos_set_config Set a configuration value
vyos_batch_config Set/delete multiple values atomically
vyos_delete_config Delete a config node
vyos_config_exists Check if a config path exists
vyos_return_values Get values at a path
vyos_commit Commit pending changes (with optional auto-rollback timer)
vyos_confirm Confirm a pending commit to cancel auto-rollback
vyos_save_config Save running config to startup

Then there are operational and diagnostic tools:

Tool What it does
vyos_show Run any operational show command
vyos_reset Run a reset command
vyos_generate Run a generate command
vyos_system_info System version and build info
vyos_interface_stats Interface statistics
vyos_routing_table IP routing table
vyos_dhcp_leases DHCP server leases
vyos_health_check Combined health check (CPU, memory, disk, uptime)
vyos_ping Ping a host from the router
vyos_traceroute Traceroute to a host

The vyos_commit tool has a confirmTimeout parameter that tells VyOS to auto-rollback after N minutes if you don’t call vyos_confirm. Handy safety net when making firewall changes that might lock you out.

Security

MCP itself has no authentication mechanism, which is why the Go server only binds to localhost. The SSH tunnel provides authentication (SSH keys), encryption, and access control. The systemd unit also runs with NoNewPrivileges=yes and PrivateTmp=yes.

For the PHP version, security comes from the VyOS API key and HTTPS. Since the MCP client spawns it as a local subprocess, the API key stays in environment variables and never hits the wire in plaintext.

What I Learned About MCP

Building both servers gave me a good feel for the protocol. A few observations:

Streamable HTTP is the way to go. The older SSE transport required two connections (one for server-to-client events, one for client-to-server requests). Streamable HTTP consolidates everything onto a single endpoint. The Go SDK makes this trivial: call NewStreamableHTTPHandler and you are done.

Stdio is simpler for development but awkward for always-on services. Having the MCP client spawn and manage the process lifecycle works fine, but you lose the ability to keep a persistent daemon running. For a router that should always be available, the HTTP transport makes more sense.

The protocol is essentially JSON-RPC 2.0 with conventions. Session management via the Mcp-Session-Id header, a well-defined initialization handshake, and a tool schema format based on JSON Schema. Nothing revolutionary, but a solid, practical design.

Tool design matters. The difference between an AI being able to successfully configure your router and it fumbling around is entirely in how you define the tool schemas and descriptions. Clear parameter descriptions, sensible defaults, and structured output go a long way.

The Bigger Picture

This is what excites me about MCP beyond just my own router tinkering. VyOS is an enterprise-grade routing platform. It can do things that no consumer router can: traffic shaping, policy-based routing, multi-WAN failover, full BGP support. But the barrier has always been the learning curve.

With a good MCP server in front of it, that barrier largely disappears. Someone who has never touched a CLI can say, “I want my kid’s devices to have no internet access after 9pm” and the AI can translate that into the correct firewall rules, time-based policies, and device groups. It can show you what it is about to do, explain why, and commit it with an auto-rollback timer in case something goes wrong.

Consumer routers are simple because they have to be. Their interfaces can only expose a fraction of what the underlying hardware can do. When the interface is natural language backed by a capable AI, the complexity of the underlying system stops being a limitation and starts being an advantage.

Both projects are on GitHub: vyos-mcp-go and vyos-mcp-php.