MCP Server¶
Splice ships a Model Context Protocol server that exposes the bridge as a set of tools for AI agents (Claude Code, Claude Desktop, and any MCP-capable client). It runs over stdio, requires no database and no YAML config, and is built directly on the Python API — an agent can connect to a NETCONF device, read telemetry, search the YANG catalog, and (subject to policy) push configuration, all through structured tool calls.
Install the extra
The MCP server depends on the mcp package. Install it with the mcp extra:
Starting the server¶
The server is launched by the splice mcp subcommand. Unlike most commands, mcp does not require --config — it manages its own in-memory device sessions.
# Plain server on stdio
splice mcp
# With a set policy (controls what set_config may write)
splice mcp --policy policy.yaml
# With an audit log of every tool call (JSON-lines)
splice mcp --audit-log audit.jsonl
# Both together
splice mcp --policy policy.yaml --audit-log audit.jsonl
| Flag | Default | Purpose |
|---|---|---|
--policy |
none (deny-all) | Path to a set-policy YAML file. Without it, all writes are denied. |
--audit-log |
splice_audit.jsonl |
Path to the JSON-lines audit file. Every tool call is appended here (and to Python logging). |
Run it from your MCP client, not by hand
splice mcp speaks the MCP protocol on stdin/stdout — it is not interactive. Start it from an MCP client (see Wiring into Claude Code) rather than from a normal shell.
The tools¶
The server registers 11 tools. Every agent workflow starts with connect_device, which returns a device_id handle that all other tools take as their first argument.
| Tool | Purpose | Key parameters |
|---|---|---|
connect_device |
Connect to a NETCONF device (real or simulated), run discovery + catalog build, return a device_id. |
host, port (830), username (admin), password, device_id, yang_dir, skip_discover, skip_fetch_yang, skip_catalog, simulate |
disconnect_device |
Close a session and release resources. | device_id |
get_telemetry |
Read current values for a path or glob via a direct NETCONF get. | device_id, path |
set_config |
Push a config value via NETCONF edit-config. Subject to the set policy. | device_id, path, value, operation (merge), dry_run (False) |
find_paths |
Glob-match the catalog and return canonical paths. | device_id, pattern |
search_paths |
Natural-language search over the catalog (hybrid BM25 + embeddings). | device_id, query, top_k (10) |
subscribe_once |
One-shot read of all values under a path prefix (gNMI Subscribe ONCE equivalent). | device_id, path |
list_devices |
List all connected sessions with status, idle/uptime, catalog size. | — |
device_health |
Connection state, circuit-breaker status, and timing for one device. | device_id |
doctor |
Diagnostic checks (connection, circuit breaker, catalog, cache) plus stats. | device_id |
list_capabilities |
Readable vs. writable catalog paths, optionally filtered. | device_id, pattern (*) |
connect_device¶
Connecting runs the full discovery pipeline (NETCONF session → module discovery → YANG fetch → catalog build), which can take a few minutes on large devices. Use the skip_* flags to reuse cached schemas and start instantly:
yang_dir— a directory of cached.yangfiles.skip_fetch_yang=true— don't re-fetch schemas (requiresyang_dirpopulated).skip_discover=true— don't re-discover modules.skip_catalog=true— defer catalog build (thenfind_pathsmay return empty).
The simulate parameter (e.g. "openroadm") connects to a built-in simulated backend instead of real hardware — no device needed. The agent may also pass host="sim://openroadm", which is recognized and treated the same way.
Connecting to the simulator
An agent calls connect_device(simulate="openroadm") and gets back:
set_config and dry_run¶
set_config supports operation values merge (default), replace, and delete. Pass dry_run=true to run the policy check only — the tool returns "policy_check": "passed" without touching the device. This lets an agent verify a write is permitted before committing to it.
Every form a path could resolve to (raw, predicate-stripped, alias-resolved) is checked against the policy, fail-closed: a write is forbidden if any form matches a forbidden pattern, and allowed only if every form is in the allowed list.
Safety¶
The MCP surface is designed to be handed to an autonomous agent, so writes and request rates are gated.
Set policy (deny-all by default)¶
set_config is governed by a SetPolicy. With no --policy file, the policy is deny-all — every write is rejected with SET_POLICY_DENIED and the message no paths are allowed (default deny-all policy). To permit writes you must explicitly allow paths.
A policy YAML supports three keys:
| Key | Meaning |
|---|---|
allowed_paths |
Glob patterns (fnmatch) a path must match to be writable. Empty ⇒ deny all. |
forbidden_paths |
Glob patterns that are always denied — forbidden overrides allowed. |
value_constraints |
Per-path-pattern numeric bounds (min / max) checked against the value. |
The file may put these under a top-level set_policy: block, or at the document root.
policy.yaml
set_policy:
allowed_paths:
- "/interfaces/interface[*]/config/description"
- "/components/component[*]/*/config/target-output-power"
forbidden_paths:
- "*/admin-state" # never let the agent shut a port
- "*/reset"
value_constraints:
"/components/component[*]/*/config/target-output-power":
min: -20.0
max: 3.0
admin-state, or an output power above 3.0, is denied before reaching the device.
Validate first
Have the agent call set_config(..., dry_run=true) to confirm the policy permits a write, then repeat without dry_run to apply it.
Rate limiting¶
Each device is rate-limited to 30 RPCs per minute over a sliding 60-second window (configurable via create_mcp_server(max_rpcs_per_minute=...)). The limiter applies to the RPC-issuing tools — get_telemetry, set_config, and subscribe_once. When the limit is hit, the tool returns a RATE_LIMITED error carrying retry_after_sec (the seconds until the oldest call ages out of the window).
Idle session cleanup¶
Sessions idle for longer than 30 minutes are closed automatically; cleanup is piggybacked on every tool invocation. A device_id whose session has been reclaimed yields DEVICE_NOT_CONNECTED on its next use — the agent should connect_device again.
Audit log¶
When --audit-log is set (default splice_audit.jsonl), every tool call appends one JSON object: timestamp, tool name, device_id, parameters, status, duration, and error_code on failure. Sensitive parameter values are redacted — any key named password, passwd, secret, token, or credential is written as ***REDACTED***.
{"timestamp": "2026-06-30T12:00:00+00:00", "tool": "connect_device", "device_id": "sim_openroadm_1a2b3c4d", "params": {"host": "sim://openroadm", "password": "***REDACTED***"}, "status": "success", "duration_ms": 842.13}
Error taxonomy¶
Tools never raise across the MCP boundary; on failure they return a structured error object. Every error carries:
error_code— one of the codes below.error: trueandstatus: "error".message— the underlying detail.retryable— whether retrying may succeed.retry_after_sec— suggested wait before retry (nullwhen not retryable).remediation_hint— a human/agent-readable next step.
error_code |
Retryable | retry_after_sec |
Meaning / hint |
|---|---|---|---|
AUTH_LOCKOUT |
yes | 300 | Device locked out after auth failures. Wait 5 minutes. |
SESSION_LIMIT |
yes | 60 | Device hit its max concurrent session limit. Close sessions or wait. |
DEVICE_UNREACHABLE |
yes | 10 | Network connectivity problem reaching the device. |
CIRCUIT_BREAKER_OPEN |
yes | 60 | Breaker open after repeated failures; wait for cooldown. |
PATH_NOT_FOUND |
no | — | Path not in the catalog. Use find_paths to locate valid paths. |
SET_REJECTED |
no | — | Device rejected the write (not writable, or bad value). |
SET_POLICY_DENIED |
no | — | Blocked by the set policy. Adjust allowed_paths. |
DISCOVERY_TIMEOUT |
yes | 30 | Discovery too slow. Retry with skip_discover=true. |
DEVICE_NOT_CONNECTED |
no | — | No live session for that device_id. Call connect_device first. |
RATE_LIMITED |
yes | (dynamic) | Too many RPCs this window; retry_after_sec gives the exact wait. |
Semantic search availability
search_paths always works in keyword-only (BM25) mode. If no embedding provider is installed, the result includes a note recommending pip install splice[search] for semantic ranking — it does not error.
Wiring into Claude Code¶
Register Splice as an MCP server in your client's MCP configuration. The command is splice mcp; pass --policy / --audit-log as extra args. Use the path to the splice entry point inside your virtualenv (or python -m splice.cli if not installed as a script).
{
"mcpServers": {
"splice": {
"command": "/path/to/.venv/bin/splice",
"args": ["mcp", "--policy", "/path/to/policy.yaml", "--audit-log", "/path/to/audit.jsonl"]
}
}
}
Start with the simulator
Before pointing an agent at real hardware, have it connect_device(simulate="openroadm"). It exercises the full tool surface — discovery, catalog search, telemetry, and policy-gated writes — with no device and no risk.
A typical agent flow¶
connect_device(host="192.0.2.10", username="admin", password="…")→device_id.search_paths(device_id, "optical receive power")orfind_paths(device_id, "*/components/*")to locate paths.get_telemetry(device_id, path)/subscribe_once(device_id, path)to read values.list_capabilities(device_id, pattern)to see which paths are writable.set_config(device_id, path, value, dry_run=true)to validate, then again withoutdry_runto apply (policy permitting).doctor(device_id)/device_health(device_id)to check connection and circuit-breaker state.disconnect_device(device_id)when done.
See also¶
- Python API — the
Deviceclass the MCP tools wrap. - Configuration — YAML config for the CLI/server stacks.
- CLI Reference — the full
splicecommand surface. - Simulation — running against the built-in simulated device.