Junos MCP
MCP server for Juniper Networks device operations via junos-ops
Open source Open in the app JSON README (API)
About
MCP server for Juniper Networks device operations via junos-ops
Details
- Kind
- MCP servers
- Topic
- No topic detected
- Publisher
- shigechika
- Origin
- official
- Category
- ferramentas
- Transport
- local
- Version
- 0.18.0
- Open pull requests
- 2
- Last push
- 2026-09-02T12:25:11Z
- Repository state
- ativo
- Language
- Python
- License
- Apache-2.0
- Added
- 2026-08-29 04:01:23
- Updated
- 2026-08-29 04:01:23
- Origin id
io.github.shigechika/junos-mcp
README
<!-- mcp-name: io.github.shigechika/junos-mcp -->
# junos-mcp
English | [日本語](README.ja.md)
MCP (Model Context Protocol) server for [junos-ops](https://github.com/shigechika/junos-ops).
Exposes Juniper Networks device operations to MCP-compatible AI assistants
(Claude Desktop, Claude Code, etc.) via STDIO transport.
While [junos-ops](https://github.com/shigechika/junos-ops) is the CLI tool for humans,
**junos-mcp** is the AI-facing interface to the same powerful engine.
## Features
### Device Information
| Tool | Description | Connection |
|------|-------------|:----------:|
| `get_device_facts` | Get basic device information (model, hostname, serial, version) | Yes |
| `get_version` | Get JUNOS version with upgrade status | Yes |
| `get_router_list` | List routers from config.ini (optionally filtered by tags) | No |
| `health_check` | Report server version + config status (router count, distinct tags). Lightweight; does NOT connect to any device | No |
### CLI Command Execution
| Tool | Description | Connection |
|------|-------------|:----------:|
| `run_show_command` | Run a single CLI show command (`output_format`: text/json/xml) | Yes |
| `run_show_commands` | Run multiple CLI commands in a single session (`output_format`: text/json/xml) | Yes |
| `run_show_command_batch` | Run a command on multiple devices in parallel (supports tag filter and `grep_pattern`) | Yes |
### Configuration Management
| Tool | Description | Connection |
|------|-------------|:----------:|
| `get_config` | Get device configuration (text/set/xml format) | Yes |
| `get_config_diff` | Show config diff against a rollback version | Yes |
| `push_config` | Push config with commit confirmed + health check | Yes |
### Upgrade Operations
| Tool | Description | Connection |
|------|-------------|:----------:|
| `check_upgrade_readiness` | Check if device is ready for upgrade | Yes |
| `compare_version` | Compare two JUNOS version strings | No |
| `get_package_info` | Get model-specific package file and hash | No |
| `list_remote_files` | List files on remote device path | Yes |
| `copy_package` | Copy firmware package via SCP with checksum | Yes |
| `install_package` | Install firmware with pre-flight checks (`unlink` flag for EX2300/EX3400) | Yes |
| `rollback_package` | Rollback to previous package version | Yes |
| `schedule_reboot` | Schedule device reboot at specified time | Yes |
### Diagnostics
| Tool | Description | Connection |
|------|-------------|:----------:|
| `collect_rsi` | Collect RSI/SCF with model-specific timeouts | Yes |
| `collect_rsi_batch` | Collect RSI/SCF from multiple devices in parallel (supports tag filter) | Yes |
### Pre-flight Checks
Equivalent to the `junos-ops check` subcommand modes. All three reuse the
junos-ops display layer for table rendering.
| Tool | Description | Connection |
|------|-------------|:----------:|
| `check_reachability` | Probe NETCONF reachability + available disk space per host (fast: no facts, 5s TCP probe) | Yes |
| `check_local_inventory` | Verify local firmware checksums against config.ini inventory | No |
| `check_remote_packages` | Verify staged firmware checksum + available disk space on devices (post-SCP verification) | Yes |
### Daily Operations
| Tool | Description | Connection |
|------|-------------|:----------:|
| `daily_brief` | Morning health check across multiple devices in parallel — alarms, interface up/down, syslog alert patterns within a look-back window (`since_hours`, default 18 h), dual-RE faults (`[RE_FAULT]`; skipped on SRX chassis clusters, whose facts misreport RE status — a failed cluster node surfaces via chassis alarms instead), and an optional `inet.0` route-count baseline (`route_baseline`, e.g. `tags=["main"], route_baseline=152`). Returns a CRITICAL/WARNING/OK Markdown summary. | Yes |
### Safety by Design
All destructive operations (`push_config`, `copy_package`, `install_package`,
`rollback_package`, `schedule_reboot`) default to **dry-run mode** (`dry_run=True`).
The AI assistant must explicitly set `dry_run=False` to make changes.
`push_config` provides additional safety features not found in other Junos MCP servers:
- **commit confirmed** with configurable timeout (auto-rollback if not confirmed)
- **Fallback health check** after commit (ping, NETCONF uptime probe, or any CLI command)
- **Automatic rollback** if health check fails (commit is not confirmed, timer expires)
- **`no_commit=True`** — issues `commit confirmed` but intentionally skips the final commit.
JUNOS auto-rolls back after `confirm_timeout` minutes. Useful for restarting services that
lack a `request ...restart` command (e.g. syslog daemon on EX3400 post-upgrade).
## Requirements
- Python 3.12+
- [junos-ops](https://github.com/shigechika/junos-ops) with a valid `config.ini`
- [MCP Python SDK](https://github.com/modelcontextprotocol/python-sdk) >= 1.0
## Installation
```bash
pip install junos-mcp
```
Or for development:
```bash
git clone https://github.com/shigechika/junos-mcp.git
cd junos-mcp
python3 -m venv .venv
. .venv/bin/activate
pip install -e ".[test]"
```
## CLI options
```bash
python -m junos_mcp --help
```
| Option | Description |
|--------|-------------|
| `-V`, `--version` | Print version and exit |
| `--check` | Load config.ini, list routers, and exit (exit code 1 on error) |
| `--check-host HOSTNAME` | With `--check`, also open a NETCONF session to verify reachability/auth |
| `--transport {stdio,streamable-http}` | Transport protocol (default: `stdio`) |
`--check` is handy to verify `JUNOS_OPS_CONFIG` and `config.ini` are reachable before registering the server with an AI assistant. Combine with `--check-host rt1` to also confirm that credentials actually authenticate against a real device.
## Tag-based host filtering
`run_show_command_batch`, `collect_rsi_batch`, and `get_router_list` accept an optional `tags` argument. The grammar matches the `junos-ops --tags` CLI flag (since junos-mcp 0.9.0 / junos-ops 0.16.6):
- Each list element is **one tag group**. Comma-separated tags inside a group **AND** together.
- Multiple list elements **OR** together across groups.
- When combined with `hostnames` on batch tools, the result is the **intersection** (tags filter further narrowed by names). An empty intersection returns an error.
```python
# 1 group, 1 tag — hosts tagged "main"
run_show_command_batch(command="show route summary", tags=["main"])
# 1 group, 2 tags — AND within the group: tokyo AND edge
collect_rsi_batch(tags=["tokyo,edge"])
# 2 groups — OR across groups: main OR backup
get_router_list(tags=["main", "backup"])
# Mixed: (tokyo AND core) OR backup
run_show_command_batch(command="show version", tags=["tokyo,core", "backup"])
# Intersection: among backup-tagged hosts, only rt1/rt2
run_show_command_batch(
command="show version",
hostnames=["rt1.example.jp", "rt2.example.jp"],
tags=["backup"],
)
```
See the [junos-ops tag documentation](https://github.com/shigechika/junos-ops#tag-based-host-filtering) for how to tag sections in `config.ini` and for the matching CLI grammar.
## Structured output format
`run_show_command` and `run_show_commands` accept an optional `output_format` parameter:
| Value | Description |
|-------|-------------|
| `"text"` | Default. Plain-text CLI output (same as typing the command) |
| `"json"` | NETCONF JSON output — device returns a structured dict |
| `"xml"` | NETCONF XML output — device returns pretty-printed XML |
**Note:** CLI pipe stages (`| match`, `| last`, `| count`, etc.) are silently dropped
regardless of `output_format`. PyEZ's `Device.cli()` sends the command over NETCONF
RPC, which JunOS does not pipe-process. Run the command without pipes and filter
client-side instead. For a single command, `run_show_command_batch`'s `grep_pattern`
argument (see below) offers server-side-style filtering — even against a single
host, by passing a one-element `hostnames` list — but it always fetches plain-text
output internally (it cannot be combined with `output_format="json"`/`"xml"`), and
it only accepts one command at a time, so it isn't a drop-in workaround for
`run_show_commands`' multi-command case.
```python
# Get structured BGP summary data
run_show_command("router-a", "show bgp summary", output_format="json")
```
## Server-side output filtering
`run_show_command_batch` accepts an optional `grep_pattern` argument (Python `re` pattern). When set, only lines matching the pattern are kept from each host's output. Header lines (starting with `#`) are always preserved. Hosts with no matching lines show `(no match)`.
This reduces large batch results — for example, 93 routers × `show route summary` — from hundreds of KB to a few hundred bytes by extracting just the relevant lines:
```python
# Extract only the inet.0 destination count from 93 routers
run_show_command_batch(
command="show route summary",
tags=["main"],
grep_pattern=r"inet\.0:\s+\d+ destinations",
)
```
## Connection pool
junos-mcp maintains a per-host NETCONF connection pool. Reusing an idle
`Device` avoids the TCP/NETCONF handshake on every tool call; the pool
serialises concurrent operations on the same host through a per-host lock.
| Environment variable | Default | Description |
|----------------------|---------|-------------|
| `JUNOS_MCP_POOL` | `1` (enabled) | Set to `0` to disable the pool and open a fresh connection per call |
| `JUNOS_MCP_POOL_IDLE` | `60` | Idle timeout in seconds. Connections unused longer than this are closed on the next call. Set to `0` to disable eviction |
**Security note:** pooled connections are long-lived SSH sessions. In
environments where session duration is restricted by policy, set
`JUNOS_MCP_POOL_IDLE` to a value shorter than the inactivity limit, or set
`JUNOS_MCP_POOL=0` to disable the pool entirely.
## Configuration
This server uses the same `config.ini` as junos-ops. See [junos-ops README](https://github.com/shigechika/junos-ops) for details.
Each tool accepts an optional `config_path` parameter. If omitted, the default search order is used:
1. Environment variable `JUNOS_OPS_CONFIG`
2. `./config.ini`
3. `~/.config/junos-ops/config.ini`
`config.ini` is not optional in practice: every tool — including `get_router_list` and `health_check`, which never open a device connection — reads from it at startup, and there is no degrade-gracefully path if it can't be found. Put a working `config.ini` in one of the three locations above before registering the server with any MCP client.
### Write operations
Five tools change device state. Everything else only reads. These are the same five that default to `dry_run=True` — see [Safety by Design](#safety-by-design) for the dry-run and commit-confirmed mechanics; this table is about what each one calls and the device-side privilege that gates it.
| Tool | API call | Permission gate |
|---|---|---|
| `push_config` | `jnpr.junos.utils.config.Config`: `lock` → `load(format="set")` → `diff` → `commit_check` → `commit(confirm=confirm_timeout)` → health check → final `commit` → `unlock` | The `config.ini` account for the target host needs a JUNOS login class permitting configuration mode and commit — not a read-only/operator class. The exact class name is whatever was provisioned per device in `config.ini`. |
| `copy_package` | `junos_ops.upgrade.copy()` — SCPs the firmware package to the device with checksum verification and pre-copy storage cleanup | Same account needs file-copy / storage-write access (SCP to device flash). |
| `install_package` | `junos_ops.upgrade.install()` — version check, pending-rollback check, copy + checksum, clear reboot schedule, rescue-config save, then PyEZ `SW.install()` (or `request system software add` via the `unlink` CLI path on low-flash EX2300/EX3400) | Requires software-installation privilege — JUNOS `maintenance`-class or superuser login class. |
| `rollback_package` | `junos_ops.upgrade.rollback()` — equivalent of `request system software rollback`, only after confirming a pending version exists | Same elevated software-maintenance privilege as `install_package`. |
| `schedule_reboot` | Schedules `request system reboot at <time>` | Requires reboot/maintenance privilege on the device. |
Provision the `config.ini` account for a host with a read-only/operator login class and these five tools fail against that host with a permission error; every other tool — show commands, config reads, diagnostics, `daily_brief` — keeps working. There is no separate plugin-level switch for this: the privilege boundary is entirely in the JUNOS login class assigned to the account in `config.ini`.
## Usage
### Claude Code (plugin)
This repository doubles as a single-plugin marketplace, so Claude Code can install the server for you:
```
/plugin marketplace add shigechika/junos-mcp
/plugin install junos-mcp@junos-mcp
```
The plugin launches `uvx junos-mcp` and reads the same environment variables described in [Configuration](#configuration); export `JUNOS_OPS_CONFIG` (or drop `config.ini` at `./config.ini` or `~/.config/junos-ops/config.ini`) before starting Claude Code.
`uvx` must be on the `PATH` of the process that runs Claude Code — a login
shell usually has it, but a GUI-launched app may not; install
[uv](https://docs.astral.sh/uv/) system-wide if the plugin fails to start.
### Claude Code (manual)
Register the MCP server with `claude mcp add`:
```bash
claude mcp add junos-mcp \
-e JUNOS_OPS_CONFIG=~/.config/junos-ops/config.ini \
-- python -m junos_mcp
```
The `--scope` (`-s`) option controls where the configuration is stored:
| Scope | Description | Config location |
|-------|-------------|-----------------|
| `local` (default) | Current project, current user only | `~/.claude.json` |
| `project` | Current project, shared with team | `.mcp.json` in project root |
| `user` | All projects, current user only | `~/.claude.json` |
### Claude Desktop
Add to Claude Desktop config file:
| OS | Config file |
|----|-------------|
| macOS | `~/Library/Application Support/Claude/claude_desktop_config.json` |
| Windows | `%APPDATA%\Claude\claude_desktop_config.json` |
| Linux | `~/.config/Claude/claude_desktop_config.json` |
```json
{
"mcpServers": {
"junos-mcp": {
"command": "python",
"args": ["-m", "junos_mcp"],
"env": {
"JUNOS_OPS_CONFIG": "/path/to/config.ini"
}
}
}
}
```
Restart Claude Desktop after editing.
### Remote Access with OAuth (via mcp-stdio)
junos-mcp supports Streamable HTTP transport, enabling remote access
from Claude Desktop or Claude Code through
[mcp-stdio](https://github.com/shigechika/mcp-stdio) as an OAuth proxy.
```mermaid
graph TB
A[junos-mcp<br/>remote server] <-- "OAuth 2.1 + HTTPS" --> B[mcp-stdio<br/>proxy]
B <-- "STDIO" --> C[Claude Desktop<br/>Claude Code]
```
**Step 1: Start junos-mcp with Streamable HTTP on the remote server**
```bash
JUNOS_OPS_CONFIG=~/.config/junos-ops/config.ini \
python -m junos_mcp --transport streamable-http
```
The server listens on `http://localhost:8000/mcp` by default.
**Step 2: Register mcp-stdio as the MCP server on your local machine**
```bash
claude mcp add junos-mcp -- mcp-stdio https://your-server:8000/mcp
```
mcp-stdio handles OAuth 2.1 authentication (RFC 8414 discovery, RFC 7591
dynamic client registration, PKCE) and relays STDIO ↔ Streamable HTTP.
See [mcp-stdio README](https://github.com/shigechika/mcp-stdio) for
detailed configuration including OAuth provider setup.
### MCP Inspector (development)
```bash
mcp dev junos_mcp/server.py
```
## Testing
```bash
pytest tests/ -v
```
133 tests covering all 23 tools, the connection pool, helper functions, and edge cases.
### Live smoke test
Those tests mock PyEZ, which is what makes them fast — and also what makes them
blind to a tool that has stopped returning real data.
`scripts/smoke_test.py` runs **every registered tool** against the configured
devices and fails on empty, malformed or error answers:
```bash
# uses the same inventory file as the server (JUNOS_OPS_CONFIG)
uv run python scripts/smoke_test.py
uv run python scripts/smoke_test.py --only facts --traceback
```
- **Read-only.** `push_config`, `copy_package`, `install_package`,
`rollback_package` and `schedule_reboot` are skipped by name, and a test
enforces that. `collect_rsi` / `collect_rsi_batch` are skipped too — they
change nothing, but they are minutes of RE CPU and a file per device for an
answer no assertion would read. The command-running tools are exercised with
`show system uptime`: they accept operational commands in general, and a
smoke test must not be the thing that types one that matters.
- **No payloads in the report.** Tool names and statuses only; error text is
redacted too, since these tools quote the device they were asked about and
the payloads are configuration.
- **Nothing estate-specific in the specs.** The device the per-host tools need
is discovered at run time from the configured inventory, and the hardware
model `get_package_info` needs comes from that device's own facts. Two tests
keep it that way: one refuses those parameters as literals, the other bans
anything address-shaped anywhere in the file, because this repository is
public.
- Every probe refuses the `Error: ...` / `Connection error: ...` lines these
tools return in place of raising — otherwise an unreachable device would read
as a successful call.
- CI enforces the cheap half: a tool registered without a probe spec fails the
build (`tests/test_smoke_probes.py`), so adding a tool forces the question
"how would we know it works?".
- `scripts/smoke_harness.py` is the engine and holds no JUNOS knowledge: it is
kept identical across the servers that share it, so fix engine bugs once and
sync the file rather than patching this copy.
## Architecture
### Stdout-safe by construction
Since junos-ops 0.14.1, core functions return structured `dict` values and never print to stdout; MCP tools render output via `junos_ops.display.format_*()`. No `contextlib.redirect_stdout` is needed, so the MCP STDIO JSON-RPC channel stays clean.
### Global State Initialization
junos-ops uses `common.args` and `common.config` as global variables. The MCP server initializes these using the same pattern as the test fixtures in junos-ops (`conftest.py`).
### Parallel Execution
Batch tools (`run_show_command_batch`, `collect_rsi_batch`) use `ThreadPoolExecutor` via junos-ops `common.run_parallel()` with configurable `max_workers`.
## License
Apache License 2.0