One API definition, many surfaces. Hydra projects a single, explicit
api/operations.yaml onto three committed surfaces — CLI (clap), HTTP
(axum), and MCP (tool schemas + stdio runtime) — so the same Rust operation
implementation powers every interface without drift.
Extracted from iris's iris-codegen,
generalized for reuse across TechGodHQ Rust projects. Born from a spike that
evaluated (and rejected) macro-based inference layers; see iris's
docs/spikes/server-less.md for the rationale. Hydra's rule: no name-based
inference — everything is declared.
api/operations.yaml (single source of truth)
|
cargo run -p hydra-codegen -- write
|
+---------------+----------------+
| | |
generated/cli.rs generated/http.rs generated/mcp.json
| | |
clap structs axum routes tool schemas
| | |
+---------------+----------------+
|
your operation dispatch (one function)
hydra-core— the API definition model (operations, parameters, locations, surface allowlists) and validation. No generation, no I/O.hydra-codegen— the generator +hydraCLI (write/check). Per-project knobs live inhydra.yaml.hydra-mcp-stdio— a minimal reusable MCP stdio server (JSON-RPC 2.0, newline-delimited) you hand your generated tool schemas and one dispatch closure.examples/notes— a complete three-surface project. Copy it as a template.
- Describe operations in
api/operations.yaml:
operations:
- name: get_note
description: Get a single note by ID.
method: GET
path: /notes/{note_id}
read: true
output_type: Note
parameters:
- name: note_id
description: Note ID to fetch.
type: string
required: true
location: path- Add a
hydra.yamlpointing generated handlers at your dispatch and state:
http_dispatch_fn: "crate::execute_operation_http"
http_state_type: "crate::AppState"
# Only needed if the definition has raw_request operations:
# http_raw_dispatch_fn: "crate::execute_operation_raw_http"- Generate and commit:
cargo run -p hydra-codegen -- write # writes generated/{cli.rs,http.rs,mcp.json}
cargo run -p hydra-codegen -- check # CI guard: fails if artifacts are staleinclude!the generated files, implement one dispatch function, and wire your binaries. Seeexamples/notes/src/lib.rs.
Hydra v0.2.0 can project a declared json body parameter to HTTP, MCP, and
CLI. Declare the JSON Schema explicitly; Hydra embeds it in the MCP input
schema and generates the HTTP route from the same operation. For a batch that
needs a shell-friendly CLI representation, declare the representation rather
than inferring one:
- name: ingest_batch
description: Apply an ordered, replayable source batch.
method: POST
path: /ingest/batches
read: false
output_type: IngestReceipt
parameters:
- name: replay_key
description: Stable idempotency key.
type: string
required: true
location: body
- name: events
description: Ordered source events.
type: json
required: true
location: body
schema:
type: array
minItems: 1
items: { type: object }
cli:
flag: event
multiple: trueHTTP and MCP callers pass events as the declared JSON array. The generated
CLI accepts repeated --event '<json object>' flags; the consumer's single
dispatch function parses that explicit CLI representation before typed
validation and persistence. Hydra does not own source-specific event models,
batch hashing, idempotency, or transactions. examples/notes contains a
tested ingest_batch reference operation and is the pattern Iris should use.
examples/security-scan is a deliberately small, fixture-backed consumer
reference. Its explicit run_security_scan operation projects to CLI, HTTP,
and MCP, while the consumer—not Hydra—owns a typed SecurityScanner trait and
the single dispatch function. The checked-in fixture only accepts
fixture:demo-repo and the optional baseline profile. It never treats input
as a command, filesystem path, or URL.
The fixture needs no configuration. A future live adapter must read only
DEEPSEC_ENDPOINT (an absolute HTTPS URL) and DEEPSEC_TOKEN (a non-empty
credential) from its environment. Neither belongs in source, generated output,
logs, requests, or public errors. Consumer adapters must map private failures
to the fixed public invalid_request, scanner_unavailable, or scan_failed
error codes without serializing vendor details, credentials, headers, endpoints,
or raw scanner output.
Operations that must see the exact wire representation — signature-verified
webhooks, for example — opt in with raw_request: true. The generated HTTP
handler receives the raw body bytes and a header map instead of typed
extraction, and dispatches to http_raw_dispatch_fn:
- name: receive_webhook
description: Receive a signed webhook payload.
method: POST
path: /hooks/github
read: false
output_type: Value
parameters: []
surfaces: [http]
raw_request: trueRules: http must be the only listed surface, the operation is unary (no
SSE), and body-location parameters are rejected — the raw bytes replace
JSON body extraction. The header map lowercases names, drops non-UTF-8
values, and collapses repeated headers to the last value. Definitions
without raw operations generate byte-identical output to before the flag
existed.
- No inference. Method, path, parameter locations, and surface allowlists are declared. The generated router, schemas, and docs cannot disagree because none of them guess.
- Validation is the product.
hydra-corerejects path placeholders without parameters, duplicate names, read/POST mismatches, empty or duplicate surface lists, and reserved identifiers at generation time — not at runtime. - Generated code is committed and deterministic;
checkgates CI. - One dispatch function per project routes every surface to the same operation implementation. Business logic never duplicates per surface.
- Selective projection via
surfaces: [http, mcp]allowlists, with CLI command renames (cli_command:) when the public name should differ.
MIT.