# Hen

Run API requests as files, from the command line or through MCP.

```text
name = Test Collection File
description = A collection of mock requests for testing this syntax.

$ API_KEY = $(./get_secret.sh)
$ USERNAME = $(echo $USER)
$ API_ORIGIN = https://lorem-api.com/api

$ ROLE = [admin, user, guest]

---

# Load other requests.
<< .fragment.hen

---

Some descriptive title for the prompt.

POST {{ API_ORIGIN }}/echo

* Authorization = {{ API_KEY }}

? query_param_1 = value

~~~ application/json
{
  "username" : "{{ USERNAME }}",
  "password" : "[[ password ]]",
  "role" : "{{ ROLE }}"
}
~~~

^ & status == 200

! sh ./callback.sh
```

## Installation

```bash
cargo install hen
```

This installs the interactive `hen` CLI and the `hen-mcp` stdio server.

## Usage

```text
Usage: hen [OPTIONS] [PATH] [SELECTOR]
       hen verify <PATH>

Arguments:
  [PATH]
  [SELECTOR]

Options:
  --export
  --benchmark <BENCHMARK>
  --input <KEY=VALUE>
  --output <OUTPUT>
  --parallel
  --max-concurrency <MAX_CONCURRENCY>
  --continue-on-error
  -v, --verbose
  -h, --help                   Print help
  -V, --version                Print version
```

### Execute a Request

To execute a request, use the `hen` command on a file containing a request:

```bash
hen /path/to/collection_directory/collection_file.hen
```

This will prompt you to select a request from the file to execute.

For automation and CI, the CLI also supports a dedicated `run` form:

```bash
hen run /path/to/collection.hen all --non-interactive
```

With `--non-interactive`, Hen fails immediately instead of prompting. That includes:

- directory paths that would require picking between multiple `.hen` files
- collections with multiple requests when no selector is provided

Prompt inputs are always non-interactive in the CLI run path. Any unresolved `[[ prompt ]]` placeholders must be supplied up front with `--input`, and Hen rejects the run before making any HTTP requests.

### Machine-Readable Output

For CI and scripts, `hen run` and `hen verify` also accept `--output text|json|ndjson|junit`:

```bash
hen run /path/to/collection.hen all --non-interactive --output json
hen run /path/to/collection.hen 0 --output junit
hen verify /path/to/collection.hen --output ndjson
```

Non-text outputs use Hen's non-interactive execution path so stdout stays machine-readable. `json` emits one structured document, `ndjson` emits one JSON object per line, and `junit` emits a JUnit testsuite XML report for CI UIs. JSON and NDJSON reports include per-request assertion arrays, while JUnit emits assertion-level testcases when assertions are present and falls back to request-level cases for non-assertion execution failures.

### Verify a Collection

To parse and validate a collection without executing shell substitutions or making network requests, use:

```bash
hen verify /path/to/collection.hen
```

The verify command preprocesses fragments, validates request syntax, and reports required `[[ prompt ]]` inputs up front.

### Specifying a Request

You can specify the nth request directly by providing an index as the second argument:

```bash
hen /path/to/collection_directory/collection_file.hen 0
```

This will bypass the request selection prompt and execute the first request in the file.

Conversely, all requests can be executed with the `all` selector:

```bash
hen /path/to/collection_directory/collection_file.hen all
```

### Selecting a Collection

Alternatively, you can specify a directory of collections.  This will prompt you to select a collection file and then a request from that file.

```bash
hen /path/to/collection_directory
```

> If the directory contains only one collection file, that file will be selected automatically, bypassing the prompt.
> Dotfiles (files starting with `.`) are ignored by the prompt.

### Parallel Execution

Hen executes requests sequentially by default. Pass `--parallel` to run independent requests concurrently according to the dependency graph built from `> requires:` declarations.

```bash
hen --parallel ./collection.hen all
```

- `--max-concurrency <N>` throttles the number of simultaneous requests. Omit or set to `0` to allow as many parallel requests as the graph permits.
- `--continue-on-error` keeps running any branches that are unaffected by a failure. Without this flag Hen stops after the first failed request.
- Outputs are still printed in plan order. Each request buffers its stdout until completion to keep logs deterministic.

## MCP Server

Hen also ships with `hen-mcp`, a stdio Model Context Protocol server for editor and agent integrations. The MCP server is non-interactive by design and does not replace the CLI:

- Use `hen` for the existing terminal workflow with prompts and interactive selection.
- Use `hen-mcp` when an MCP client needs structured, non-interactive access to collections.
- For collections that contain `[[ prompt ]]` placeholders, MCP callers must provide those values explicitly via tool inputs; `hen-mcp` will not prompt interactively.

### Running the MCP Server

If `hen-mcp` is on your `PATH`, the server can be started directly:

```bash
hen-mcp
```

From a checkout of this repository, you can run it with Cargo:

```bash
cargo run --bin hen-mcp
```

For MCP clients, prefer a compiled binary over `cargo run` in normal use:

```bash
cargo build --release
./target/release/hen-mcp
```

### Example VS Code MCP Configuration

Add a server entry to `.vscode/mcp.json`:

```json
{
  "servers": {
    "hen": {
      "type": "stdio",
      "command": "/absolute/path/to/hen-mcp"
    }
  },
  "inputs": []
}
```

If `hen-mcp` is installed globally, `command` can simply be `hen-mcp`.

### Example Claude Code MCP Configuration

Add a server entry to `.mcp.json` in your project root:

```json
{
  "mcpServers": {
    "hen": {
      "type": "stdio",
      "command": "/absolute/path/to/hen-mcp"
    }
  }
}
```

If `hen-mcp` is installed globally, `command` can simply be `hen-mcp`.

### Exposed MCP Tools

#### `run_hen`

Runs a collection or request non-interactively.

Common arguments:

- `path`: path to a `.hen` file, or a directory containing exactly one `.hen` file.
- `selector`: request index or `all`. Required when a collection has multiple requests.
- `inputs`: object containing values for `[[ prompt ]]` placeholders.
- `parallel`: run independent requests concurrently.
- `maxConcurrency`: optional concurrency cap.
- `continueOnError`: continue unaffected branches after a failure.
- `includeBody`: include response bodies in the returned records.
- `maxBodyChars`: truncate response bodies to a fixed size.

Example tool call payload:

```json
{
  "path": "./examples/map_request.hen",
  "selector": "all",
  "inputs": {
    "password": "secret"
  },
  "parallel": true,
  "maxConcurrency": 4,
  "continueOnError": true
}
```

`run_hen` uses the same execution engine as the CLI. That means it can execute shell-based variable assignments, callbacks, and HTTP requests. It remains non-interactive, so any required prompt placeholders must be supplied in `inputs`.

#### `verify_hen_syntax`

Parses and validates Hen source without executing shell substitutions or network requests.

Arguments:

- `path`: validate a file on disk.
- `source`: validate inline Hen text instead of a file.
- `workingDirectory`: base directory for resolving `<<` imports when `source` is provided.

Provide either `path` or `source`, but not both.

#### `get_hen_authoring_guide`

Returns the built-in authoring docs from this repository.

Optional argument:

- `topic`: `syntax` or `usage`

### Exposed MCP Resources

- `hen://authoring-guide`: the syntax reference.
- `hen://readme`: the README content.

### Non-Interactive MCP Behavior

- Any unresolved `[[ prompt ]]` placeholder causes an error. Supply prompt values through the `inputs` object passed to `run_hen`.
- Directory paths are only accepted when exactly one `.hen` file exists in the directory.
- When a collection contains multiple requests, MCP callers must provide a `selector` or use `all`.
- The existing `hen` CLI behavior is unchanged and remains interactive.

## Defining an API Request

An API request is defined in a text file with the `.hen` extension.

At a minimum, a request must have a method and a URL. The method is one of `GET`, `POST`, `PUT`, `PATCH`, `DELETE`, `HEAD`, or `OPTIONS`. The URL is the endpoint of the request.

```text

[description]

METHOD url

[* header_key = header_value]

[? query_key = query_value]

[~ form_key = form_value]

[~~~ [content_type]
body
~~~]

[> requires: Request Name]

[& response_capture]

[ [^ assertion_condition] || "failure message"]

[! callback]

```

### Headers

Headers are key-value pairs that are sent with the request. Headers are specified with the `*` character.  For example,

```text
* Authorization = abc123
* Content-Type = application/json
```

If a request and the collection preamble both define the same header with the same name, the request-level value overrides the global value.
Use consistent header casing when overriding a global header. Header names that differ only by case are currently merged as separate entries before the request is built, so a case-only override is not reliable.

### Query Parameters

Query parameters are key-value pairs that are appended to the URL. Query parameters are specified with the `?` character. For example,

```text
? page = 2
? limit = 10
```

### Multipart Form Data

Multipart form data is used to send files or text fields with a request. Multipart form data is specified with the `~` character. For example,

```text
$ file_1 = $(cat ./test_file.txt)
---

POST https://lorem-api.com/api/echo

# form data can be used to send text data
~ form_text_1 = lorem ipsum.
~ form_text_2 = {{ file_1 }}

# form data can also be used to send files
~ file_1 = @./test_file.txt
```

### Request Body

The request body is the data sent with the request. The body is a multiline block specified with the `~~~` characters. The body can optionally be followed by a content type. For example,

```text
~~~ application/json
{
  "key": "value"
}
~~~
```

### User Prompts in Requests

Prompt placeholders can be declared with the `[[ variable_name ]]` syntax. A prompt may be used as a value for a query, header, form, or in the request body or URL. For example,

```text
GET https://example.com/todos/[[ todo_id ]]

? page = [[ page ]]
* Origin = [[ origin ]]
~ file = @[[ file_path ]]
```

CLI runs do not prompt for these values. Supply them explicitly with `--input` before execution starts.

#### Supplying Prompt Values from the CLI

Use the `--input` flag to supply prompt values up front. Each `--input` expects `key=value` and may be repeated. Keys correspond to the placeholder name inside `[[ ... ]]`.

```bash
hen --input foo=bar --input password=secret ./collection.hen
```

When a matching prompt is encountered, the provided value is used automatically. Any remaining prompts cause the run to fail before execution starts.

### Response Captures

Response captures extract data from the response and store it in a variable for use in subsequent requests, callbacks, and assertions. The response is denoted with the `&` character, and can expose the status code, headers, and body of the response.

```text
& status -> $STATUS
& header.content-type -> $CONTENT_TYPE
& body -> $BODY
```

For JSON bodies, you can extract specific fields using structured path access.

```text
& body.token -> $TOKEN
& body.data.user.id -> $USER_ID
& body.items[0].name -> $FIRST_ITEM_NAME
& body.[0].id -> $FIRST_POST_ID
```

Use `body.field` when the response body root is an object. Use `body.items[0].name` for arrays nested under object keys. Use `body.[0].field` when the response body root is an array.

Capture paths support field and numeric index access. They do not support jq or JSONPath-style filters or expressions.

For a runnable example covering direct, nested, defaulted, root-array, and dependency-based captures, see [examples/json_response_captures.hen](/Users/bengoodman/projects/hen/examples/json_response_captures.hen).

Response captures update the callback execution context, making the captured variables available to any callbacks defined later in the same request, or in subsequent requests in the same collection.

```text
Get JWT

POST {{ API_URL }}/jwt

~~~ application/json
{
  "username": "foo",
  "password": "[[ password ]]"
}
~~~

& body.token -> $TOKEN

! echo $TOKEN | cut -d '.' -f2 | base64 --decode | jq .
```

#### Referencing Dependency Responses

When a request declares dependencies with `> requires:`, it can also read from those responses using the capture syntax. Prefix the capture path with `&[Request Name]` to target the upstream response:

```text
> requires: Get JWT

GET https://api.example.com/profile

&[Get JWT].body.token -> $TOKEN
&[Get JWT].status -> $LOGIN_STATUS
```

The same accessors (`body`, `header`, `status`) are supported. If a capture references a request that has not been declared as a dependency, Hen raises an error during parsing.

### Assertions

Assertions provide lightweight response validation without leaving the request file. Each assertion line starts with `^` followed by a boolean expression. Assertions run after captures resolve and before callbacks execute. If any assertion fails, request execution stops and the failure message is surfaced.

```text
POST https://api.example.com/login

& body.token -> $TOKEN
^ $TOKEN ~= /[A-Za-z0-9_-]+/ || "Token missing or malformed"
^ & status == 200
```

Available operators are `==`, `!=`, `<`, `<=`, `>`, `>=`, and `~=`. The `~=` operator acts as a substring match for quoted strings and as a regular expression when the right-hand side is wrapped in `/` delimiters.

Assertions can reference the following values:

- Response metadata: `STATUS`, `STATUS_TEXT`, `RESPONSE`, and `DESCRIPTION`.
- Any response captures defined earlier in the request (e.g., `$TOKEN`).
- Numeric and string literals (quoted or unquoted when numeric).
- Response data directly via the capture syntax used elsewhere. For example: `^ & body.token == 'abc'` or `^ &[Login].header.authorization ~= /Bearer/`.

Optionally append `|| "custom message"` to override the default failure text. Messages may interpolate collection or request variables via `{{ variable }}` before evaluation.

### Callbacks

Callbacks are shell commands that are executed after a request is made. Callbacks are defined in the request definition with the `!` character. For example,

```text
GET https://lorem-api.com/api/user

# inline shell command
! echo "Request completed."

# a shell script
! sh ./post_request.sh
```

If a request has multiple callbacks, they are executed in the order they are defined, top to bottom.

```text
GET https://lorem-api.com/api/user

# This is executed first
! echo '1'

# This is executed second
! echo '2'
```

#### Callback Execution Context

Callbacks are executed with response data passed as environment variables.  The following environment variables are available to callbacks:

- `STATUS`: The HTTP status code of the response.
- `STATUS_TEXT`: The canonical reason phrase for the HTTP status code, when available.
- `RESPONSE`: The response body of the request.
- `DESCRIPTION`: The description of the request.

For example, the following callback will assert that the status code of the response is 200.

```bash
#!/bin/bash
# ./post_request.sh

if [ "$STATUS" -eq "200" ]; then
    echo "✅ [$DESCRIPTION] Received status 200"
    echo $RESPONSE
else
    echo "❌ [$DESCRIPTION] Expected status 200 but got $STATUS"
    echo $RESPONSE
fi
```

```text
Echo body w. callback

POST https://lorem-api.com/api/echo

! sh ./post_request.sh
```

## Defining an API Collection

A file containing multiple requests is called a collection.  Collections can be used to group related requests together.

Collections can start with a preamble that contains metadata about the collection. The preamble is separated from the requests by a line containing three dashes `---`.  The same line is also used to separate requests from each other.

```text
name = Optional Collection Name
description = Optional Collection Description

[VARIABLES]

[GLOBAL HEADERS]

[GLOBAL QUERIES]

[GLOBAL CALLBACKS]

---

[request 1]

---

[request 2]

---

etc.

```

### Global Headers, Queries and Callbacks

Any headers, queries or callbacks defined in the collection preamble become global and are included in all requests in the collection.

Request-level headers are merged on top of global headers. When overriding a global header, use the exact same header name and casing in the request block.

In the example below, the `Authorization` header and `page` query is included in all requests in the collection.  When each request is executed and a response received, the callback `echo "Request completed."` is executed.

```text
* Authorization = foo
? page = 2
! echo "Request completed."
---
GET https://api.example.com/users
---
GET https://api.example.com/posts
```

> Global callbacks are executed before request-specific callbacks.

### User Prompts in Collections

Prompt placeholders can also be used in a collection preamble. CLI runs require those values to be supplied with `--input` before the collection is executed.

```text
$ foo = [[ bar ]]
---
POST https://lorem-api.com/api/echo

~~~ application/json
{ "foo" : "{{ foo }}" }
~~~
```

Prompt values can be supplied from the CLI using the `--input` flag, as described in the [User Prompts in Requests](#user-prompts-in-requests) section.

### Global Variables

Global variables are key/value pairs defined in the preamble of a collection file with the `$` character. For example,

<!-- markdownlint-disable MD014 -->
```text
$ api_origin = https://example.com
$ api_key = abc123
$ username = alice
```

Variables can be used in the request definition by enclosing the variable name in double curly braces. For example,

```text
GET {{ api_origin }}/todos/2

* Authorization = {{ api_key }}

? username = {{ username }}
```

Variables can also be set dynamically by running a shell command. For example,

```text
$ api_key = $(./get_secret.sh)
$ username = $(echo $USER)
```

Or by setting the variable interactively:

```text
$ api_key = [[ api_key ]]
```

<!-- markdownlint-enable MD014 -->

#### Request-Level Variables

Variables can also be declared inside an individual request. These request-level variables use the same `$ name = value` syntax and may appear before the HTTP method line or intermixed with other request statements. When both a global variable and a request variable share the same key, the request variable takes precedence for that request only.

```text
$ api_origin = https://lorem-api.com/api
---
Fetch banner image

$ banner_text = Promo%20Time!
$ banner_fill = 444444

GET {{ api_origin }}/image?text={{ banner_text }}&fill={{ banner_fill }}
```

Request variables fully support shell substitutions (`$(...)`) and interactive prompts (`[[ ... ]]`), and are resolved using the same working directory as the collection. This makes it easy to specialize a base request without modifying the collection preamble or other requests.

#### Array Variables and Map Requests

Variables can also be defined as simple arrays by using square brackets:

<!-- markdownlint-disable MD014 -->
```text
$ AUTH_USERNAME = [foo, bar]
```
<!-- markdownlint-enable MD014 -->

When a request references one or more array variables, Hen clones that request and executes it once per value (or per Cartesian combination when multiple arrays are used). Each clone receives the corresponding value injected everywhere the array variable appears—URLs, bodies, assertions, captures, callbacks, and so on. Every generated request is labeled with a bracketed suffix (for example, `Echo a request [AUTH_USERNAME=foo]`) and is logged, asserted, and benchmarked independently.

Key details:

- Arrays must contain scalar values (no whitespace or nested arrays) and can be declared globally or inside a request. You can still compute them dynamically with `$(...)` commands or prompt interpolation.
- A request may reference at most two distinct array variables and may expand into up to 128 total combinations. Hen raises a parse error if either limit is exceeded.
- Map requests abort after the first failing iteration. Remaining iterations are skipped and reported as a `MapAborted` failure, even when `--continue-on-error` is set, ensuring side-effectful callbacks do not repeat.
- Environment exports produced by each iteration are suffixed with the same bracket label (for example, `TOKEN[AUTH_USERNAME=foo]`) to keep values isolated.
- Requests cannot depend on a mapped request; declare dependencies on a non-mapped helper if shared setup is required.

This behavior makes it easy to fan a single request across a table of inputs without duplicating boilerplate in the collection file.

### Request Dependencies

Requests can depend on other requests in the same collection. Declare dependencies with the `>` directive placed before the HTTP method (or alongside other request-level declarations):

```text
> requires: Get JWT

GET https://api.example.com/profile
```

Hen constructs a directed acyclic graph (DAG) from these declarations when it loads the collection. Each dependency must reference another request's description exactly, and any cycles cause an error. When you execute a request, Hen automatically runs its prerequisites first—even if you selected only a single request from the CLI.

All variables captured or exported by a dependency become part of the dependent request's context. That means placeholders like `{{ TOKEN }}` resolve using values captured upstream, and captures can reference dependency responses via `&[Dependency].body...` as described earlier. This makes multi-step workflows (login → reuse token → perform action) straightforward to model in one collection file.

## Additional Syntax

### Comments

Comments are lines that are ignored by the parser. Comments start with the `#` character. For example,

```text
# This is a comment

GET https://example.com/todos/2
```

### Fragments

Fragments are reusable blocks of text that can be included in multiple requests. Fragments are defined in a separate file and included in a request with the `<<` character. For example,

```text
# .fragment.hen

* Authorization = abc123
```

```text
GET https://example.com/todos/2
<< .fragment.hen
```

Fragment paths can be absolute or relative to the collection file.

Fragments can contain multiple requests, headers, query parameters, and request bodies. Fragments can also contain variables and other fragments.

### Conditional Guards

Prefix an assertion or fragment with a boolean guard to evaluate it only when the predicate resolves to `true`:

```text
[ROLE == admin] ^ $STATUS == 200

[ROLE == admin] << admin.assertions.hen
```

- Guards are parsed with the same expression grammar as assertions and can reference variables, captures, or response metadata (`STATUS`, `RESPONSE`, etc.).
- Lowercase bare words are treated as string literals, so `ROLE == admin` compares against the literal value `"admin"`.
- Guarded assertions are skipped (and reported as such) when the predicate is false.
- Guard prefixes on fragment includes apply to every statement in the fragment, so guarded fragments are only executed when the predicate passes.

## Additional Features

### Export Requests

Requests can be exported as curl commands.  This is useful for debugging or sharing requests with others.

```text
 $ API_URL = https://lorem-api.com/api

---

POST {{ API_URL }}/echo

~~~ application/json
{ "foo" : "bar" }
~~~
```

```bash
curl -X POST 'https://lorem-api.com/api/echo' -H 'Content-Type: application/json' -d ' { "foo" : "bar" }'
```

Exporting happens once all variables and prompts have been resolved and the request is ready to be executed. Callbacks are ignored during export.

### Benchmarking

Requests can be benchmarked by specifying the `--benchmark` flag with the number of iterations to run.  This will run the request the specified number of times and output the average time taken to complete the request.

```bash
hen /path/to/collection_file.hen --benchmark 10
```

```text
Benchmarking request: Echo form data
[##################################################] 100.00%

Mean Duration: 399.95937ms
Variance (%): 0.6831308901940525
```

Notes:

- Callbacks are ignored when benchmarking.
- User prompts will still be executed in benchmarked requests, and so should be avoided, or used in the preamble only.

## Examples

### Basic Request

```text
GET https://lorem-api.com/api/user/foo
```

### Request with Headers, Query Parameters, and Form Data

```text
POST https://lorem-api.com/api/echo

# Header
* foo = abc123

# Query
? bar = abc123

# Form Data
~ baz = abc123
```

### Request with Body

```text
POST https://lorem-api.com/api/jwt

~~~ application/json
{
  "username": "bar",
  "password": "qux"
}
~~~
```

### Request with Callback

```bash
#!/bin/bash

if [ "$STATUS" -eq "200" ]; then
    echo "✅ [$DESCRIPTION] Received status 200"
else
    echo "❌ [$DESCRIPTION] Expected status 200 but got $STATUS"
fi
```

```text
GET https://lorem-api.com/api/user

! sh callback.sh
! echo $JWT_TOKEN | cut -d '.' -f2 | base64 --decode | jq . -> $JWT_PAYLOAD
```

When using `command -> $VARIABLE`, surround the arrow with whitespace. The command still runs in the same environment, but its stdout is suppressed; only the sanitized output is saved into the named variable for later requests and callbacks.

### Request with Assertions

```text
$ IMAGE_TEXT = https%3A%2F%2Florem-api.com
$ FILL_COLOR = CECECE

GET {{ API_URL }}/image?format=png&fill={{FILL_COLOR}}&text={{IMAGE_TEXT}}

^ STATUS == 200
^ & header.content-type  == "image/png"

```

### Request with Dependencies

```text
$ API_URL = https://lorem-api.com/api

---

Get JWT

POST {{ API_URL }}/jwt

~~~ application/json
{
  "username": "foo",
  "password": "[[ password ]]"
}
~~~

& body.token -> $JWT_TOKEN

^ STATUS == 200
^ $JWT_TOKEN ~= /[A-Za-z0-9_.-]+/

---

Echo Response

> requires: Get JWT

POST {{ API_URL }}/echo

~~~ application/json
{ "foo" : "{{ JWT_TOKEN }}" }
~~~

^ & body.foo == $JWT_TOKEN
```

### Mapped Request

```text
$ REST_ORIGIN = https://lorem-api.com/api
$ USERNAME = [foo, bar]
$ ROLE = [admin, user, guest]

---

Echo a request

POST {{ REST_ORIGIN }}/echo

~~~ application/json
{
  "username" : "{{ USERNAME }}",
  "role" : "{{ ROLE }}"
}
~~~

^ & status == 200
^ & body.username ~= /bar|foo/
^ & body.role ~= /admin|user|guest/
```
