# junos-mcp-server A Model Context Protocol (MCP) server for Juniper Junos devices that enables LLM interactions with network equipment. ## Table of Contents - [junos-mcp-server](#junos-mcp-server) - [Table of Contents](#table-of-contents) - [Important Security Notice](#important-security-notice) - [Security Requirements](#security-requirements) - [Security Best Practices](#security-best-practices) - [Important Configuration Notice](#important-configuration-notice) - [Getting Started](#getting-started) - [Running with uv](#running-with-uv) - [Start Junos MCP Server](#start-junos-mcp-server) - [Configuration](#configuration) - [Config for Claude Desktop (stdio transport)](#config-for-claude-desktop-stdio-transport) - [Config for Claude Desktop (using uv)](#config-for-claude-desktop-using-uv) - [Config for Claude Desktop (Docker container)](#config-for-claude-desktop-docker-container) - [Docker Usage](#docker-usage) - [Build Docker Container](#build-docker-container) - [Running with Default Settings](#running-with-default-settings) - [Overriding Default Arguments](#overriding-default-arguments) - [Junos Device Configuration](#junos-device-configuration) - [VSCode + GitHub Copilot Integration](#vscode--github-copilot-integration) - [Start Your Server](#start-your-server) - [Point to This URL in Your VSCode Config](#point-to-this-url-in-your-vscode-config) - [Authentication for MCP Server Access](#authentication-for-mcp-server-access) - [Authentication Behavior](#authentication-behavior) - [Token Management](#token-management) - [Generate a New Token](#generate-a-new-token) - [List All Tokens](#list-all-tokens) - [Show Token Value (Recovery)](#show-token-value-recovery) - [Revoke a Token](#revoke-a-token) - [Server Authentication Status](#server-authentication-status) - [Client Configuration with Authentication](#client-configuration-with-authentication) - [VSCode Configuration with Token](#vscode-configuration-with-token) - [Testing with curl](#testing-with-curl) - [Docker with Authentication](#docker-with-authentication) - [Security Best Practices](#security-best-practices-1) - [Token File Format](#token-file-format) - [GuardRails for Config Commit](#guardrails-for-config-commit) - [GuardRails for Executing Operational Commands](#guardrails-for-executing-operational-commands) - [Using MCP Server with Juniper Cloud-Native Router (JCNR)](#using-mcp-server-with-juniper-cloud-native-router-jcnr) - [Developer Guide](#developer-guide) - [Architecture Overview](#architecture-overview) - [How Tools Work](#how-tools-work) - [Adding a New Tool](#adding-a-new-tool) - [Step 1: Create a Handler Function](#step-1-create-a-handler-function) - [Step 2: Register the Handler](#step-2-register-the-handler) - [Step 3: Define Tool Metadata](#step-3-define-tool-metadata) - [Example: Creating a BGP Neighbors Tool](#example-creating-a-bgp-neighbors-tool) - [Best Practices for Tool Development](#best-practices-for-tool-development) - [Using PyEZ for Advanced Operations](#using-pyez-for-advanced-operations) - [Testing Your Tools](#testing-your-tools) - [Debugging Tips](#debugging-tips) ## Important Security Notice > **Warning:** This server enables LLM access to your network infrastructure. Please review these security considerations carefully. ### Security Requirements - **Corporate Policy Compliance**: Only use this server if your company's policy allows sending data of Junos devices to LLM services. - **Server Security**: Always secure your Junos MCP server before deployment in production environments. - **Authentication**: Do **not** use password authentication for production deployments. We strongly recommend using SSH key-based authentication for enhanced security. - **Deployment Strategy**: Until your MCP server is properly secured, only deploy locally for testing purposes. Do not deploy remote servers in production without proper security measures. ### Security Best Practices - Use SSH key authentication instead of passwords - Implement proper network access controls - Monitor and log all MCP server activities - Regular security audits and updates - Follow your organization's security policies ## Important Configuration Notice > **Warning:** The Junos MCP server supports configuration changes, but please ensure you only use this functionality when you want LLM-generated configurations to be loaded and committed on your Junos router. **Always review the configuration being generated by the LLM and only allow tool execution if it's the correct configuration for your use case.** ## Getting Started Get the code. ```bash git clone https://github.com/Juniper/junos-mcp-server.git cd junos-mcp-server pip install -r requirements.txt ``` ### Running with uv If you're using [uv](https://github.com/astral-sh/uv), you can run the server directly: ```bash uv run python jmcp.py -f devices.json -t stdio ``` ## Start Junos MCP Server ```bash python3.11 jmcp.py --help Junos MCP Server options: -h, --help show this help message and exit -f DEVICE_MAPPING, --device-mapping DEVICE_MAPPING the name of the JSON file containing the device mapping -H HOST, --host HOST Junos MCP Server host -t TRANSPORT, --transport TRANSPORT Junos MCP Server transport -p PORT, --port PORT Junos MCP Server port ``` Junos MCP server supports both streamable-http and stdio transport. Do not use --host with stdio transport. ### Environment Variables - `JUNOS_TIMEOUT`: Command timeout in seconds for Junos CLI operations. - `JMCP_POOL_IDLE_TIMEOUT`: Idle timeout in seconds for pooled SSH/NETCONF connections. - SSH sessions are reused across tool calls via a connection pool; a background cleanup thread (running once a minute) closes connections idle longer than this timeout. - Default: `300`. Invalid values are logged and fall back to the default. ## Configuration ### Config for Claude Desktop (stdio transport) ```json { "mcpServers": { "jmcp": { "type": "stdio", "command": "python3", "args": ["jmcp.py", "-f", "devices.json", "-t", "stdio"] } } } ``` ### Config for Claude Desktop (using uv) ```json { "mcpServers": { "jmcp": { "type": "stdio", "command": "uv", "args": ["run", "python", "jmcp.py", "-f", "devices.json", "-t", "stdio"] } } } ``` **Note:** Please provide absolute path for jmcp.py and devices.json file. ### Config for Claude Desktop (Docker container) ```json { "mcpServers": { "jmcp": { "type": "stdio", "command": "/usr/local/bin/docker", "args": [ "run", "--rm", "-i", "-v", "devices.json:/app/config/devices.json", "-v", "vsrx_keypair.pem:/app/config/vsrx_keypair.pem", "junos-mcp-server:latest" ] } } } ``` ## Docker Usage ### Build Docker Container ```bash docker build -t junos-mcp-server:latest . ``` ### Running with Default Settings By default, the Docker container runs with stdio transport: ```bash docker run --rm -it -v /path/to/your/devices.json:/app/config/devices.json junos-mcp-server:latest ``` This uses the default command: `python jmcp.py -f /app/config/devices.json -t stdio` ### Overriding Default Arguments You can override any arguments by specifying the full command: **For stdio transport:** ```bash docker run --rm -it -v /path/to/your/devices.json:/app/config/devices.json junos-mcp-server:latest python jmcp.py -f /app/config/devices.json -t stdio ``` **For streamable-http transport:** > **Security:** the streamable-http transport refuses to start without a valid > `.tokens` file. Generate one with `python jmcp_token_manager.py generate > --id ` and mount it into the container as shown below. See > [Authentication](#authentication) for details, or pass > `--allow-unauthenticated-http` for loopback-only local development. ```bash docker run --rm -it \ -v /path/to/your/devices.json:/app/config/devices.json \ -v /path/to/.tokens:/app/.tokens \ -p 30030:30030 \ junos-mcp-server:latest \ python jmcp.py -f /app/config/devices.json -t streamable-http -H 0.0.0.0 ``` **For streamable-http with custom port:** ```bash docker run --rm -it \ -v /path/to/your/devices.json:/app/config/devices.json \ -v /path/to/.tokens:/app/.tokens \ -p 8080:8080 \ junos-mcp-server:latest \ python jmcp.py -f /app/config/devices.json -t streamable-http -p 8080 -H 0.0.0.0 ``` **Note:** - Always mount your device configuration file using `-v /path/to/you/ devices.json:/app/config/devices.json` - For streamable-http transport, expose the port using `-p host_port:container_port` - Mount any SSH private key files if using key-based authentication (e.g., `-v /path/to/key.pem:/app/config/key.pem`) Build docker container for Junos MCP Server ```bash docker build -t junos-mcp-server:latest . ``` **Note:** Mount your config file `devices.json` and mount any other files, in my case I am using pem file for ssh priv key authentication so I am also mounting vsrx_keypair.pem ## Junos Device Configuration Junos MCP server supports both `password` based auth as well as `SSH key` based authentication (See first 2 routers configs [router-1, router-2]). ```json { "router-1": { "ip": "ip-addr", "port": 22, "username": "user", "auth": { "type": "password", "password": "pwd" } }, "router-2": { "ip": "ip-addr", "port": 22, "username": "user", "auth": { "type": "ssh_key", "private_key_path": "/path/to/private/key.pem" } }, "router-3": { "ip": "ip-addr", "port": 22, "username": "user", "ssh_config": "~/.ssh/config_dc", "auth": { "type": "ssh_key", "private_key_path": "/path/to/private/key.pem" } }, "router-4": { "ip": "ip-addr", "port": 22, "username": "user", "ssh_config": "/home/user/.ssh/config_jumphost", "auth": { "type": "password", "password": "pwd" } } } ``` Junos MCP server also provides support for `ProxyCommand`. (See last 2 routers configs [router-3, router-4]), which enables you to access a target device through an intermediary host that supports `netcat`. This is useful when you can only log in to the target device through the intermediate host (jumphost). This is an example of an SSH config file being used `.ssh/config_jumphost`: ```bash # Jumphost VM Connection Host jumphost-vm HostName 10.2.11.200 User root # Used for MCP server IdentityFile /home/user/.ssh/id_rsa_claude IdentitiesOnly yes StrictHostKeyChecking no # cRPD Devices (via jump host) Host dt-crpd1 dtwin-crpd1 digital-twin-crpd1 clab-digital-twin-eop6-pe1 HostName 172.20.20.11 User claude IdentityFile c # ProxyJump jumphost-vm # Not working with JunOS MCP ProxyCommand ssh -l root jumphost-vm nc %h 22 2>/dev/null StrictHostKeyChecking no ``` **Note #1:** `Port` value should be an integer (typically `22` for SSH). **Note #2:** `IdentityFile` recommendation use full path (e.g `/home/user/.ssh /id_rsa_claude` rather than `~/.ssh/id_rsa_claude`). ## VSCode + GitHub Copilot Integration ### Start Your Server ```bash python3.11 jmcp.py -f devices.json [06/11/25 08:26:11] INFO Starting MCP server 'jmcp-server' with transport 'streamable-http' on http://127.0.0.1:30030/mcp INFO: Started server process [33512] INFO: Waiting for application startup. INFO: Application startup complete. INFO: Uvicorn running on http://127.0.0.1:30030 (Press CTRL+C to quit) ``` ### Point to This URL in Your VSCode Config ```json { "mcp": { "servers": { "my-junos-mcp-server": { "url": "http://127.0.0.1:30030/mcp/" } } } } ``` **Note:** You can use VSCode's `Cmd+Shift+P` to configure MCP server. ## Authentication for MCP Server Access The Junos MCP server supports token-based authentication for secure client access when using streamable-http transport. This prevents unauthorized access to your network infrastructure. ### Authentication Behavior - **stdio transport** (Claude Desktop): No authentication required - secure by design as it runs locally - **streamable-http transport** (VSCode, web clients): Token-based authentication available ### Token Management The server includes a dedicated token management CLI tool: `jmcp_token_manager.py` By default, both the server and token manager use the `.tokens` file alongside `jmcp.py`, independent of the current working directory. On POSIX systems, the token manager creates the file with owner-only permissions (`0600`) and repairs those permissions whenever it writes an existing file. #### Generate a New Token ```bash # Basic token generation python jmcp_token_manager.py generate --id "vscode-dev" # With description python jmcp_token_manager.py generate --id "vscode-dev" --description "VSCode development environment" # Use a custom token file location python jmcp_token_manager.py --tokens-file /secure/path/tokens.json generate --id "vscode-dev" # Example output: Generated new token: ID: vscode-dev Token: jmcp_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6q7r8 Description: VSCode development environment Save this token securely - it won't be shown again! ``` #### List All Tokens ```bash python jmcp_token_manager.py list # Example output: ID Description Created ------------------------------------------------------------------------------------- vscode-dev VSCode development environment 2025-01-28T10:30:00Z prod-client Production client access 2025-01-28T09:15:00Z ``` #### Show Token Value (Recovery) ```bash python jmcp_token_manager.py show --id "vscode-dev" # Example output: Token ID: vscode-dev Token: jmcp_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6q7r8 Description: VSCode development environment Created: 2025-01-28T10:30:00Z ``` #### Revoke a Token ```bash python jmcp_token_manager.py revoke --id "vscode-dev" # Example output: Token 'vscode-dev' has been revoked ``` ### Server Authentication Status For the `streamable-http` transport the server **fails closed**: it refuses to start unless a valid, non-empty `.tokens` file is present. The only way to start without tokens is the explicit `--allow-unauthenticated-http` flag, which is in turn restricted to loopback binds (`127.0.0.1`, `::1`, `localhost`). To use a custom location, pass the same path to the server: ```bash python jmcp.py -f devices.json -t streamable-http --tokens-file /secure/path/tokens.json ``` **With tokens configured:** ```bash python jmcp.py -f devices.json -t streamable-http INFO - Token-based authentication enabled INFO - Clients must send 'Authorization: Bearer ' header INFO - Use jmcp_token_manager.py to manage tokens INFO - Streamable HTTP server started on http://127.0.0.1:30030 ``` **Without tokens configured (default - refuses to start):** ```bash python jmcp.py -f devices.json -t streamable-http ERROR - Refusing to start streamable-http transport without authentication: .tokens file not found ERROR - Generate a token with: python jmcp_token_manager.py generate --id ERROR - Or, for local development on loopback only, re-run with --allow-unauthenticated-http ``` **Explicit unauthenticated mode (loopback only, development only):** ```bash python jmcp.py -f devices.json -t streamable-http --allow-unauthenticated-http WARNING - *** Streamable HTTP authentication is DISABLED (--allow-unauthenticated-http). .tokens file not found. Server is open to any client that can reach 127.0.0.1:30030 and can commit configuration to mapped devices. Use only for local development. *** INFO - Streamable HTTP server started on http://127.0.0.1:30030 ``` Combining `--allow-unauthenticated-http` with a non-loopback bind (for example `-H 0.0.0.0`) is rejected at startup. ### Client Configuration with Authentication #### VSCode Configuration with Token ```json { "mcp": { "servers": { "my-junos-mcp-server": { "url": "http://127.0.0.1:30030/mcp/", "headers": { "Authorization": "Bearer jmcp_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6q7r8" } } } } } ``` #### Testing with curl ```bash # Test authentication with valid token curl -X POST "http://127.0.0.1:30030/mcp/" \ -H "Authorization: Bearer jmcp_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6q7r8" \ -H "Content-Type: application/json" \ -H "Accept: application/json, text/event-stream" \ -d '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}' # Test without token (should fail with 401) curl -X POST "http://127.0.0.1:30030/mcp/" \ -H "Content-Type: application/json" \ -H "Accept: application/json, text/event-stream" \ -d '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}' ``` **Note:** MCP streamable-http requires the `Accept: application/json, tex /event-stream` header. #### Docker with Authentication When using Docker, mount the `.tokens` file to enable authentication: ```bash # Generate token first (outside container) python jmcp_token_manager.py generate --id "docker-client" # Run container with token file mounted docker run --rm -it \ -v /path/to/devices.json:/app/config/devices.json \ -v /path/to/.tokens:/app/.tokens \ -p 30030:30030 \ junos-mcp-server:latest \ python jmcp.py -f /app/config/devices.json -t streamable-http -H 0.0.0.0 ``` ### Security Best Practices 1. **Token Security**: - Store tokens securely (password managers, environment variables) - Use descriptive token IDs for easy management - Regularly rotate tokens by revoking old ones and generating new ones - Never commit tokens to version control 2. **Access Control**: - Generate separate tokens for different clients/environments - Revoke tokens immediately when no longer needed - Monitor server logs for unauthorized access attempts 3. **Network Security**: - Run streamable-http server behind reverse proxy with HTTPS in production - Use firewall rules to restrict access to MCP server port - Consider VPN access for remote clients ### Token File Format The `.tokens` file stores tokens in JSON format: ```json { "vscode-dev": { "token": "jmcp_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6q7r8", "description": "VSCode development environment", "created": "2025-01-28T10:30:00Z" }, "prod-client": { "token": "jmcp_x9y8z7w6v5u4t3s2r1q0p9o8n7m6l5k4j3i2", "description": "Production client access", "created": "2025-01-28T09:15:00Z" } } ``` **Important**: Keep this file secure and don't commit it to version control. ## GuardRails for Config Commit The `load_and_commit_config` tool now includes a pre-commit guardrail check that validates the submitted candidate configuration against patterns in `block.cfg` before any device commit actions are attempted. ### How it works - Each non-comment line in `block.cfg` is treated as a blocked pattern. - Pattern matching is done against normalized config lines from the submitted `config_text`. - Patterns support regex tokens (for example to match dynamic usernames). - Literal keyword tokens also match non-empty Junos CLI abbreviations, so `set sys root-auth` matches `set system root-authentication`. - If any line matches, the request is rejected and **no configuration is loaded or committed**. ### Example `block.cfg` ```text # Blocked configuration prefixes/patterns for load_and_commit_config set system root-authentication set system login user ([^ ]+) authentication ``` This protects common high-risk configuration areas (for example, root authentication or unmanaged local user credential changes) from being committed by automation. ## GuardRails for Executing Operational Commands The `execute_junos_command` and `execute_junos_command_batch` tools now include command guardrails using `block.cmd`. ### How it works - Each non-comment line in `block.cmd` is treated as a regex command pattern. - Submitted commands are normalized and checked before execution. - Literal keyword tokens match both their full form and non-empty Junos CLI abbreviations; regex patterns remain supported. - If a command matches a blocked pattern, execution is rejected. - For batch execution, the blocked command is rejected before dispatching to routers. ### Example `block.cmd` ```text # Blocked operational command prefixes/patterns for execute_junos_command request system reboot request system halt request system power-cycle request system power-off request system zeroize ``` This blocks disruptive commands (reboot/power/zeroize class actions) while still allowing read-only operational show commands. ## Using MCP Server with Juniper Cloud-Native Router (JCNR) JCNR is a cloud native router that runs on various cloud environments. One can use this MCP server with JCNR as well by following the steps given below. Please refer to JCNR documentation for more details on configuration. - Configure SSH access in JCNR on a desired port other than 22. This is required because, JCNR runs as a container on shared operating system. Running SSH on default port is not recommended. By default SSH is enabled on port 24. But, it is preferred to change this to desired port depending on your networking needs. - Enable authentication method for SSH. JCNR supports SSH key and password based authentications. - Enable Netconf over SSH. This is enabled by default. ```junos set system services netconf ssh set system services ssh port 3030 set system services ssh root-login allow set system root-authentication encrypted-password "$6$3vvMI$RNemhmu9izWXzO46msh38frIg4VoeFNJWJZugxgnU.NQso3OQ00QWOIZmzNePD.MWjDOD BBEYut/W7kfADdV." (or) set system root-authentication load-key-file ``` ## Developer Guide This section explains the architecture of the Junos MCP server and how to extend it with new tools. ### Architecture Overview The Junos MCP server uses the Model Context Protocol (MCP) to enable LLMs to interact with Juniper network devices. The server architecture consists of: 1. **MCP Server Core** (`jmcp.py`): Handles MCP protocol communication 2. **Tool Handlers**: Individual functions that implement specific network operations 3. **Tool Registry**: Maps tool names to their handler functions 4. **Transport Layer**: Supports stdio (Claude Desktop) and streamable-http (VSCode) ### How Tools Work Each tool in the MCP server follows this flow: ![MCP Tool Flow](docs/diagrams/mcp-tool-flow.svg "MCP Tool Flow") ### Adding a New Tool Adding a new tool is a simple 3-step process: #### Step 1: Create a Handler Function Create an async handler function in `jmcp.py` (before the `TOOL_HANDLERS` dictionary): ```python async def handle_my_new_tool(arguments: dict) -> list[types.ContentBlock]: """Handler for my_new_tool - describe what it does""" # Extract arguments router_name = arguments.get("router_name", "") my_param = arguments.get("my_param", "default_value") # Validate router exists if router_name not in devices: result = f"Router {router_name} not found in the device mapping." else: # Your tool logic here log.debug(f"Executing my_new_tool on router {router_name}") result = _run_junos_cli_command(router_name, f"show {my_param}") return [types.TextContent(type="text", text=result)] ``` #### Step 2: Register the Handler Add your handler to the `TOOL_HANDLERS` dictionary (around line 330): ```python TOOL_HANDLERS = { "execute_junos_command": handle_execute_junos_command, "get_junos_config": handle_get_junos_config, "junos_config_diff": handle_junos_config_diff, "gather_device_facts": handle_gather_device_facts, "get_router_list": handle_get_router_list, "load_and_commit_config": handle_load_and_commit_config, "my_new_tool": handle_my_new_tool, # Add your tool here } ``` #### Step 3: Define Tool Metadata Add the tool definition to the `list_tools()` method (around line 410): ```python types.Tool( name="my_new_tool", description="Brief description of what your tool does", inputSchema={ "type": "object", "properties": { "router_name": {"type": "string", "description": "The name of the router"}, "my_param": {"type": "string", "description": "Description of parameter"} }, "required": ["router_name"] # List required parameters } ) ``` ### Example: Creating a BGP Neighbors Tool Here's a complete example of adding a tool to show BGP neighbors: ```python # Step 1: Handler function async def handle_show_bgp_neighbors(arguments: dict) -> list[types.ContentBloc ]: """Handler for show_bgp_neighbors tool""" router_name = arguments.get("router_name", "") neighbor_address = arguments.get("neighbor_address", "") if router_name not in devices: result = f"Router {router_name} not found in the device mapping." else: log.debug(f"Getting BGP neighbors from router {router_name}") if neighbor_address: cmd = f"show bgp neighbor {neighbor_address}" else: cmd = "show bgp summary" result = _run_junos_cli_command(router_name, cmd) return [types.TextContent(type="text", text=result)] # Step 2: Add to TOOL_HANDLERS TOOL_HANDLERS = { # ... existing tools ... "show_bgp_neighbors": handle_show_bgp_neighbors, } # Step 3: Add to list_tools() types.Tool( name="show_bgp_neighbors", description="Show BGP neighbor information", inputSchema={ "type": "object", "properties": { "router_name": {"type": "string", "description": "The name of the router"}, "neighbor_address": {"type": "string", "description": "Optional: specific neighbor IP"} }, "required": ["router_name"] } ) ``` ### Best Practices for Tool Development 1. **Error Handling**: Always handle connection errors and invalid inputs gracefully 2. **Logging**: Use the global `log` logger for debugging 3. **Validation**: Check if router exists before attempting operations 4. **Documentation**: Write clear descriptions for tools and parameters 5. **Timeouts**: Support configurable timeouts for long-running operations 6. **Return Format**: Always return `list[types.ContentBlock]` with text content 7. **Context Parameter**: Use the `context: Context` parameter to send progress and log messages to the client ### Using PyEZ for Advanced Operations For operations beyond CLI commands, use PyEZ directly: ```python from jnpr.junos import Device from jnpr.junos.utils.config import Config # Example: Using PyEZ tables async def handle_get_interfaces(arguments: dict) -> list[types.ContentBlock]: router_name = arguments.get("router_name", "") if router_name not in devices: result = f"Router {router_name} not found in the device mapping." else: device_info = devices[router_name] try: connect_params = prepare_connection_params(device_info, router_name) with Device(**connect_params) as junos_device: # Use PyEZ tables or other utilities interfaces = junos_device.rpc.get_interface_information() # Process interfaces... result = "Interface information..." except Exception as e: result = f"Error: {e}" return [types.TextContent(type="text", text=result)] ``` ### Testing Your Tools 1. **Unit Testing**: Test handler functions with mock arguments 2. **Integration Testing**: Test with actual Junos devices or vSRX 3. **Error Cases**: Test with invalid routers, network failures, etc. Example test: ```python # Test the handler directly result = await handle_my_new_tool({ "router_name": "router-1", "my_param": "test" }) print(result[0].text) ``` ### Debugging Tips 1. Enable debug logging to see detailed execution: ```python logging.basicConfig(level=logging.DEBUG) ``` 2. Use the stdio transport for easier debugging: ```bash python jmcp.py -f devices.json -t stdio ``` 3. Test individual commands manually: ```python result = _run_junos_cli_command("router-1", "show version") print(result) ```