Page MenuHomeVyOS Platform

T9030-mcp-implementation.md

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

T9030-mcp-implementation.md

# T9030: Implementation — Files & Deployment
## Files Created in vyos-1x Repo
```
src/services/api/mcp/
├── __init__.py # empty
├── routers.py # mcp_init / mcp_clear: Streamable HTTP session manager mount/unmount
├── server.py # mcp.server.Server instance + dynamic capability registration
├── tools.py # Dynamic meta-tool dispatch (validated against schema cache)
├── resources.py # Dynamic resource registration (harvested from schema)
├── schema.py # XML reference -> JSON Schema projection
├── prompts.py # Prompt templates for LLM guidance
└── auth.py # MCP-specific auth integration (reuses check_auth/JWT helpers)
```
## Files Modified in vyos-1x Repo
| File | Change |
|------|--------|
| `src/services/vyos-http-api-server` | Add `mcp_init(app)` / `mcp_clear(app)` calls in `initialization()`, mirroring the REST/GraphQL pattern. Read the `mcp` block from `api_config_state`. |
| `src/services/api/session.py` | Add `mcp`, `mcp_mode`, `mcp_introspection` attributes to `SessionState._initialize()`, mirroring the existing `rest`/`graphql` flags. |
| `src/conf_mode/service_https.py` | In `generate()`: include the MCP config subtree (mode, introspection) when writing `/run/http-api-state`. |
| `interface-definitions/service_https.xml.in` | Add the MCP CLI node tree under `service https api mcp` with leaf nodes for mode and introspection. Standard VyOS validators. |
| `debian/control` | Add `python3-mcp` (or equivalent) as a dependency of `vyos-1x`. |
### `src/services/vyos-http-api-server` — Changes
In `initialization()`, after the existing REST/GraphQL blocks and following the
identical boolean-flag convention (`session.rest`, `session.graphql`):
```python
# parse the mcp block from api_config_state into SessionState, mirroring
# the rest/graphql handling already present in initialization()
mcp_config = server_config.get('mcp', {})
if 'mcp' in server_config:
session.mcp = True
session.mcp_mode = mcp_config.get('mode', 'read-only')
session.mcp_introspection = bool('introspection' in mcp_config)
else:
session.mcp = False
# ... after `app.state = session`, alongside rest_init/graphql_init:
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)` takes only `app` and reads its configuration from `app.state`
(the `SessionState` singleton), exactly like `rest_init(app)` / `graphql_init(app)`
— it does **not** take a separate config argument. There is a single Uvicorn
server bound to `/run/api.sock`; MCP shares it and does **not** get its own
listen port in the initial implementation (see
[port discussion](T9030-mcp-security.md#cli-configuration)).
### `src/conf_mode/service_https.py` — Changes
In `generate()`, where the `api` subtree of the config dict is serialized into
`/run/http-api-state`, include the `mcp` block so the running server can read it
back as `session.mcp*`:
```python
# include the mcp subtree in the api state dict before serialization,
# alongside the existing rest/graphql handling
if 'mcp' in https['api']:
api_dict['mcp'] = https['api']['mcp']
```
Defaults are resolved by `get_config_dict(..., get_first_key=True,
with_recursive_defaults=True)` (already used for the `api` subtree), so the
`mode` default lands in the dict without manual merging.
### `interface-definitions/service_https.xml.in` — New Nodes
The new nodes follow the conventions already used by the sibling `rest` and
`graphql` nodes in the same file: a valueless flag for boolean toggles (compare
`graphql/introspection`) and an unanchored `<regex>` constraint (VyOS anchors
regex constraints implicitly — do **not** wrap them in `^...$`).
```xml
<node name="mcp">
<properties>
<help>Model Context Protocol (MCP) endpoint for AI agent integration</help>
</properties>
<children>
<leafNode name="mode">
<properties>
<help>MCP tool access level</help>
<completionHelp>
<list>read-only read-write</list>
</completionHelp>
<valueHelp>
<format>read-only</format>
<description>Read configuration and run operational commands only</description>
</valueHelp>
<valueHelp>
<format>read-write</format>
<description>Additionally allow configuration changes and system management</description>
</valueHelp>
<constraint>
<regex>(read-only|read-write)</regex>
</constraint>
</properties>
<defaultValue>read-only</defaultValue>
</leafNode>
<leafNode name="introspection">
<properties>
<help>Expose schema introspection resources to AI agents</help>
<valueless/>
</properties>
</leafNode>
</children>
</node>
```
> A dedicated MCP listen port (decoupled from the main HTTPS port) is deliberately
> **not** modelled here. Committing a CLI node that the daemon silently ignores is
> poor VyOS practice; the port is tracked as future work (see the
> [Architecture open questions](T9030-mcp-architecture.md#open-questions)).
## Python Dependencies & Debian Packaging
The MCP Python SDK (`mcp`) and its dependencies (like `anyio`, `starlette`, etc.) must be available to the HTTP API server. Since official Model Context Protocol Python SDK packages may not be available as native upstream Debian packages for the version VyOS tracks, the build pipeline must be updated:
1. Add `python3-mcp` (and required transitive dependencies) to the package building infrastructure so they can be listed in `debian/control`.
2. Alternatively, install these dependencies into the `vyos-http-api-tools` virtualenv during the build process.
On the running system, the virtualenv at `/usr/share/vyos-http-api-tools/`
(the interpreter named in the `vyos-http-api-server` shebang) must include:
- `mcp` (Model Context Protocol Python SDK)
- `mcp.server.Server` (low-level server used for dynamic capability registration)
- `mcp.server.streamable_http_manager.StreamableHTTPSessionManager` (Streamable HTTP transport)
- `mcp.server.streamable_http.StreamableHTTPServerTransport` (per-session transport)
## Files on Running System
### API module files
```
/usr/libexec/vyos/services/api/mcp/
├── __init__.py
├── routers.py
├── server.py
├── tools.py
├── resources.py
├── schema.py
├── prompts.py
└── auth.py
```
Installed via `debian/vyos-1x.install` preserving `src/services/api/` →
`/usr/libexec/vyos/services/api/`.
### Updated files
| Path | Purpose |
|------|---------|
| `/usr/libexec/vyos/services/vyos-http-api-server` | Updated with MCP init/clear logic |
| `/usr/libexec/vyos/conf_mode/service_https.py` | Updated to write MCP state |
| `/run/http-api-state` | JSON includes `mcp` block when configured |
| `/run/systemd/system/vyos-http-api.service` | Generated from template (no template changes needed) |
| `/run/api.sock` | Existing Unix socket (no change — MCP shares same Uvicorn) |
### Generated at commit time (by `service_https.py`)
`/run/http-api-state` with MCP block:
```json
{
"keys": { ... },
"rest": { ... },
"graphql": { ... },
"mcp": {
"mode": "read-only",
"introspection": {}
}
}
```
`introspection` is a valueless flag: its presence (an empty dict in the
`get_config_dict` projection) means schema resources are exposed; its absence
means they are suppressed.
### MCP endpoints on running system
| Method | Path | ASGI Mount |
|--------|------|------------|
| GET/POST/DELETE | `/mcp` | `StreamableHTTPSessionManager.handle_request()` → auth wrapper |
Served by the existing Uvicorn on `/run/api.sock`, reverse-proxied
by Nginx on port 443. No additional ports or sockets.
## Startup Sequence
1. Administrator configures `set service https api mcp` and commits.
2. `service_https.py` writes MCP block to `/run/http-api-state`.
3. `vyos-http-api-server` receives SIGHUP, `reload_handler` triggers.
4. Server tears down, re-runs `initialization()`.
5. `mcp_init(app)` is called:
- Creates an `mcp.server.Server` instance named `"VyOS"`.
- Dynamically reads `reftree.cache` / `op_cache.json` to register resources and tools.
- Creates `SseServerTransport` bound to the server instance.
- Mounts `/mcp` (single endpoint handling GET/POST/DELETE) on the FastAPI app.
- Applies mode filtering (`read-only` omits write tools at registration time).
- Applies introspection control (unset suppresses schema resources).
6. LLM client connects via `GET /mcp` (passing API key in headers).
7. Client sends JSON-RPC 2.0 requests via `POST /mcp` with `Mcp-Session-Id` header.
## Build Impact
- `dpkg-buildpackage` or `make deb` picks up new `api/mcp/` files via
`debian/vyos-1x.install`.
- XML preprocessing via `make all` processes updated
`interface-definitions/service_https.xml.in`.
- No changes to `Makefile` targets required — new files are plain Python
(no compilation needed).
- `libvyosconfig/` — no changes (MCP uses existing Python APIs only).
## Testing and QA Strategy
- **Unit Tests**: Add tests under `tests/` verifying the dynamic schema projection in isolation by mocking `vyos.xml_ref`.
- **Smoke Tests**: Add test cases to the `smoketest/` suite to verify the Streamable HTTP handshake and JSON-RPC tool invocation. The tests should be executable via `vyos-build`'s `check-qemu-install --smoketest`.
## Documentation Plan
- The `vyos-documentation` repository must be updated.
- Document the new `set service https api mcp *` nodes.
- Provide usage guidelines, security considerations, and examples for administrators integrating AI orchestrators.

File Metadata

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

Event Timeline