endpoint-gen
Schema-first code generator for WebSocket RPC services. You describe endpoints once in
declarative RON; endpoint-gen produces the Rust models, the human-facing docs, the MCP
tool schemas, and — optionally — OpenAPI 3.1 and AsyncAPI 3.0 documents.
The generated code targets endpoint-libs,
which is the runtime that serves it.
Shipping an MCP server is close to free. Define an endpoint in RON, and its MCP tool
definition — name, description, inputSchema, outputSchema — is generated with
everything else. endpoint-libs exposes the lot over JSON-RPC 2.0 with one enable_mcp()
call, on the same socket the RPC protocol already uses, with tools/list filtered by the
caller's roles. No tool definitions to hand-write, no JSON Schema to maintain by hand, no
separate MCP server to deploy and keep in sync. docs/<service>_mcp_tools.json shows
exactly what agents will see, so a schema change is a reviewable diff.
The codegen arrow points from the schema to the code, which is the whole point: most
Rust OpenAPI crates (utoipa, aide, poem-openapi, dropshot) derive a spec from
hand-written handlers and so delete no boilerplate. See
endpoint-libs' comparison doc
for when you should use one of those instead.
Installation
Usage
| Flag | Effect |
|---|---|
--config-dir <path> |
Directory holding the .ron files and version.toml. Defaults to the current directory. |
--output-dir <path> |
Project root; generated/ is written beneath it. Defaults to the current directory. |
--check |
Verify instead of write — see below. |
--openapi |
Also emit docs/openapi.json (OpenAPI 3.1). Off by default. |
--asyncapi |
Also emit docs/asyncapi.json (AsyncAPI 3.0). Off by default. |
--public-only |
Restrict the specification documents to frontend_facing endpoints. |
--allow-empty-descriptions |
Permit missing endpoint/variant/error descriptions. Legacy escape hatch. |
Generated artifacts
| Path | Always? | What it is |
|---|---|---|
generated/model.rs |
yes | Rust types, method codes, handler scaffolding. Gitignored in our repos. |
docs/services.json |
yes | Machine-readable endpoint description in our own format — see below. |
docs/<service>_mcp_tools.json |
yes | Exactly what a server reports via MCP tools/list. |
docs/README.md |
yes | Human-facing reference. |
docs/error_codes/error_codes.md |
yes | The error-code catalog. |
docs/asyncapi.json |
--asyncapi |
AsyncAPI 3.0 — the protocol, in a standard format. |
docs/openapi.json |
--openapi |
OpenAPI 3.1 — a projection for HTTP tooling. |
docs/openapi-README.md |
with either | Explains whichever specification documents you enabled. |
services.json and AsyncAPI are parallel, not sequential
The specification documents were added in 2.1 and deprecate nothing. services.json
remains the primary machine-readable artifact and the one to build internal tooling
against:
It is always written, it is a format we define and control, and it changes when we decide it changes. For anything inside our own stack that is materially less friction than conforming to a specification whose vocabulary only approximately fits a WebSocket RPC protocol.
Reach for --asyncapi when a consumer outside your control needs to read the protocol
and a bespoke format would be the obstacle. Both describe the same endpoints; pick by
audience.
One behavioural difference worth knowing: services.json contains only
frontend_facing endpoints, always. The specification documents contain everything
unless you pass --public-only.
--check
Regenerates everything into a temporary directory and diffs it against the committed
docs/ tree, writing nothing and exiting non-zero on drift.
A committed artifact is only trustworthy if something proves it still matches its source;
this is that proof, and it belongs in CI. Only docs/ is compared — generated/ is
gitignored in every consumer repo, so it is not a committed artifact and cannot
meaningfully drift.
Pass the same spec flags you generate with, or the check will report the documents you chose not to emit as missing.
Specification documents
--openapi and --asyncapi are opt-in: upgrading the generator will not start adding
committed artifacts to your repository.
The OpenAPI document is a projection for tooling, not a servable API. This transport has no URLs, so paths are synthesized as
/{serviceName}/{endpoint_snake_name}. The AsyncAPI document is the accurate one of the two. Both carry that warning ininfo.description, anddocs/openapi-README.mdis generated alongside them.
Config Directory
The config directory must contain a version.toml and any number of .ron files. All .ron files are discovered recursively.
version.toml
Declares the required versions of the binary and endpoint-libs:
[]
= ">=0.5.0"
[]
= "1.3.4"
Generation will fail if the installed binary or the caller's endpoint-libs version does not satisfy these constraints.
RON File Format
Each .ron file wraps a single Definition:
#![enable(unwrap_newtypes)]
#![enable(unwrap_variant_newtypes)]
Config(
definition: <DefinitionVariant>( ... )
)
Endpoints
Define a service's WebSocket endpoints with EndpointSchemaList. Each file specifies the service name, a unique numeric service ID, and a list of endpoint schemas.
Config(
definition: EndpointSchemaList (
"my_service",
1,
[
EndpointSchema(
name: "UserGetBalance",
code: 10100,
parameters: [
Field(name: "user_id", ty: Optional(Int)),
],
returns: [
Field(name: "data", ty: Struct(
name: "Balance",
fields: [
Field(name: "amount", ty: Numeric),
],
)),
],
stream_response: None,
description: "Returns the current balance for the user.",
json_schema: (),
roles: ["UserRole::Superadmin"],
),
]
)
)
Endpoints with push/subscription behaviour set stream_response to the type streamed back to the client:
stream_response: Some(DataTable(
name: "LivePosition",
fields: [
Field(name: "id", ty: BigInt),
Field(name: "price", ty: Numeric),
],
)),
Endpoint errors can declare the public handler errors generated for an endpoint. Error codes use the same quoted enum-path style as roles and must reference ErrorCode:
errors: [
EndpointErrorSchema(
name: "WrongPassword",
code: "ErrorCode::WrongPassword",
message: "Wrong password",
fields: [],
),
EndpointErrorSchema(
name: "PasswordTooShort",
code: "ErrorCode::PasswordTooShort",
message: "Password too short",
fields: [
Field(name: "min_length", ty: Int32),
Field(name: "actual_length", ty: Int32),
],
),
],
Available field types
| Type | Description |
|---|---|
UInt32 |
Unsigned 32-bit integer |
Int32 |
Signed 32-bit integer |
Int64 |
Signed 64-bit integer |
Float64 |
64-bit float |
Boolean |
Boolean |
String |
UTF-8 string |
Bytea |
Byte array |
UUID |
UUID |
IpAddr |
IP address |
TimeStampMs |
Timestamp in milliseconds |
Object |
Arbitrary JSON object |
Unit |
No value |
Optional(T) |
Nullable field |
Vec(T) |
List of T |
Struct(name, fields) |
Inline named struct |
StructRef(name) |
Reference to a named struct |
StructTable(struct_ref) |
List of a named struct (tabular data) |
Enum(name, variants) |
Inline enum definition |
EnumRef(name, prefixed_name) |
Reference to a named enum |
BlockchainDecimal |
Blockchain decimal value |
BlockchainAddress |
Blockchain address |
BlockchainTransactionHash |
Blockchain transaction hash |
Enums
Define enums with EnumList:
Config(
definition: EnumList (
[
Enum(
name: "UserRole",
variants: [
EnumVariant(name: "Superadmin", value: 1, comment: "Full access."),
EnumVariant(name: "Support", value: 2, comment: "Read-only access."),
],
),
]
)
)
Structs
Shared struct types can be declared with Struct or StructList and will be emitted as top-level types in the generated model.
Error Codes
Built-in endpoint-libs error codes such as ErrorCode::BadRequest and ErrorCode::Unauthorized are always available. Project-specific codes are declared with ErrorCodeList, commonly in config/errors.ron:
Config(
definition: ErrorCodeList(
codes: [
ErrorCodeSchema(
name: "WrongPassword",
code: 200001,
description: "The submitted password does not match the user account.",
),
],
),
)
The generated model emits these as EnumErrorCode, and generation fails if an endpoint references an unknown ErrorCode::Variant.
JSON Schema Generation
Enable JSON schema generation for enums and structs by setting the json_schema_gen configuration option on the parent element:
Config(
definition: EnumList(
config: (json_schema_gen: true),
enum_elements: [
EnumElement(
inner: Enum(
name: "MyEnum",
variants: [
EnumVariant(name: "Variant1", value: 0, description: "..."),
],
),
),
],
),
)
This applies the configuration to all child elements. When enabled, the generated code will include schemars::JsonSchema derives and imports. Your project must include the schemars crate as a dependency to use this feature.
Version compatibility
endpoint-gen and endpoint-libs are not versioned together and their minor versions
need not match — as of this writing endpoint-gen 1.13 works with endpoint-libs 2.1 and
honey_id-types 2.0.
What is enforced, at generation time:
endpoint-genis built against a specificendpoint-libsrequirement (currently^2.1) and compares it against[libs] versionin yourconfig/version.toml.- Your
config/version.tomldeclares a requirement on the binary via[binary] version.
A mismatch on either fails generation with an explicit message rather than emitting subtly
wrong code. Keep [libs] version in step with what Cargo.lock actually resolves — a
stale declaration is the usual cause of a confusing refusal to run.
# config/version.toml
[]
= "1.13.0" # a caret range: any 1.x >= 1.13.0
[]
= "2.1.0" # the concrete endpoint-libs version this repo builds against
The release order across the whole chain — endpoint-libs first, then honey_id-types and endpoint-gen, then the backends — is documented in endpoint-libs' release-order runbook.
Releasing
Releases are managed with cargo-release and git-cliff. Both must be installed:
To cut a release:
||
The script will:
- Run
cargo release --execute <level>— bumps the version inCargo.toml, updates the deps.rs badge in this README, regeneratesCHANGELOG.md, and commits everything aschore(release): vX.Y.Z. - Open your
$EDITORwith the auto-generated tag notes (fromgit-cliff) for review. - Create an annotated tag using the edited notes as the tag body (shown as GitHub Release notes).
- Push the commit and tag.
- Prompt whether to publish to crates.io.
To preview what cargo-release would do without making changes: