Skip to content

Repository files navigation

openrun

CI Go Reference Go Report Card Release License

OpenAPI 3.x MCP stdio Go 1.22+

openrun is a spec-driven Go library and CLI for running HTTP requests directly from an OpenAPI 3.x document.

Validate a spec, discover operations, generate request values, send one request, or run a stateful collection with assertions and response extraction. No server instrumentation or generated client is required.

Status: v0.1.0 is the first tagged release. The project is usable for local development and API smoke tests; interfaces may still evolve before v1.0.0.

Highlights

  • OpenAPI 3.x YAML/JSON loading and runner-focused validation.
  • Operation selection by operationId or METHOD /path.
  • Generated path, query, header, auth, and JSON body values.
  • Single requests, curl output, run-all, and YAML/JSON collections.
  • Status, header, JSONPath, body, regex, and response-schema assertions.
  • Retries, per-attempt timeouts, bounded parallel execution, and JSON reports.
  • Stateful workflows with dependencies, conditions, extraction, and safe hooks.
  • Named environments and Postman Collection v2.1 import/export.

Install

openrun requires Go 1.22 or newer:

go install github.com/dmedovich/openrun/cmd/openrun@v0.1.0
go install github.com/dmedovich/openrun/cmd/openrun-mcp@v0.1.0

Or build from source:

git clone https://github.com/dmedovich/openrun.git
cd openrun
make verify
./openrun version
./openrun-mcp -V

Quick start

Start with any OpenAPI 3.x document:

openrun validate -spec openapi.yaml
openrun list -spec openapi.yaml
openrun template -spec openapi.yaml -op createUser -json
openrun curl -spec openapi.yaml -op createUser -base http://localhost:8080
openrun run -spec openapi.yaml -op createUser -base http://localhost:8080 -expect 201

-op accepts an operationId or a selector such as POST /users/{id}. For daily use, initialize project defaults once and pass the operation as a positional argument:

openrun init
openrun run createUser -b http://localhost:8080 -d '{"email":"alice@example.com"}'
openrun run getUser -p id=42

openrun init creates .openrun.yaml. Commands discover it from the current directory or a parent directory, so a typical project can keep the spec, environment, and base URL out of every invocation:

spec: docs/openapi.yaml
env_file: environments.yaml
environment: local

Explicit flags always override project defaults. The long forms remain available for scripts, while common interactive overrides have short aliases:

Short Long Purpose
-s -spec OpenAPI spec path
-b -base Base URL
-e -environment Named environment
-p -path Path parameter
-q -query Query parameter
-H -H Header
-a -auth Security-scheme credential
-d -body JSON body or @file.json

Flags may appear before or after the positional operation. Running openrun run in a terminal without an operation opens a filterable operation selector. Generated request values can be overridden without editing the spec:

openrun run \
  -spec openapi.yaml \
  -op createUser \
  -base http://localhost:8080 \
  -path id=42 \
  -query notify=true \
  -H X-Trace-ID=local-run \
  -body '{"email":"alice@example.com"}' \
  -expect 201 \
  -validate-response

Use -body @body.json to load the JSON body from a file.

CLI commands

Command Purpose
init Create .openrun.yaml project defaults.
validate Load and validate runner-facing OpenAPI fields.
list [filter] List or filter methods, paths, tags, and operation IDs.
template Generate a request template for one operation.
export Export one or every generated template as JSON.
curl Print an equivalent curl command without sending it.
run Run one operation or a collection.
run-all Run every operation in the spec.
import-postman Convert Postman Collection v2.1 JSON to openrun files.
export-postman Convert a simple openrun collection to Postman v2.1.

Run openrun without arguments for the compact command reference.

Authentication

If an operation declares OpenAPI security schemes, provide credentials by scheme name and openrun will place them according to the spec:

openrun run \
  -spec openapi.yaml \
  -op getProfile \
  -base http://localhost:8080 \
  -auth bearerAuth="$TOKEN" \
  -auth apiKey="$API_KEY"

Supported auto-auth schemes are:

  • type: http, including bearer.
  • type: apiKey in header, query, or cookie.

OAuth, OpenID Connect, and mutual TLS specs are accepted, but credentials for those schemes are not applied automatically yet.

With apiary

openrun pairs naturally with apiary: generate the contract from Go, then immediately exercise it.

go install github.com/yaop-labs/apiary/cmd/apiary@latest
apiary -out openapi.yaml ./...
openrun validate -spec openapi.yaml
openrun run -spec openapi.yaml -op userCreateUser -base http://localhost:8080 -expect 201

openrun remains framework-independent: it reads the OpenAPI contract and does not inspect a Gin router or application process at runtime.

MCP server

openrun-mcp exposes the same library operations to MCP clients over stdio. It lets an agent inspect a spec, construct or send requests, run stateful collections, and synchronize an apiary-generated contract without parsing CLI text.

Tool Purpose Effect
validate_spec Validate runner-facing OpenAPI fields Read-only
list_operations Search by method, path, ID, tag, or summary Read-only
template_operation Build an editable request template Read-only
curl_operation Produce curl without sending a request Read-only
run_operation Send one generated request External HTTP
run_collection Execute an openrun collection External HTTP
generate_openapi Regenerate the configured spec with apiary Writes spec
check_openapi_stale Check source/spec synchronization Read-only
diff_openapi Return saved-versus-generated unified diff Read-only

Configure a client to launch the binary from the API project:

{
  "mcpServers": {
    "openrun": {
      "command": "openrun-mcp",
      "args": ["-config", ".openrun.yaml"]
    }
  }
}

The configured working directory is the project root containing .openrun.yaml. A minimal project configuration is:

spec: docs/openapi.yaml
env_file: environments.yaml
environment: local

Example tool inputs:

list_operations:

{"filter":"users"}

run_operation:

{
  "operation": "getUser",
  "path": {"id":"42"},
  "expect": "2xx",
  "validate_response": true
}

run_collection:

{"collection":"smoke.collection.yaml","timeout":"10s","retries":2}

An agent can use the tools as one deterministic workflow:

check_openapi_stale
  → diff_openapi when stale
  → generate_openapi after approval
  → validate_spec
  → list_operations
  → run_operation or run_collection

The last three tools invoke the existing apiary CLI. generate_openapi explicitly writes the configured spec, while stale-check and diff are read-only. The diff tool generates the current document through apiary -out - and compares it without overwriting the saved file.

By default apiary is resolved from PATH and reads its own apiary.yaml. Optional overrides belong in .openrun.yaml:

spec: docs/openapi.yaml
apiary:
  command: apiary
  workdir: .
  patterns: ["./internal/handler/...", "./internal/dto/..."]
  args: ["-title", "My API", "-version", "1.0.0"]

MCP-provided file paths are restricted to the project root. Tool annotations mark HTTP execution as an external action and OpenAPI generation as a destructive file write so clients can apply their approval policy.

Environments

Environment files provide a base URL, reusable variables, and auth credentials:

base_url: http://localhost:8080
vars:
  user_id: "42"
  email: alice@example.com
auth:
  bearerAuth: local-token
  apiKey: local-api-key

Use them with single requests, curl generation, collections, or run-all:

openrun run -spec openapi.yaml -env local.yaml -op getUser -path id="{{user_id}}"

One file can also contain named environments while preserving the same selected environment shape:

default: local
environments:
  local:
    base_url: http://localhost:8080
    vars:
      email: local@example.com
    auth:
      bearerAuth: local-token
  staging:
    base_url: https://staging.example.com
    vars:
      email: staging@example.com
    auth:
      bearerAuth: staging-token

Omit -environment to use default, or select one explicitly:

openrun run -spec openapi.yaml -env environments.yaml -environment staging -collection smoke.yaml

Named selection is supported by curl, run, and run-all. Files without a default require an explicit name. Single-environment files remain fully backward compatible.

Collections

A collection is a small YAML/JSON file with runnable request cases:

name: smoke
mode: sequential
headers:
  X-Test-Suite: smoke
query:
  locale: en
auth:
  bearerAuth: local-token
requests:
  - name: create user
    id: create_user
    operation: createUser
    body:
      email: "{{email}}"
      username: alice
    assert:
      status: 2xx
      schema: true
      headers:
        Content-Type: application/json
      json:
        $.username: alice
      body_contains:
        - alice
      body_matches:
        - '"id":[0-9]+'
    extract:
      user_id: $.id
  - name: get user
    operation: getUser
    depends_on:
      - create_user
    path:
      id: "{{user_id}}"
    assert:
      status: 200-299
      schema: true
      json:
        $.id: "{{user_id}}"

Assertions support exact status codes (201), status classes (2xx), inclusive ranges (200-299), exact response headers, JSON equality using paths such as $.user.id and $.items[0].id, body substring checks, and regular expressions. The older expect: 201 field remains supported as an exact status assertion. Set schema: true to validate the response selected by status code and Content-Type against its OpenAPI schema. Single operations and run-all can enable the same check with -validate-response.

The built-in validator currently covers the runner-facing JSON Schema subset: types (including integer vs number), required properties, objects, arrays, enums, local component refs, additionalProperties, allOf, anyOf, and oneOf. More advanced constraints remain on the roadmap.

A passed request can atomically extract JSON values into runtime variables for later sequential requests:

requests:
  - name: create user
    operation: createUser
    assert:
      status: 201
    extract:
      user_id: $.id
      access_token: $.auth.token
  - name: get created user
    operation: getUser
    path:
      id: "{{user_id}}"
    headers:
      Authorization: "Bearer {{access_token}}"

Strings are stored directly; numbers, booleans, objects, arrays, and null are stored as compact JSON. Extraction is applied only when every assertion and every extraction for the request succeeds. Reports expose extracted variable names but not their potentially sensitive values. The caller's environment is copied and is never mutated. Collections with extraction must use sequential mode.

Sequential requests can declare dependencies by stable request id. A request is skipped when any dependency failed or was skipped. Dependencies must refer to earlier requests, which prevents cycles and keeps execution deterministic:

requests:
  - id: create_user
    operation: createUser
    extract:
      user_id: $.id
  - id: load_user
    operation: getUser
    depends_on: [create_user]
    path:
      id: "{{user_id}}"

Requests can also be skipped from environment or extracted runtime variables:

skip_if:
  variable: environment
  equals: production

The predicate can contain exactly one of equals, not_equals, or missing. For example, missing: true skips when the variable does not exist. Skipped requests are reported separately, do not count as failures, and expose the variable name but never its value. depends_on is intentionally unavailable in parallel mode.

Reusable request defaults can be declared once at collection level:

name: smoke
headers:
  X-Test-Suite: smoke
  X-Tenant: "{{tenant_id}}"
query:
  locale: en
auth:
  bearerAuth: "{{access_token}}"
requests:
  - operation: listUsers
  - operation: getUser
    headers:
      X-Test-Suite: focused

Merge precedence is OpenAPI-generated values, collection defaults, request overrides, then CLI overrides. Shared values are expanded before every request, so sequential collections can reference variables extracted by earlier requests. CLI -H, -query, and -auth apply to every collection request; operation-specific -path and -body require a single -op.

Sequential requests can run small declarative hooks without executing scripts or arbitrary code:

pre_request:
  - require: access_token
  - set:
      variable: request_id
      value: $uuid
  - unset: stale_value
    when:
      variable: environment
      equals: local

post_response:
  - set:
      variable: last_status
      value: $response.status
  - set:
      variable: trace_id
      value: $response.header.X-Trace-ID
  - set:
      variable: created_id
      value: $response.json.$.id

Supported safe values are $uuid, $timestamp, $unix, $response.status, $response.body, $response.header.<name>, and $response.json.<JSONPath>. Ordinary values support {{variable}} substitution; prefix a literal dollar with $$. Each action contains exactly one of set, unset, or require, and may use a when condition with the same equals, not_equals, or missing predicates as skip_if.

Hook mutations, response extraction, and post-response updates form one request transaction: they become visible to following requests only when the request and every hook succeeds. Hooks are intentionally unavailable in parallel mode.

Run it:

openrun run -spec openapi.yaml -env local.yaml -collection smoke.yaml -report-json

Each request can override timeout and retry behavior:

requests:
  - operation: createUser
    timeout: 5s
    retries: 2
    retry_backoff: 200ms
    retry_statuses: [429, 500, 502, 503, 504]

Retries use exponential backoff and always rebuild the request body. Transport errors are retried as well. CLI-wide defaults are available through -timeout, -retries, and -retry-backoff; collection request values override them. Failed collection results include the final URL, attempt count, and a compact response body in text and JSON reports.

Collections run sequentially by default. Independent requests can use a bounded worker pool while keeping report results in file order:

name: read-only checks
mode: parallel
concurrency: 4
requests:
  - operation: health
  - operation: listProducts
  - operation: getVersion

CLI flags -parallel -concurrency 4 override execution mode for a collection or run-all. Keep stateful request chains sequential: response extraction, dependencies, and hooks are intentionally unavailable in parallel mode.

run-all builds a temporary collection from every operation in the spec:

openrun run-all -spec openapi.yaml -env local.yaml -expect 200

Postman compatibility

Simple Postman Collection v2.1 files can be converted to spec-driven openrun collections. Every request is matched to an OpenAPI operation by HTTP method and path; an unmatched or ambiguous URL is reported as an error.

openrun import-postman \
  -spec openapi.yaml \
  -in postman.json \
  -out smoke.collection.yaml \
  -env-out local.env.yaml

Postman collection variables are written separately through -env-out so their values are never silently discarded. Nested folders are flattened into request names. Headers, path and query values, raw JSON bodies, bearer auth, literal basic auth, and header/query API keys are supported. See examples/simple.postman.json for a small importable collection.

A simple openrun collection can also be exported:

openrun export-postman \
  -spec openapi.yaml \
  -collection smoke.collection.yaml \
  -env local.env.yaml \
  -out postman.collection.json

The exporter resolves OpenAPI-generated defaults and collection/request overrides into Postman requests. It intentionally rejects assertions, timeouts/retries, extraction, dependencies, skip conditions, hooks, and parallel execution because a simple Postman document cannot preserve those openrun semantics. Import likewise rejects scripts, non-JSON raw bodies, form-data, URL-encoded bodies, GraphQL, and unsupported auth types. Saved Postman response examples are ignored because they do not affect execution.

Go

spec := openrun.MustLoad("openapi.yaml")

resp, err := spec.Run(ctx, server.URL, "createUser")
if err != nil {
    t.Fatal(err)
}
if resp.StatusCode != 201 {
    t.Fatalf("status = %d", resp.StatusCode)
}

Timeouts and retries are also available from Go:

resp, err := spec.Run(
    ctx,
    server.URL,
    "createUser",
    openrun.WithTimeout(5*time.Second),
    openrun.WithRetries(2, 200*time.Millisecond),
)

Templates can be edited before execution:

tmpl, err := spec.Template("createUser")
if err != nil {
    t.Fatal(err)
}
tmpl.
    WithPathParam("id", "42").
    WithHeader("Authorization", "Bearer "+token).
    WithBody(map[string]any{
        "email": "alice@example.com",
    })

resp, err := spec.RunTemplate(ctx, server.URL, tmpl)

Responses can be checked directly from Go without running a collection:

checks := openrun.CheckResponse(resp, openrun.ResponseAssertions{
    Status:  "2xx",
    Headers: map[string]string{"Content-Type": "application/json"},
    JSON: map[string]any{
        "$.user.id": 42,
        "$.ok":      true,
    },
    BodyContains: []string{"created"},
})
for _, check := range checks {
    if !check.Passed {
        t.Error(check.Error)
    }
}

Schema-aware checks use the loaded spec and operation selector:

checks := spec.CheckResponse("createUser", resp, openrun.ResponseAssertions{
    Status: "2xx",
    Schema: true,
})

To inspect the final request without sending it:

target, err := openrun.URL(server.URL, tmpl)
curl, err := openrun.CurlTemplate(server.URL, tmpl)

Scope

  • OpenAPI YAML/JSON loading.
  • Spec validation for required runner-facing OpenAPI fields, refs, path params, responses, and supported security scheme shapes.
  • Operation listing by method, path, and operationId.
  • Request templates for path, query, header, and JSON body fields.
  • Sample generation from JSON Schema types, refs, recursive refs, maps, arrays, allOf, oneOf, anyOf, enums, defaults, and examples.
  • HTTP execution and curl output.
  • Environment files, collection files, run-all reports, and template export.
  • Structured response assertions in collections and JSON reports.
  • Response validation against documented status codes, media types, and JSON schemas.
  • Per-attempt timeouts, retries with exponential backoff, and failure diagnostics.
  • Sequential and bounded-parallel collection execution with stable report ordering.
  • Atomic JSONPath response extraction into runtime variables for sequential request chains.
  • Deterministic request dependencies and variable-based skip conditions.
  • Shared collection-level headers, query values, and auth credentials with explicit precedence.
  • Backward-compatible single and named multi-environment files with default selection.
  • Transactional declarative pre-request and post-response hooks with safe built-in values.
  • Spec-matched import and export for simple Postman Collection v2.1 documents.
  • Project defaults, compact positional CLI commands, and interactive operation selection.
  • MCP tools for validation, discovery, execution, collections, and apiary synchronization.

This is intentionally not a full OpenAPI validator yet. The first release focuses on building useful request templates and running them predictably from Go tests or a CLI.

Future development continues across reports, OpenAPI compatibility, security, and release automation.

License

Apache-2.0.

Releases

Packages

Contributors

Languages