diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..9e628b9 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,14 @@ +.git/ +.github/ +.venv/ +venv/ +tests/ +dist/ +build/ +*.egg-info/ +__pycache__/ +*.py[cod] +.pytest_cache/ +.mypy_cache/ +.ruff_cache/ +.env diff --git a/.github/workflows/docker-publish.yml b/.github/workflows/docker-publish.yml new file mode 100644 index 0000000..91b4238 --- /dev/null +++ b/.github/workflows/docker-publish.yml @@ -0,0 +1,39 @@ +name: Publish Docker image + +on: + push: + tags: ["v*.*.*"] + workflow_dispatch: + +jobs: + docker: + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + steps: + - uses: actions/checkout@v4 + + - name: Log in to GHCR + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Image metadata + id: meta + uses: docker/metadata-action@v5 + with: + images: ghcr.io/${{ github.repository }} + tags: | + type=semver,pattern={{version}} + type=raw,value=latest + + - name: Build and push + uses: docker/build-push-action@v6 + with: + context: . + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} diff --git a/.github/workflows/publish-pypi.yml b/.github/workflows/publish-pypi.yml new file mode 100644 index 0000000..2813d0f --- /dev/null +++ b/.github/workflows/publish-pypi.yml @@ -0,0 +1,24 @@ +name: Publish to PyPI + +# One-time setup: create the project on PyPI and add this repo as a Trusted +# Publisher (PyPI -> project -> Publishing), then no API token is needed. + +on: + release: + types: [published] + workflow_dispatch: + +jobs: + pypi: + runs-on: ubuntu-latest + environment: pypi + permissions: + id-token: write # OIDC for trusted publishing + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: "3.12" + - run: pip install build + - run: python -m build + - uses: pypa/gh-action-pypi-publish@release/v1 diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..44e31d3 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,17 @@ +# VyMCP speaks MCP over stdio, so run the container attached: +# docker run -i --rm -e VYMANAGER_BASE_URL=... -e VYMANAGER_API_TOKEN=... vymcp +FROM python:3.12-slim + +WORKDIR /app + +# Install the package (build deps are fetched in an isolated PEP 517 env). +COPY pyproject.toml README.md LICENSE ./ +COPY src ./src +RUN pip install --no-cache-dir . + +# Run as a non-root user. +RUN useradd --create-home --uid 1000 vymcp +USER vymcp + +# Configuration comes from the environment (see .env.example); never baked in. +ENTRYPOINT ["vymcp"] diff --git a/README.md b/README.md index e74af0a..0936ff8 100644 --- a/README.md +++ b/README.md @@ -2,107 +2,33 @@ [![CI](https://github.com/Community-VyProjects/VyMCP/actions/workflows/ci.yml/badge.svg)](https://github.com/Community-VyProjects/VyMCP/actions/workflows/ci.yml) -> Model Context Protocol (MCP) server for VyOS routers, powered by [VyManager](https://github.com/Community-VyProjects/VyManager). +> MCP server for VyOS routers, powered by [VyManager](https://github.com/Community-VyProjects/VyManager). -VyMCP lets AI agents and MCP-aware tools (Claude, IDEs, automation) **read and manage VyOS routers** through a safe, audited interface. Rather than talking to routers directly, VyMCP wraps the VyManager API — so every action inherits VyManager's per-user authentication, role-based access control, audit logging, and version-aware (VyOS 1.4 / 1.5) configuration handling. +VyMCP lets AI agents read and manage VyOS routers through the VyManager API — inheriting VyManager's authentication, RBAC, audit, and commit-confirm safety. It never talks to routers directly. -> **Status: alpha.** Read tools cover every VyManager feature; guarded write tools cover ~86 features through a generic, discovery-driven proposer (plus curated firewall-group tools). The transport is stdio today (hosted HTTP/SSE is planned). Interfaces may still change. +> **Status: alpha.** stdio transport; interfaces may change. ---- +## Install -## Why a server, not a direct integration - -VyMCP deliberately **does not** connect to VyOS devices itself. It is a thin, stateless translation layer in front of VyManager's REST API. This keeps a single, enforced source of truth for everything that matters in an enterprise environment: - -- **Per-user identity** — a VyMCP request acts as a real VyManager user, never a shared service account. -- **RBAC** — a token can never exceed its owner's permissions. -- **Audit** — every configuration change is attributed to the user, token, and instance. -- **Commit-confirm safety** — writes ride VyManager's existing safety rails. -- **Version awareness** — VyOS 1.4 vs 1.5 differences are handled by VyManager, not reimplemented here. - -``` - MCP client (Claude / IDE / agent) - │ MCP over HTTP+SSE (or stdio) - ▼ - VyMCP ── stateless translation layer - │ Authorization: Bearer vym_… - │ X-VyOS-Instance-Id: - ▼ - VyManager API ── RBAC · audit · commit-confirm · 1.4/1.5 mappers - │ - ▼ - VyOS 1.4 / 1.5 +```bash +pipx install git+https://github.com/Community-VyProjects/VyMCP # provides `vymcp` ``` -## Security model - -VyMCP authenticates to VyManager with a **personal access token** that a user mints in the VyManager UI (Sites → API Tokens). Tokens are least-privilege by design: - -- **Read-only scope** — a token can be restricted so it can never make configuration changes. -- **Instance/site scoping** — a token can be limited to specific routers or sites (always within the user's own grants). -- **Per-call targeting** — the target router is selected per request via the instance the token is allowed to reach. - -This means you can hand VyMCP a token that, for example, can only *read* a single production router — and that limit is enforced by VyManager, not by VyMCP. - -## Tools - -**Read (always available):** - -| Tool | Purpose | -|---|---| -| `list_instances` | The instances this token can reach (returns `instance_id`s for the other tools). | -| `list_features` | Every VyOS feature VyMCP can read. | -| `get_capabilities(feature, instance_id)` | Version-aware capability flags for a feature. | -| `get_config(feature, instance_id)` | Normalized configuration for a feature. | - -**Write (only when `VYMANAGER_ENABLE_WRITES=true`):** changes go through a mandatory -**propose → apply** flow — a `propose_*` tool validates and returns a `plan_id` without -touching anything, then `apply_change(plan_id, confirm=true)` executes that one reviewed -plan. Plans are single-use, time-limited, and bound to their target instance. - -| Tool | Purpose | -|---|---| -| `describe_feature_operations(feature)` | The operations available for a feature (op names, arg counts, descriptions, and any required top-level/subject fields). | -| `propose_operations(feature, instance_id, operations, fields)` | Generic proposer — build a change for **~86 features** from the discovered vocabulary (no effect until applied). | -| `propose_create_address_group` / `propose_add_address_group_members` / `propose_remove_address_group_members` / `propose_delete_address_group` | Curated firewall address-group proposers (nicer ergonomics for a common case). | -| `apply_change(plan_id, confirm)` | Apply a proposed plan, protected by VyManager's commit-confirm when available. | -| `confirm_change(instance_id)` | Keep a pending commit-confirm change (settles the device, stops the rollback). | -| `discard_changes(instance_id)` | Drop unsaved changes — only when no commit-confirm is pending. | -| `get_pending_changes(instance_id)` | Show the commit-confirm rollback window and any pending change. | - -`propose_operations` covers every feature whose batch handler dispatches uniformly — the routing, firewall, NAT, service, VPN, system surface plus interface sub-types. A few features with bespoke handlers (`route`, bridge firewall, `ethernet`) are intentionally **not** exposed generically, because their custom per-op wiring can't be driven safely by introspection alone; manage those in the VyManager UI. - -### Guardrails +Or `pip install .` from a clone, or run the Docker image `ghcr.io/community-vyprojects/vymcp` (attached, with `-i`). -- **Read-only by default** — write tools are not even registered unless `VYMANAGER_ENABLE_WRITES=true`, on top of the token's own read-only scope (a read-only token is rejected by VyManager regardless). -- **Vocabulary-bound, not arbitrary** — `propose_operations` only accepts operations that exist in the feature's discovered vocabulary (validated against VyManager's own contract); there is no "set any path" tool. -- **Propose → apply** — `apply_change` requires `confirm=true` and a valid plan id, so the model can only apply a change a human has seen. -- **Never auto-confirms** — applied changes ride VyManager's commit-confirm timer. That timer is a *lockout safety net*: if a change cuts off access to the router, the device auto-reverts (reboots) and you regain access. To keep a change, call `confirm_change`; to undo one that didn't lock you out, `confirm_change` then apply the inverse change. +## Configure -## Requirements +Set environment variables (see `.env.example`): -- A reachable [VyManager](https://github.com/Community-VyProjects/VyManager) instance. -- A VyManager API token (Sites → API Tokens; read-only recommended to start). -- One or more VyOS 1.4 / 1.5 routers registered in VyManager. -- Python 3.10+. +| Variable | Required | Purpose | +|---|---|---| +| `VYMANAGER_BASE_URL` | yes | VyManager API URL | +| `VYMANAGER_API_TOKEN` | yes | A `vym_` token (Sites → API Tokens; read-only recommended) | +| `VYMANAGER_ENABLE_WRITES` | no | Enable write tools (default off) | -## Getting started +`VYMANAGER_VERIFY_SSL` (default `true`) and `VYMANAGER_TIMEOUT` (default `30`) are optional. -Install: - -```bash -pip install -e . # or: pip install -e ".[dev]" for tests -``` - -Configure via environment variables (see `.env.example`): - -```bash -export VYMANAGER_BASE_URL="https://vymanager.example.com" -export VYMANAGER_API_TOKEN="vym_…" # read-only recommended -# export VYMANAGER_ENABLE_WRITES=true # opt in to write tools -``` - -Register with an MCP client (stdio). Example client config: +## Connect an MCP client ```json { @@ -118,29 +44,20 @@ Register with an MCP client (stdio). Example client config: } ``` +## Tools + +- **Read** (always): `list_instances`, `list_features`, `get_capabilities`, `get_config`. +- **Write** (only when `VYMANAGER_ENABLE_WRITES=true`): `describe_feature_operations` + `propose_operations` cover ~86 features; `apply_change` / `confirm_change` / `discard_changes` / `get_pending_changes` drive the rollout. + +Writes are safe by design: off by default, capped to the token's scope, and gated by a **propose → apply** flow. `apply_change` requires explicit confirmation and rides VyManager's commit-confirm auto-rollback (a lockout safety net) — it never auto-confirms. + ## Development ```bash pip install -e ".[dev]" -pytest # deterministic; no live device needed -ruff check . -mypy +pytest && ruff check . && mypy ``` -## Roadmap - -- [x] stdio server + token auth to VyManager -- [x] Read tools across all features -- [x] Guarded write tools (propose → apply → commit-confirm) -- [x] Generic, discovery-driven writes across ~86 features -- [ ] Cover the remaining bespoke-handler features (`route`, bridge firewall, `ethernet`) -- [ ] Hosted remote transport (HTTP/SSE) with per-user auth -- [ ] Packaging and deployment docs - -## Related projects - -- **[VyManager](https://github.com/Community-VyProjects/VyManager)** — the web UI and API that VyMCP builds on. - ## License -See `LICENSE` for details. +GPLv3 — see [`LICENSE`](LICENSE).