asn1-docs writes a documentation site from an ASN.1 module. If the project has a Schematron rule set, asn1-docs adds the rules to the pages.
Each element page starts with the element itself. The page gives the type, the size limit, the position in the tree, and the fields. The rules that constrain the element follow in a separate section.
The order is deliberate. A reader must find the answer on the page, and not in the specification.
Nobody writes these pages by hand. asn1-docs reads three inputs:
| Input | Supplies |
|---|---|
| the element tree, as JSON | element names, paths, types, and optionality |
the .asn module |
declarations, size limits, and the comments in the module |
*.sch |
the rules, with id, role, clause, message, and line |
An element name in XER is not always the field name. XER names the members of a
SEQUENCE OF after their type. An instance therefore contains
<PINConfiguration>, although no field has that name.
A program that reads the module alone must parse ASN.1 to get this right. A second parser is a second thing that can disagree with the first.
asn1-docs reads the tree from the runtime descriptors of asn1c. The tool
vn-tree in asn1c-vn writes that tree as
JSON. asn_TYPE_descriptor_t holds the member names, their types, and their
optionality. The field xml_tag holds the name that XER writes.
The first version of asn1-docs did parse the module by hand. It produced four wrong trees, and each failure was silent. The descriptors remove the guess.
-
Write the element tree:
make vn-tree GEN_DIR=<asn1c output> PDU=<root type> ./vn-tree > build/tree.json
-
Write
asn1-docs.jsonnext to the rules:{ "title": "eUICC Profile Reference", "version": "SAIP 3.4.1", "description": "Element reference and rules for profile packages.", "site": "https://example.github.io", "base": "/my-validator", "outDir": "../site", "repo": "https://github.com/example/my-validator/blob/main", "tree": "build/tree.json", "asn": "schema/module.asn", "rules": "rules", "runTests": "tests/run-tests", "descriptions": "descriptions.json", "out": "docs-site" } -
Run the command:
asn1-docs
-
Build the site:
cd docs-site && npm install && npm run build
Four keys are required: title, tree, asn, and out. The rest are optional.
| Key | Effect |
|---|---|
rules |
If you omit this key, the site holds element and type pages only. |
repo |
If you omit this key, source references appear as plain text. Links to a repository that does not exist are worse than no links. |
descriptions |
If you omit this key, elements without a comment get a derived label. |
runTests |
The file that names one counter-example for each rule. |
version |
The header shows this text next to the title. |
The command writes the Astro project into out, and then writes the pages into
it. The file src/styles/theme.css is an exception: asn1-docs writes it once
and never replaces it, because a project can change its own colors.
The module comments some fields and not others. Where a name has no comment,
asn1-docs derives a label from the name and the position in the tree. The
element ef-imsi under usim becomes "EF IMSI of the usim profile element".
A descriptions.json file gives one sentence for each element that needs more
than a label.
asn1-docs marks these sentences with a badge. The tooltip states that the sentence does not come from a normative document.
This difference is the reason for the badge. A rule cites its clause, and a reader can check that clause against a new edition of the specification. A written sentence has no clause.
asn1-docs also writes index.json, a machine-readable copy of the same model.
The built site serves it, so a reader needs the address alone and no checkout.
asn1-docs-mcp serves that file over the Model Context Protocol. It gives an
agent the graph, and not the prose:
element --has type--> type
element --contains--> element
element --constrained--> rule
type --used by--> element
rule --applies to--> element
pip install mcp
asn1-docs-mcp docs-site/public/index.json
asn1-docs-mcp https://euicc.waigel.com/index.jsonSix tools: overview, find, entity, neighbours, rules_by_clause and
unchecked_elements.
Plugins for Starlight exist that serve the pages instead: starlight-mcp, @stellayazilim/mcp-starlight and starlight-llms-txt. All three search rendered text. Two common questions have an answer in the graph and none in the prose: "which rules cite clause 8.3.3", and "which elements does nothing check". This server therefore reads the model.
The mcp package is needed for the server alone. The page generator does not
import it.
asn1-docs does not read the specification. The clause references come from the
@see attribute of each rule. asn1-docs prints them and does not resolve them.
asn1-docs cannot tell you that a description is out of date. No mechanism for this exists. A project that writes descriptions must read them again after each new edition of the specification.
asn1c writes a type name where a member has no name of its own. asn1-docs skips
three of these: OCTET_STRING, OBJECT_IDENTIFIER, and SEQUENCE. They are
type names in the element namespace, and not elements that a person writes.
asn1_docs/
rules.py reads *.sch: id, role, clause, message, line, and the
element that each rule is about, from its XPath context
asn1doc.py reads the .asn: declarations, size limits, comments
generate.py joins the three inputs and writes Markdown
scaffold.py writes the Astro project that renders the Markdown
mcp_server.py serves the same model as a graph, over MCP
template/ the parts of that project that are the same for every
project, with a TextMate grammar for ASN.1, which Shiki lacks
bin/asn1-docs writes the pages
bin/asn1-docs-mcp serves the graph
The Markdown is the product, and Astro is replaceable. The documentation generator of Peppol is a Docker image from 2019, and nobody released its source. Their site cannot be rebuilt without that image. A readable intermediate format prevents this result.
MIT.