neterse
Minimum-token renderings of network CLI output for LLM agents — the
| brief the vendor never shipped.
LLM-driven network agents burn most of their context window on the noise in
show-command output: separator dashes, static legends, wrapped headers,
all-zero counter tables, and keys repeated on every row. neterse rewrites
that output into the smallest representation that preserves the semantics,
before it enters model context — so agents spend tokens on reasoning, not
formatting. Savings of 40–60% are typical on tabular output; on huge
mostly-zero tables, compression is the difference between a truncated tool
result and a complete one.
from neterse import compact, render
# The one verb — smallest faithful text for whatever you're holding:
# a connection, a scrapli Response, raw text, or already-parsed rows
# (with neterse[textfsm] installed, TextFSM parsing competes too):
output = compact(conn, "show interface status")
output = compact(raw, "show interface status", platform="cisco_nxos")
output = compact(rows)
# Full API — every candidate that shrinks the output; policy is yours:
candidates = render(raw, command="show interface status", platform="cisco_nxos")
best = min(candidates, key=lambda c: len(c.text), default=None)
# Parsed tier — rows Genie/ntc-templates already produced, re-encoded
# header-once (beats json.dumps(rows) by ~45-50% on multi-row output):
candidates = render(raw, command="show ip int brief", parsed=rows)
# Opt-in declared-lossy projection; the rendering itself says what was
# withheld: "[omitted: name, vlan, ... — re-query profile=full]"
candidates = render(raw, command="show interface status", profile="updown")
Port Name Status Vlan Duplex Speed Type
--------------------------------------------------------------------------------
Eth1/11 RSRFF206 Twe1/0/3 connected routed full 10G 10Gbase-SR
Eth1/45 RFRA3213-Eth1/48 connected routed full 10G 10Gbase-LR
│
▼ neterse
port,name,status,vlan,duplex,speed,type
Eth1/11,RSRFF206 Twe1/0/3,connected,routed,full,10G,10Gbase-SR
Eth1/45,RFRA3213-Eth1/48,connected,routed,full,10G,10Gbase-LR
One verb for netmiko, scrapli — and whatever comes next
compact dispatches on the shape of what you hand it, never on the
producing library, so the call looks the same everywhere and neterse
never imports any runner library:
from neterse import compact
# a connection — netmiko, scrapli, any work-alike with send_command
# (extra kwargs pass through to the library call):
output = compact(conn, "show interface status")
# a scrapli Response you already have:
output = compact(response)
# raw text from anywhere:
output = compact(raw, "show ip route", platform="cisco_ios")
# rows something already parsed — netmiko use_textfsm=True,
# NAPALM getters, gNMI, plain dicts/lists:
output = compact(rows)
The raw and TextFSM-parsed tiers compete whenever parsing is possible
(pip install neterse[textfsm]), and platform strings resolve the way
the ecosystem actually spells them — netmiko device_types
(cisco_ios_ssh, cisco_xe), scrapli's cisco_iosxe, plain ntc names.
Everything stays fail-open: no template, no extra, nothing shrinkable —
you get the original back, byte-identical. scrapli's
send_commands([...]) returns a MultiResponse carrying no per-command
info; map it: [compact(r) for r in multi].
A future library is a small source adapter
(neterse.sources.register_adapter) — never a new API name.
MCP tool results
The same payloads reach agents through MCP servers, and the client's
server config is the one seam you control for every server — so neterse
ships a proxy (pip install "neterse[mcp]").
Wrap any server from your client's config
Wherever your client lists MCP servers — mcp.json in VS Code,
claude mcp add in Claude Code, the equivalent in Cursor — point the
entry at neterse-mcp and hand it the upstream you would have
configured. An HTTP upstream is one argument:
"aci": {
"command": "neterse-mcp",
"args": ["https://apic-mcp.example.com/mcp"]
}
A stdio upstream is the original command line, verbatim:
"github": {
"command": "neterse-mcp",
"args": ["docker", "run", "-i", "--rm", "-e", "GITHUB_PERSONAL_ACCESS_TOKEN",
"ghcr.io/github/github-mcp-server"],
"env": { "GITHUB_PERSONAL_ACCESS_TOKEN": "..." }
}
(the proxy forwards its full environment to a stdio upstream, so env
blocks keep working). An authenticated URL upstream moves its headers
block onto the command line, curl-style and repeatable:
"sdwan": {
"command": "neterse-mcp",
"args": ["https://vmanage-mcp.example.com/mcp",
"--header", "Authorization: Bearer TOKEN"]
}
No global install needed —
"command": "uvx", "args": ["--from", "neterse[mcp]", "neterse-mcp", ...]
resolves it on the fly, and from a source checkout use
"command": "uv", "args": ["run", "--isolated", "--with-editable", "/path/to/neterse", "--with", "fastmcp", "neterse-mcp", ...] —
--isolated matters: without it, a workspace venv that has any released
neterse installed can shadow the checkout and silently run old code
(uvx is isolated by nature).
Restart the server entry and watch its output/log pane: every compacted call prints one ledger line on stderr —
[neterse] aci_bridge_domains_get: 30,190 -> 6,617 chars (~7,547 -> ~1,654 tok, -78%)
Across a 16-tool live corpus (a real ACI fabric and a real SD-WAN
overlay) the proxy removes 61% of the characters — measured 46% of the
real o200k tokens. Redundancy folds instead of repeating: a column whose
value is identical in every row (an ACI class response repeats the class
name and a dozen defaults per object) collapses to one leading
[31 rows, each: _class=fvBD,lcOwn=local,...] line, values intact, row
count preserved — lossless, like everything on the default path.
What the model sees
Per tools/call result, a text block whose content is JSON comes back
re-encoded through the parsed tier (vendor envelopes like ACI imdata
included) — and only when strictly smaller than the wire text; nothing
ever grows. A structuredContent that merely mirrors that block — the
way FastMCP duplicates every tool return — follows it instead of
smuggling the original back beside the compacted text: the
{"result": <text>} string mirror is rewritten, a deep-equal copy is
dropped, and listed tools shed outputSchema accordingly (the proxy's
contract is "the text block is the payload"). Everything else passes
through byte-identical: non-JSON text, error results, images, input
schemas, and structured content that carries anything of its own.
Flags: --quiet silences the ledger; --keep-structured disables the
dedupe and preserves upstream structuredContent and outputSchema
verbatim (at the cost of hosts feeding the model both copies).
Servers you author, loops you own
For a FastMCP server you author, skip the proxy — it's one line:
from neterse.mcp import CompactMiddleware
mcp.add_middleware(CompactMiddleware())
When your consumer is an agent loop you own, you don't need MCP
middleware at all — call compact() on the result before it enters
context.
Install
pip install neterse # zero dependencies, import name: neterse
pip install "neterse[all]" # every optional capability below (or [full])
Nothing is ever required — each extra widens what neterse can do and the core imports the standard library only. Pick individually if you prefer:
| extra | pulls in | what it buys |
|---|---|---|
textfsm |
ntc-templates |
neterse.ntc parses and compresses in one call |
gcf |
gcf-python |
the GCF authors' encoder — named tables, API envelopes, ragged rows |
toon |
toon-format |
the same, for TOON |
tokens |
tiktoken |
real token counts in scripts/neterse_report.py (the runtime stays chars/4) |
mcp |
fastmcp |
neterse-mcp, a proxy that compacts any MCP server's tool results, + CompactMiddleware for FastMCP servers |
Without gcf/toon the stdlib encoders stand in and cover fewer shapes;
without textfsm you parse it yourself and pass parsed=. Everything
fails open, so a missing extra costs you candidates, never a traceback.
Named neterse ("network terse") because the natural name
terseis occupied on PyPI by an unrelated package abandoned in 2019. Distribution, import, and CLI all share the one name.
Design in one paragraph
Two tiers, one contract. The raw-text tier compresses CLI output
directly — declarative specs, authored as one YAML file per
vendor/command (neterse/specs/<vendor>/<family>.yaml, compiled to
plain dicts so the runtime stays dependency-free), drive generic
strategies (fixed-width table → CSV, line-regex table → CSV, key-value
scan), with plain-Python compressors as the escape hatch for genuinely
stateful formats. The parsed tier re-encodes rows that Genie /
ntc-templates / TTP / NAPALM already parsed into compact header-once
form (render(..., parsed=rows) → CSV, spec-compliant TOON and GCF
generic-profile candidates; the CSV typically undercuts
json.dumps(rows) by ~45–50%) — and the optional
neterse.ntc front-end (pip install neterse[textfsm]) runs
ntc-templates for you, so one call covers both tiers. Opt-in profiles (profile="updown") narrow output to a
declared projection and say so inline
([omitted: … — re-query profile=full]). Both tiers emit Candidates;
the library never picks a winner — smallest-wins, ledgers, metrics,
and caching belong to the consumer. Full architecture:
docs/DESIGN.md.
Invariants
- Fail-open, always. A compressor that raises, returns a non-string, returns empty, or fails to shrink produces no candidate. Raw data is never lost and never enlarged.
- Zero runtime dependencies. Standard library only — enforced in CI.
Tokenizers are a CI concern;
Candidate.est_tokens()is a chars/4 estimate by design. - Candidates, not policy. Consumers decide what to send to the model.
- Preserve semantically relevant data; declare every drop. Noise
(separators, legends, all-zero rows — kept visible via explicit
(all zero)markers) is dropped freely; anything else a rendering omits must be declared machine-readably on the candidate (Candidate.dropped_fields). - Byte-parity discipline. The original implementation is
frozen in-tree (
tests/legacy_snapshot.py); the parity suite replays a cross-matrix corpus and pins today's outputs byte-for-byte. Engine refactors may change how, never what. Intentional output changes are recorded baseline decisions.
Provenance & prior art
neterse grew out of a TOON optimizer ("Token-Optimized Output for
Networks", inspired by NetClaw's TOON serialization work) built for a
network agent, and is now a standalone, community-extensible library. It
complements — not competes with — the parsing ecosystems: ntc-templates,
Genie/pyATS and TTP turn CLI text into structure; neterse makes
structure (and unparseable raw text) cheap to show to a model. It also
complements the token-format ecosystem rather than betting on one
notation: the parsed tier emits spec-compliant
TOON — Token-Oriented Object Notation and GCF documents as
candidates alongside its own CSV, so smallest-wins (or your policy —
TOON's [N]{fields} truncation guardrails, GCF's null-vs-absent
distinction and tooling ecosystem) decides per payload.
Roadmap
| Phase | Contents | Status |
|---|---|---|
| 0 | API frozen (render/Candidate/optimize/register); 15 compressor families extracted verbatim; byte-parity baseline |
✅ |
| 1 | Spec engine (generic strategies over declarative specs), platform-keyed dispatch, declared-lossiness manifests | ✅ |
| 2 | Parsed tier: field projection + compact encoders (CSV/TOON) over pre-parsed rows; opt-in profiles with inline omission markers; kv_extract strategy |
✅ |
| 3 | neterse audit coverage tool, fixture-per-file contribution flow, CI token-savings regression (pinned tokenizer), multi-vendor expansion (Arista EOS, Junos, Aruba AOS-CX, MikroTik), PyPI release machinery |
✅ |
| 4 | Contributor scale-out: YAML spec authoring (one specs/<vendor>/<family>.yaml per family, compiled — never parsed — at runtime), registry self-append, neterse[textfsm] extra driving ntc-templates end-to-end |
✅ |
| 5 | Runner integration: the universal compact() verb (shape dispatch over connections / responses / raw / rows; source adapters for future libraries) + rows-only render_parsed/optimize_parsed for already-parsed output |
✅ |
| 6 | Interop format candidates: spec-compliant TOON ([N]{fields} declarations double as truncation guardrails) and GCF generic-profile encoders in the parsed tier — emitted only when the document conforms, chosen only when your policy wants them |
✅ |
| 7 | Consumers swap vendored copies for the pip dependency; propose a TOON profile for network data upstream | ▢ |
| 8 | Beyond the CLI: the same candidates contract for MCP tool results (the neterse-mcp proxy + CompactMiddleware, neterse[mcp]), API responses, and generic JSON payloads |
◐ |
| 9 | Session/delta encoding — render only what changed since the agent's last poll of the same command. Deliberately last: stateful and correctness-sensitive, it needs per-command row-key definitions and extensive testing before it can ship | ▢ |
Coverage
Command families ship for Cisco IOS/IOS-XE/NX-OS (15 legacy families:
routes, interfaces, BGP/OSPF/EIGRP neighbors, CDP/LLDP, VLANs, ACLs,
counters, port-channels, version, running-config), Arista EOS
(interfaces status, ip arp, vlan), Juniper Junos (interfaces terse,
ospf neighbor), Aruba AOS-CX (interface brief, vlan) and MikroTik
RouterOS (/ip address print, /interface print) — and the parsed
tier covers anything your parser already handles, on any platform.
Measure coverage over your own agent's traffic with the bundled CLI:
neterse audit run.jsonl --show 3 # {"command":..., "platform":..., "raw":...} per line
neterse audit tests/fixtures # or point it at a fixture tree
It reports per-family reduction, what reached the model uncompressed
(NO COMPRESSOR / false-match / platform-skip), and each winning
entry's declared drops — the gaps it prints are, in order, the next
specs worth contributing.
Contributing
See CONTRIBUTING.md. Short version: most new command
families are one YAML spec file plus two fixture files —
neterse/specs/<platform>/<family>.yaml (the vendor/command layout
ntc-templates made familiar) and
tests/fixtures/<platform>/<family>/{commands.txt,raw.txt} — no parser
code, no registry edit, and the suite auto-covers anything dropped
there. python scripts/compile_specs.py validates the spec loudly and
regenerates the zero-dependency runtime module. The escape hatch for
stateful formats is a plain function under the same fail-open contract.
License
Apache-2.0 — see LICENSE.
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file neterse-0.5.0.tar.gz.
File metadata
- Download URL: neterse-0.5.0.tar.gz
- Upload date:
- Size: 125.7 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
c09692c12979394c8facaa168eeeeb71230b1f9c31c9a954679d3fd0be57ccda
|
|
| MD5 |
30a5291e9b51bde31bae39b6b6e56ec7
|
|
| BLAKE2b-256 |
0b7402991c9f2bb225b0aa0adcad267bfd2a1102c2c800776dd070c914d884d7
|
Provenance
The following attestation bundles were made for neterse-0.5.0.tar.gz:
Publisher:
release.yml on pcDamasceno/neterse
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
neterse-0.5.0.tar.gz -
Subject digest:
c09692c12979394c8facaa168eeeeb71230b1f9c31c9a954679d3fd0be57ccda - Sigstore transparency entry: 2366647255
- Sigstore integration time:
-
Permalink:
pcDamasceno/neterse@3a7cb900ef70c749fa25204decf3c9275fd81355 -
Branch / Tag:
refs/tags/v0.5.0 - Owner: https://github.com/pcDamasceno
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@3a7cb900ef70c749fa25204decf3c9275fd81355 -
Trigger Event:
push
-
Statement type:
File details
Details for the file neterse-0.5.0-py3-none-any.whl.
File metadata
- Download URL: neterse-0.5.0-py3-none-any.whl
- Upload date:
- Size: 83.3 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
7bb7787c590b5fce7df4af373000d2a3f38b3059dfbf82f78e0e280e6c6aa7e2
|
|
| MD5 |
7c9921ee709010e9829ad047c9a47a66
|
|
| BLAKE2b-256 |
f09fd452354c6c11290d1d93092e8517ca0f1295690bdcd45cd133dbd634ba1b
|
Provenance
The following attestation bundles were made for neterse-0.5.0-py3-none-any.whl:
Publisher:
release.yml on pcDamasceno/neterse
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
neterse-0.5.0-py3-none-any.whl -
Subject digest:
7bb7787c590b5fce7df4af373000d2a3f38b3059dfbf82f78e0e280e6c6aa7e2 - Sigstore transparency entry: 2366647300
- Sigstore integration time:
-
Permalink:
pcDamasceno/neterse@3a7cb900ef70c749fa25204decf3c9275fd81355 -
Branch / Tag:
refs/tags/v0.5.0 - Owner: https://github.com/pcDamasceno
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@3a7cb900ef70c749fa25204decf3c9275fd81355 -
Trigger Event:
push
-
Statement type: