# 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