The hub

Heddle is a capability compiler.

You write a service idea once: domain types, operations, and an effect that runs them. That value is the AST. Everything a human, a system, or an agent uses is an interpreter of it.

  • Humans get HTTP, OpenAPI / Swagger, and a web UI.
  • Systems get the same HTTP, plus Client.
  • Agents get MCP 2026-07-28 over Streamable HTTP or a stdio process, plus the 2025 handshake for hosts that still send it.
  • CLI is a later interpreter of the same OpArgs. It is not a second product.

That shape is a human-centric AI service hub. Agents do not get a shadow API. They get the same BoundOp, marked with .mcp.

capability compiler
BoundOp

getShow

Endpoint + In => ZIO[R, E, Out]. Written once.

GET/shows/{id}.mcpReadOnly
CLI

Same OpArgs. Not shipped. The AST is already this shape.

GET /shows/1 HTTP/1.1

HTTP/1.1 200 OK
content-type: application/json

{"id":1,"name":"Evening bill"}

Hosts are interpreters. Do not grow a second tool DSL.

Progressive, not a conversion

Day one is Routes and HeddleApp. That is already a service. You can stop there.

Endpoint + Api.bind is how documentation and the other hosts appear. OpenAPI is a projection. MCP is a host protocol. stdio is the same engine on a pipe. Do not grow a second tool DSL.

The effect is the body

A handler is Request => ZIO[R, E, Response]. Api.bind is In => ZIO[R, E, Out]. Accept, read, and write are ordinary ZIO on the JVM (Loom by default), on Node, and on Native. Interrupt a connection and the fiber stops. Typed errors stay in E until a host maps them to HTTP or MCP.

The AST is the contract. The effect is the work. Hosts are projections.

Three runtimes

Server.install is the same bind on the JVM, on Node, and on Scala Native. Loom is the JVM default scheduler, not a requirement to bind. HTTP/2 is the JVM bind. JS and Native serve HTTP/1.1. docs-js mounts Hub widgets. It is not a second Api.

Where to go

A server is still a server

This is the only snippet on this page that is meant to be copied. The rest is the hub, not a code sample.

val routes = Routes(Method.GET / "health" -> Handler.text("ok"))