Skip to content
Subako Docs
English
Esc
↑↓navigate↵open⌘Jpreview
On this page

Other languages (ABI)

The ABI, for writing a package in any language.

The SDKs are conveniences. Any language that builds a WebAssembly module for WASI preview 1 can write a function package by speaking ABI 1 directly — C and C++ with wasi-sdk, Zig, AssemblyScript, TinyGo, Swift, a language of your own.

ABI 1 is small: the module is an MCP server over MCP’s stdio transport, on the stateless revision 2026-07-28, and it may import one host module for HTTP.

The module

Requirement
Kind A WebAssembly core module (not a component)
Target WASI preview 1 — wasm32-wasip1, or wasm32-wasi in older toolchains
Exports _start, taking and returning nothing (a WASI command), and its linear memory as memory
Imports Only from wasi_snapshot_preview1 and subako_http
Features WebAssembly 2.0 without GC: bulk memory, reference types, multi-value, sign extension, non-trapping float-to-int, and 128-bit SIMD. Threads, shared memory, exceptions, tail calls, memory64, multi-memory, and relaxed SIMD are not available.

Upload checks all of this, and names what it refused: an import Subako does not provide, a missing export, a component instead of a module.

A call, from start to end

Subako starts the module

It instantiates the module and calls _start, with argv of one entry and no environment variables. The first request is already waiting on stdin.

The module answers each request

Requests arrive on stdin, one JSON-RPC 2.0 message per line. Answer each on stdout, as one line of compact JSON ending in \n, and flush. Subako sends one request at a time; answer it as it arrives, because stdin stays open until the last request is answered.

Subako closes stdin

Once the last request is answered, stdin reaches end of file. The module must then exit with status 0 — return from _start, or proc_exit(0) — within one second.

Stderr is free for logs. Stdout is the protocol alone: any line on it that is not a JSON-RPC message breaks the call.

The protocol

Every request carries the protocol revision and the client’s capabilities in _meta. There is no initialize handshake.

{"jsonrpc":"2.0","id":"call-0192f5c4","method":"tools/call","params":{"name":"greet","arguments":{"name":"Ada"},"_meta":{"io.modelcontextprotocol/protocolVersion":"2026-07-28","io.modelcontextprotocol/clientInfo":{"name":"subako","version":"0.2.3"},"io.modelcontextprotocol/clientCapabilities":{}}}}

Echo each request’s id in its response. Ids are opaque — a string or a number — and differ every time.

When a package is uploaded

Subako runs the module once to learn its tools, with 10 seconds to answer, no volume, no network, and only /package mounted.

  1. server/discover — answer the revisions you speak and that you offer tools:

    {"jsonrpc":"2.0","id":"discover-1","result":{"supportedVersions":["2026-07-28"],"capabilities":{"tools":{}}}}
  2. tools/list — answer every tool. A nextCursor asks for another page, up to 16 pages.

    {"jsonrpc":"2.0","id":"tools-list-1","result":{"tools":[{"name":"greet","description":"Greet someone by name.","inputSchema":{"type":"object","properties":{"name":{"type":"string","description":"Who to greet."}},"required":["name"]}}]}}
  3. Stdin closes; exit 0.

What tools/list answers is what the package offers, until its next version:

Rule
Tools 1 to 64, with unique names
name 1 to 64 characters: a–z, 0–9, _, -
description Up to 1,024 characters. The model reads it.
inputSchema A JSON Schema with "type": "object" at the top, up to 64 KiB
outputSchema Optional, up to 64 KiB. Shown to people, not the model.
All tools together Up to 256 KiB

When the agent calls a tool

One tools/call with the tool’s name and its arguments, an object. Answer with an MCP CallToolResult:

{"jsonrpc":"2.0","id":"call-0192f5c4","result":{"content":[{"type":"text","text":"{\"message\":\"Hello, Ada!\"}"}],"structuredContent":{"message":"Hello, Ada!"}}}

Subako does not check arguments against your schema, so validate them yourself. A failure the model should read is a result with isError, never a JSON-RPC error:

{"jsonrpc":"2.0","id":"call-0192f5c4","result":{"content":[{"type":"text","text":"name must not be empty"}],"isError":true}}

The content blocks Subako reads:

Block Shown to the model as
{"type": "text", "text": ...} The text
{"type": "image", "mimeType": "image/png", "data": "<base64>"} The image, counted against stdout’s 1 MiB
{"type": "resource_link", "uri": "volume:///tmp/chart.png", "name": "chart.png"} The image file at /volume/tmp/chart.png, read after the module exits

A volume:/// URI is the path beneath /volume, each segment percent-encoded. Images are PNG, JPEG, GIF, or WebP. Any other resource_link is shown as a line naming its link, and any other block as a note that it was left out.

What breaks a call

The model is told the function crashed, with the last 4 KiB of stderr, when the module:

  • answers with a JSON-RPC error, or a resultType other than complete;
  • prints a line on stdout that is not a JSON-RPC message, or answers an id it was not asked;
  • sends a request of its own — a stateless server sends none;
  • exits before answering, exits non-zero, or does not exit within a second of stdin closing;
  • traps, or runs out of memory or time.

Notifications — messages without an id — are ignored.

Files

The filesystem is a set of WASI preopens, in this order: /volume/tmp, then /volume/data when the agent enables it, then /package when the package has assets, then /skills and each /skills/<name>. Open paths by their absolute names.

fd_allocate answers ENOTSUP. Writes to files past the call’s 1 GiB write budget answer EDQUOT. The sock_* functions are linked but never handed a socket: HTTP goes through subako_http.

HTTP: subako_http

Two imports from the module subako_http, on i32 values:

Import Does
request(ptr, len) -> len Reads a JSON request of len bytes at ptr, sends it, holds the JSON answer, and returns its length. Blocks until the answer is in.
read_response(ptr, len) -> 0 Copies the held answer to ptr. len must be what request returned. Returns 0, and forgets the answer.

A negative return is a misuse of the ABI: -1, a range outside the module’s memory; -2, no answer held; -3, a length that does not match.

__attribute__((import_module("subako_http"), import_name("request")))
int32_t subako_http_request(const uint8_t *ptr, int32_t len);

__attribute__((import_module("subako_http"), import_name("read_response")))
int32_t subako_http_read_response(uint8_t *ptr, int32_t len);

The request, up to 8 MiB, takes no fields but these:

{
  "method": "POST",
  "url": "https://api.example.com/orders",
  "headers": { "accept": "application/json" },
  "body": { "type": "json", "json": { "order": 1042 } },
  "output": { "type": "inline" },
  "follow_redirects": true,
  "timeout_ms": 5000
}
Field
method GET (the default), HEAD, POST, PUT, PATCH, DELETE, or OPTIONS
url Required. https only.
headers Lowercase names. Connection headers such as host and content-length are dropped.
body {"type": "text", "text": ...}, {"type": "json", "json": ...}, or {"type": "file", "path": "/volume/tmp/in.bin"}
output {"type": "inline"} (the default), or {"type": "file", "path": "/volume/tmp/out.bin"} to save the body to the volume
follow_redirects true by default, up to 10 hops
timeout_ms Bounds the whole request. The call’s own timeout applies either way.

The answer is {"ok": ...} or {"error": ...}:

{"ok":{"status":200,"headers":{"content-type":"application/json"},"body":{"type":"inline","text":"{\"status\":\"shipped\"}"}}}
{"ok":{"status":200,"headers":{},"body":{"type":"file","path":"/volume/tmp/out.bin","bytes":48211}}}
{"error":{"kind":"host_not_allowed","message":"api.example.com is not in this function's network allowlist"}}

An inline body is at most 256 KiB, and must be UTF-8 text; otherwise the request fails with too_large or not_text, and should save to a file instead. The error kinds are invalid_request, host_not_allowed, forbidden_destination, credential_header, credential_unavailable, too_many_redirects, timeout, too_large, not_text, write_budget, bad_path, connection, and abi.

Testing locally

A module can be tried on your machine in any runtime for WASI preview 1 commands: give it one request on stdin and read its answer on stdout.

printf '%s\n' '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"_meta":{"io.modelcontextprotocol/protocolVersion":"2026-07-28","io.modelcontextprotocol/clientCapabilities":{}},"name":"greet","arguments":{"name":"Ada"}}}'

The runtime must provide subako_http for the module to load. The Rust and Go templates ship tests/subako_http_stub.wat, a stub whose every request fails, for runtimes that can link one module into another:

(module
  (func (export "request") (param i32 i32) (result i32) i32.const -1)
  (func (export "read_response") (param i32 i32) (result i32) i32.const -2))

Checklist

  1. Build a WASI preview 1 command module exporting _start and memory, without threads or exceptions.
  2. Read stdin a line at a time; at end of file, exit 0.
  3. Ignore messages without an id.
  4. Answer server/discover with "supportedVersions": ["2026-07-28"] and a tools capability.
  5. Answer tools/list with your tools and their object input schemas.
  6. Answer tools/call with a CallToolResult, using isError for failures.
  7. Write one compact JSON line per answer to stdout, and flush. Log to stderr.
  8. Read and write files under /volume/tmp, /volume/data, /package, and /skills.
  9. Send HTTP through subako_http.

Then pack module.wasm with a subako-package.json whose abi is 1.

Was this page helpful?