Page MenuHomeVyOS Platform

T9030-mcp-architecture.md

Authored By
mihakralj
Jul 1 2026, 2:50 AM
Size
9 KB
Referenced Files
None
Subscribers
None

T9030-mcp-architecture.md

# T9030: MCP Architecture & Transport Layer
| Field | Value |
|-------|-------|
| Task | [T9030](https://vyos.dev/T9030) |
| Status | Draft / design |
| Component | `service https api mcp` |
| Target branch | `current` (1.5+) |
| Related specs | [Dynamic Schema Harvesting](T9030-mcp-dynamic-schema.md) · [MCP Tools](T9030-mcp-tools.md) · [Security & Concurrency](T9030-mcp-security.md) |
## Overview
Integrate a native HTTPS-based Model Context Protocol (MCP) server into VyOS,
running alongside `set service https api rest` and `set service https api graphql`.
The server lets MCP-capable LLM clients (Claude Desktop, IDE agents, custom
orchestrators) drive VyOS operational and configuration workflows through a
standard, typed protocol instead of bespoke REST glue.
The server must harvest all tools and data dynamically from the VyOS internal
schema, ensuring zero maintenance as the OS evolves.
**Key constraint**: all tools, resources, and schemas are derived on-the-fly
from `interface-definitions/` and `op-mode-definitions/` XML templates via the
`vyos.xml_ref` reference cache (`vyos.xml_ref.cache.reference` for config nodes,
`vyos.xml_ref.op_cache.op_reference` for op-mode nodes). No static
`@mcp.tool()` definitions are committed to the tree.
### Goals
- Expose the full VyOS CLI surface (op-mode + conf-mode) to MCP clients with
**no per-feature code**.
- Reuse the existing HTTPS API daemon, authentication, and TLS termination —
no new listening socket, no new systemd unit.
- Guarantee that an LLM can only issue paths that exist in the live schema,
eliminating arbitrary shell execution and command hallucination.
- Stay invisible/disabled by default; opt-in via a single `set` command.
### Non-goals
- stdio transport (local subprocess MCP) — not applicable to a network appliance.
- A separate OAuth 2.1 authorization server — auth is inherited from the
existing API layer (see [Security spec](T9030-mcp-security.md)).
- Replacing REST/GraphQL — MCP is an additional, parallel surface.
### Terminology
| Term | Meaning |
|------|---------|
| Tool | An MCP-invokable action (`execute_operational_command`, `modify_configuration`). |
| Resource | Read-only, addressable data exposed via a URI template (config/state/schema). |
| Prompt | A reusable templated instruction set surfaced to the LLM. |
| Meta-tool | A single generic tool that accepts a path array, instead of thousands of generated tools. |
| Reference cache | Compiled XML schema (`reftree.cache`) consulted for path validation. |
## Transport: Streamable HTTP
- Protocol: JSON-RPC 2.0 over Streamable HTTP.
- stdio transport is **not** supported — incompatible with remote network devices.
- Operates natively over HTTPS port 443, traverses reverse proxies and firewalls.
- Uses `mcp.server.streamable_http_manager.StreamableHTTPSessionManager` from the
MCP Python SDK, mounted as an ASGI sub-application. Sessions are stateful:
the initial GET establishes an SSE stream and returns a `Mcp-Session-Id`
header; subsequent POST requests carry that session ID. DELETE terminates the
session.
### Endpoints
| Method | Path | Purpose |
|--------|--------|------------------------------------------------------|
| GET | `/mcp` | Establish SSE stream, negotiate session |
| POST | `/mcp` | Client-to-server JSON-RPC messages (with session ID) |
| DELETE | `/mcp` | Terminate session |
The single `/mcp` endpoint handles all MCP traffic. The initial GET returns
the `Mcp-Session-Id` header for session correlation. The POST channel carries
JSON-RPC requests scoped to that session. The DELETE allows clean client-initiated
teardown.
### Client Configuration Example
Orchestrators (e.g. Claude Desktop, Cline) connecting to this MCP server should
configure the connection matching the Streamable HTTP standard and providing the
authentication headers.
```json
{
"mcpServers": {
"vyos-router": {
"url": "https://<router-ip>/mcp",
"headers": {
"apikey": "<vyos-api-key>"
}
}
}
}
```
*Note: Due to typical orchestrator limitations regarding custom headers, it may
be required to pass the API key via query parameters or environment variables
depending on client implementation, or require a lightweight local proxy for the
standard `apikey` header.*
### Connection sequence
```mermaid
sequenceDiagram
participant C as MCP Client (LLM)
participant P as FastAPI / ApiServer
participant A as Auth dependency
participant M as api.mcp.routers (Streamable HTTP app)
C->>P: GET /mcp (apikey / Bearer)
P->>A: validate credentials
A-->>P: 401 if invalid (no handshake)
P->>M: open SSE stream, create session
M-->>C: SSE stream + Mcp-Session-Id header
C->>P: POST /mcp {initialize} (Mcp-Session-Id)
P->>M: route by session id
M-->>C: SSE: initialize result (capabilities)
C->>P: POST /mcp {tools/list} (Mcp-Session-Id)
M-->>C: SSE: tool catalog (mode-filtered)
C->>P: POST /mcp {tools/call ...} (Mcp-Session-Id)
M-->>C: SSE: result / JSON-RPC error
C->>P: DELETE /mcp (Mcp-Session-Id)
M-->>C: session terminated
```
### Transport-level error handling
- Credential failure on the initial `GET /mcp` returns HTTP 401 and the SSE
stream is never opened — the client cannot enumerate tools or resources.
- A POST referencing an unknown/expired session id returns HTTP 404; the client
must re-establish the session via a new GET.
- Protocol/parse failures are returned as JSON-RPC `error` objects over the SSE
stream (codes per the JSON-RPC 2.0 / MCP spec), never as raw HTML.
## Integration into VyOS HTTP API
- Embed MCP as an `api.mcp.routers` module, parallel to `api.rest.routers` and
`api.graphql.routers` under `src/services/api/`.
- Mount the MCP ASGI app onto the existing FastAPI instance via
`app.mount("/mcp", _mcp_asgi_app)` (analogous to how GraphQL adds its route).
- Reuses the existing `ApiServer(UvicornServer)` defined in
`src/services/vyos-http-api-server` — no new daemons, no new socket
(the server already binds the unix domain socket `/run/api.sock`).
- The module is loaded/unloaded dynamically based on `api_config_state`
(`/run/http-api-state`), exactly like REST and GraphQL: `mcp_init(app)` /
`mcp_clear(app)` mirror `rest_init`/`rest_clear` and `graphql_init`/`graphql_clear`.
- Inherits FastAPI dependency injection for auth (API keys, JWT) via a wrapper
ASGI app that validates credentials before delegating to the session manager.
### Module layout
```
src/services/api/mcp/
├── __init__.py
├── routers.py # mcp_init / mcp_clear, Streamable HTTP mount, auth wrapper
├── server.py # MCP Server instance, capability registration
├── tools.py # meta-tool dispatch (see T9030-mcp-tools.md)
├── resources.py # URI-template resources (see T9030-mcp-dynamic-schema.md)
├── schema.py # XML reference -> JSON Schema projection
├── prompts.py # reusable prompt templates
└── auth.py # credential extraction at the handshake (see Security spec)
```
### Router registration pattern
`vyos-http-api-server` already branches on the parsed config state to wire each
sub-API. MCP follows the same idempotent init/clear contract:
```python
# in vyos-http-api-server, alongside rest/graphql wiring
if session.mcp:
from api.mcp.routers import mcp_init
mcp_init(app)
else:
from api.mcp.routers import mcp_clear
mcp_clear(app)
```
`mcp_init(app)` is idempotent (no-op if already mounted, mirroring how
`rest_init` guards with `all(r in app.routes for r in router.routes)`) and
`mcp_clear(app)` removes the mounted sub-app from `app.routes`. In-flight
connections terminate when the worker reloads (the server is torn down and
`initialization()` re-runs on `SIGHUP`); `mcp_clear` cancels the session manager
lifespan, terminating all active sessions.
## Dependencies
- `python3-mcp` (MCP Python SDK) — added to `debian/control` build/runtime deps
for the `vyos-1x` package. The SDK supplies `mcp.server.Server`,
`mcp.server.streamable_http.StreamableHTTPServerTransport`,
`mcp.server.streamable_http_manager.StreamableHTTPSessionManager`, and the
JSON-RPC types.
- No new compiled component; the feature is pure Python on top of the existing
FastAPI/Uvicorn stack already required by the HTTP API.
- Reference cache (`reftree.cache` / `op_cache`) is produced during image build
by the existing `vyos.xml_ref` tooling — no additional build step.
## Lifecycle Management
- `ApiServer(UvicornServer)` explicitly overrides `install_signal_handlers()`
(it is a no-op) to prevent interference with the VyOS systemd manager; the MCP
sub-app inherits this and adds no signal handling of its own.
- The MCP SDK Streamable HTTP transport is fully ASGI compliant — no background
threads or signal hijacking required. The session manager runs its task group
within the app lifespan, managed by `app.router.lifespan_context`.
- On `service_https.py` commit, the API state file is rewritten and the running
server reconciles routers (mount/unmount MCP) without restarting the process;
REST and GraphQL connections are unaffected (see
[Reconfiguration Behavior](T9030-mcp-security.md#reconfiguration-behavior)).
## Open questions
- Whether to expose a dedicated MCP listen port distinct from 443. Deferred:
the initial implementation shares the single Uvicorn socket (`/run/api.sock`),
so no `port` CLI node is shipped until the transport is actually decoupled
(see [CLI Configuration](T9030-mcp-security.md#cli-configuration)).

File Metadata

Mime Type
text/plain
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
3820425
Default Alt Text
T9030-mcp-architecture.md (9 KB)

Event Timeline