Skip to content

Repository files navigation

Kaja

A canvas for your APIs

An agent calling your APIs leaves you a transcript to trust. Kaja gives it a canvas instead.
Connect your gRPC, OpenAPI and MCP apps, and what your agent does is drawn, logged call by call, and paused for your approval whenever you want one.

Download on the Mac App Store   Pull from Docker Hub

Live Demo · Website

Latest Release Docker Pulls License

Kaja running a script against a gRPC app

Features

  • Your agent drives it. Kaja runs an MCP server, so your agent reads what your apps expose, writes TypeScript against them, and runs it. Nothing it does is invisible: every run lands in the sidebar next to your own.
  • A canvas, not a transcript. A script draws what it produced. Tables fill row by row as a loop runs, page and search themselves, and sit alongside text and code.
  • Every call is on the record. Request, response, headers and duration for each call in a run, whoever pressed Run.
  • Approval when you want it. A script can hold a call until you approve it, so a write goes out when you say so and not before.
  • gRPC, OpenAPI and MCP. Read the surface from your .proto files, from gRPC server reflection, from an OpenAPI document, or from another MCP server. Twirp is supported too.
  • Still yours to drive. Click a method and Kaja writes you the call, with full autocomplete for apps, methods and message fields.
  • macOS & Docker. Available on the Mac App Store or as a Docker container for any environment.

Run with Docker

docker run --pull always --name kaja -d -p 41520:41520 \
    -v /my_app/proto:/workspace/proto \
    -v /my_app/kaja.json:/workspace/kaja.json \
    -v /my_app/scripts:/workspace/scripts \
    --add-host=host.docker.internal:host-gateway kajatools/kaja:latest

Then open http://localhost:41520.

The scripts mount is optional: .ts files in /workspace/scripts appear under Scripts in the sidebar, ready to open and run. The container serves its workspace read-only, so they can't be edited from the browser — check them into the repository they belong to and mount them in.

Configuration

On macOS, apps are configured through the UI. The configuration is stored at ~/Library/Application Support/kaja/kaja.json.

With Docker, create a kaja.json file and mount it into the container. Every entry in apps is one app: a name plus one block whose key is the app's type, holding that type's parameters:

{
  "apps": [
    {
      "name": "users",
      "twirp": {
        "url": "http://host.docker.internal:41522",
        "proto_dir": "users/proto"
      }
    },
    {
      "name": "teams",
      "grpc": {
        "url": "host.docker.internal:41523",
        "reflection": true,
        "headers": { "Authorization": "Bearer xxx" }
      }
    }
  ]
}

The server serves a workspace it does not own, so the UI shows this configuration read-only: it is managed by whoever mounts the file — checked into Git, deployed with the container — and not edited by the engineers running it.

App options

Each app has a name and exactly one typed block:

Type Parameters
grpc url, proto_dir (path to .proto files), reflection (use gRPC server reflection instead of local proto files), headers
twirp url, proto_dir, headers

headers are sent with each request (e.g. {"Authorization": "Bearer xxx"}); for gRPC they are sent as metadata.

Migrating from the old format

Earlier versions used a top-level projects list with a protocol field. Kaja migrates these automatically on load — but to update a file by hand, move each project into apps and replace its protocol/url/protoDir/useReflection fields with a block named after the type.

Before:

{
  "projects": [
    { "name": "users", "protocol": "RPC_PROTOCOL_TWIRP", "url": "http://host.docker.internal:41522", "protoDir": "users/proto" }
  ]
}

After:

{
  "apps": [
    { "name": "users", "twirp": { "url": "http://host.docker.internal:41522", "proto_dir": "users/proto" } }
  ]
}

Docker arguments

Argument Description
--pull always Always pull the latest image. Kaja is updated frequently.
--name kaja Name the container for easy management.
-d Run in detached mode.
-p 41520:41520 Map the container port. Kaja listens on 41520 by default.
-v .../proto:/workspace/proto Mount your proto_path into the container.
-v .../kaja.json:/workspace/kaja.json Mount your configuration file.
--add-host=host.docker.internal:host-gateway Access host services from the container.

Development

The development scripts require Go and Bun installed. If not installed, they will offer to install them for you via Homebrew.

  • Run in local server: scripts/server (pass --editable to edit workspace/kaja.json from the UI)
  • Run in Docker: scripts/docker
  • Run the desktop app: scripts/desktop
  • Test UI: (cd ui && bun test)
  • TSC UI: (cd ui && bun run tsc)
  • Test server: (cd server && go test ./... -tags development -v) — the tag scripts/server builds with. Without it the packages embed a production UI bundle, which only go run cmd/build-ui/main.go writes.
  • Update demo protos: scripts/demo-protos — refreshes the quirks and grpcb.in protos in workspace/. The demo services live in kaja-tools/website; theatre, seating and concierge need no protos here, since they are OpenAPI, gRPC reflection and MCP respectively.

The demo, and the preview apps

Both public deployments serve this repository's own workspace/ — the demo apps scripts/server starts on — baked into the image by the Dockerfile's demo stage. One image, two places it runs, so a change is clicked through on exactly what it will become:

  • demo.kaja.tools (deploy/demo/fly.toml) — deployed by the main workflow on every push to main, and so from the commit that changed it. One machine stays up, so the first visitor of the day doesn't wait for a cold start.
  • https://kaja-pr-<number>.fly.dev (deploy/preview/fly.toml) — one app per pull request, so a change can be clicked through before it is merged. The preview workflow rebuilds it on every push and destroys the app when the pull request closes; the URL is a comment on the pull request throughout. A preview runs no machine until someone opens its URL.

The workspace's configuration is read-only, like any server build, so neither ever asks Fly for a disk, and workspace/scripts/ ships with them — the demo opens with scripts to press Run on.

Fork pull requests are skipped: they have no access to the token. Setting this up in a fresh repository takes a FLY_API_TOKEN secret that may create and destroy apps (fly tokens create org <org> — an app-scoped deploy token can't create the per-pull-request apps), and optionally a FLY_ORG variable if the org isn't personal. The demo's own app and certificate are created once, by hand:

fly apps create kaja-demo
fly certs add demo.kaja.tools --app kaja-demo

Releases

Releases are cut from GitHub — no local build needed. Every push to main uploads a new build to TestFlight. To ship a version, run the release workflow (Actions → Run workflow):

  • open (with patch/minor/major) — bumps the version on a branch and opens a PR. Merge it yourself: main is protected, so the bump has to arrive as a PR and pass the test check. TestFlight builds carry the new version from then on.
  • ship — tags the commit and publishes a GitHub Release for it, with notes covering the whole cycle. Run it when you promote one of those TestFlight builds to the App Store.

About

A code-based UI for exploring and calling gRPC and Twirp APIs.

Topics

Resources

Stars

16 stars

Watchers

1 watching

Forks

Releases

Packages

Used by

Contributors

Languages