Page MenuHomeVyOS Platform

T9030-mcp-dynamic-schema.md

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

T9030-mcp-dynamic-schema.md

# T9030: Dynamic Schema Harvesting & Resources
| Field | Value |
|-------|-------|
| Task | [T9030](https://vyos.dev/T9030) |
| Status | Draft / design |
| Related specs | [Architecture & Transport](T9030-mcp-architecture.md) · [MCP Tools](T9030-mcp-tools.md) · [Security & Concurrency](T9030-mcp-security.md) |
## Principle
All data exposed to the LLM is harvested dynamically from the VyOS XML schema
cache. No tools, resources, or schemas are hardcoded. When VyOS adds new
features (EVPN/VXLAN, new VPN protocols, etc.), the MCP server exposes them
automatically once `reftree.cache` is rebuilt at image-build time.
## Schema Interrogation
The `vyos.xml_ref` library (`python/vyos/xml_ref/`) is the single source of truth.
It lazily loads the compiled reference via `load_reference()` and exposes the
following public functions (signatures from `python/vyos/xml_ref/__init__.py`):
| Function | Returns | Use in MCP |
|----------|---------|------------|
| `cli_defined(path, node, non_local=False)` | `bool` | Validate that a path/node exists before staging it. |
| `is_tag(path)` | `bool` | Mark a node as a tag node (named instance, e.g. interface name). |
| `is_tag_value(path)` | `bool` | Distinguish the tag value level from the tag node. |
| `is_multi(path)` | `bool` | Emit a JSON array vs. a scalar in the schema projection. |
| `is_valueless(path)` | `bool` | Render as a boolean flag (no value expected). |
| `is_leaf(path)` | `bool` | Terminal node — stop schema recursion. |
| `owner(path, with_tag=False)` | `str` | Identify the conf-mode script that owns the subtree. |
| `default_value(path)` | `str \| list \| None` | Populate `default` in the JSON Schema. |
| `get_defaults(path, get_first_key=False, recursive=False)` | `dict` | Bulk default expansion for a subtree. |
| `component_version()` | `dict` | Surface schema/component versioning to clients. |
Config nodes resolve against `vyos.xml_ref.cache.reference`; op-mode nodes
resolve against `vyos.xml_ref.op_cache.op_reference`. Both are generated at build
time from `interface-definitions/` and `op-mode-definitions/` respectively.
Each node entry in the cache carries `node_data` with: `node_type`, `multi`,
`valueless`, `default_value`, `owner`, and `priority` — these are the raw inputs
to the JSON Schema projection below.
## Tool Strategy: Meta-Tools
Instead of exposing thousands of individual tools, the server exposes a small
set of **meta-tools** that accept string arrays of config paths:
- `execute_operational_command` — accepts a `path` array (e.g., `["show", "interfaces"]`)
- `modify_configuration` — accepts an `operations` array of `{op, path, value}` objects
The LLM learns valid paths via MCP Resources (schema introspection) rather than
via a giant tool catalog, keeping the tool list compact and the context window
small. Full tool schemas and behavior are defined in
[T9030-mcp-tools.md](T9030-mcp-tools.md).
## Resource URI Scheme
Resources expose the live configuration tree and operational state as structured
JSON via resource templates. The LLM requests specific slices to avoid
overwhelming its context window.
| Resource Template | Internal Mechanism | Purpose |
|------------------------------------|------------------------------------------------|--------------------------------------|
| `vyos-config://running/{path}` | `Config.show_config(path)` | Active configuration subtree |
| `vyos-config://effective/{path}` | `Config.exists_effective(path)` | Check if a node is effectively applied |
| `vyos-schema://config/{path}` | `vyos.xml_ref.cli_defined()` + projection | JSON Schema of valid CLI nodes |
| `vyos-schema://op/{path}` | `vyos.xml_ref.op_cache.op_reference` | Valid op-mode command continuations |
| `vyos-state://operational/{path}` | `op_mode/*.py` script output | Operational state (routing, counters)|
### Path encoding
- `{path}` is the URL-encoded, slash-joined CLI path
(`vyos-config://running/interfaces/ethernet/eth0`).
- The router splits `{path}` into a list, URL-decodes each element, and rejects
any element containing shell metacharacters or `..` before it touches a VyOS
backend (defense-in-depth in addition to schema validation).
- An empty `{path}` addresses the root of the respective tree.
### Schema Resource (`vyos-schema://`)
Resolves the "chicken-and-egg" problem: the LLM must understand expected syntax
before constructing configuration payloads. Returns dynamically compiled JSON
Schema derived from XML definitions, including:
- Node existence and hierarchy
- Validator constraints (IPv4 format, MAC format, integer ranges)
- Multi-value vs. scalar indicators (`is_multi`)
- Valueless flag nodes (`is_valueless`)
- Tag nodes / named instances (`is_tag`)
- Default values (`default_value`)
#### Projection algorithm (XML reference → JSON Schema)
```
project(path, depth=1):
if is_leaf(path):
node = { "type": "array" if is_multi(path) else "string" }
if is_valueless(path): node = { "type": "boolean" }
if default_value(path): node["default"] = default_value(path)
attach validator-derived constraints (pattern / range / enum)
return node
else:
if depth <= 0:
# bound the payload: advertise children by name, let the LLM drill down
return { "type": "object", "x-children": children(path) }
properties = {}
for child in children(path):
properties[child] = project(path + [child], depth - 1)
if is_tag(path):
return { "type": "object", "additionalProperties": project(tag_value_level, depth - 1) }
return { "type": "object", "properties": properties }
```
The `depth` bound is a defensive guard: the VyOS schema is deep (interfaces,
firewall, routing), and an unbounded projection at the tree root would produce a
multi-megabyte response that blows the client's context window. Responses default
to a shallow projection (`depth=1`) and the LLM requests deeper `{path}`s to
drill down.
Validator constraints are mapped from the XML `<constraint>` elements:
| XML constraint | JSON Schema |
|----------------|-------------|
| `<validator name="numeric" argument="--range 1-256"/>` | `"minimum": 1, "maximum": 256` |
| `<regex>...</regex>` | `"pattern": "..."` |
| `<completionHelp><list>key token</list></completionHelp>` | `"enum": ["key","token"]` |
### Op-mode Schema Resource (`vyos-schema://op/...`)
Mirrors the config schema resource but walks `op_reference`, returning the set
of valid next tokens for a partial op-mode command. This lets the LLM construct
`execute_operational_command` paths without guessing.
### Config Resources (`vyos-config://`)
- `running/{path}`: mirrors REST `/retrieve` (op: `showConfig`); backed by
`Config.show_config(path)` (JSON format).
- `effective/{path}`: mirrors REST `/retrieve` (op: `exists`); backed by
`Config.exists_effective(path)`.
- Sanitizes path parameters and delegates to `vyos.config.Config` objects.
- Returns only the requested subtree, not the entire configuration.
### State Resources (`vyos-state://`)
- `operational/{path}`: invokes the owning `op_mode/*.py` script and returns its
structured output (preferring `--raw`/JSON where the script supports it), so
the LLM reads counters and routing state without an explicit tool call.
## Implementation
The `api.mcp.routers` / `api.mcp.resources` modules:
1. Register resource templates at startup using the SDK's resource-template
registration with the URI schemes above.
2. Intercept `resources/read` JSON-RPC requests.
3. Extract `{path}` from the URI, URL-decode, split, and **validate against the
`vyos.xml_ref` cache** (`cli_defined` for config, `op_reference` for op-mode).
4. Delegate to the VyOS config/op-mode backends (`Config`, op-mode scripts),
running blocking calls via `asyncio.to_thread()` (see
[Concurrency](T9030-mcp-security.md#concurrency--event-loop)).
5. Serialize the result as JSON resource content delivered over the Streamable HTTP
response.
### Caching & performance
- The reference cache is loaded once per process (memoized inside
`load_reference()` via its `cache=[]` default) — schema validation is an
in-memory dict walk, not a disk read.
- The JSON Schema projection for a given `{path}` is deterministic for the life
of the process and may be memoized; it is invalidated only on daemon restart
(i.e., a new image / schema).
- Resource reads never mutate state and are safe to serve concurrently.
- **Token optimization**: serialize responses as compact JSON to preserve the
client's context window and reduce token cost — use `json.dumps(obj,
separators=(',', ':'))`, omit keys whose value equals the schema default, and
strip repetitive boilerplate (e.g. redundant log preambles) from op-mode
output. Keep the output as valid JSON; do not invent a non-standard encoding
that MCP clients cannot parse.
### Failure modes
| Condition | Response |
|-----------|----------|
| `{path}` not in cache | JSON-RPC error: unknown resource path (the LLM self-corrects via the schema resource). |
| Path contains illegal characters | JSON-RPC error before any backend call. |
| Backend op-mode script error | Error string from `vyos.opmode` serialized into the JSON-RPC error. |

File Metadata

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

Event Timeline