Request and the Verb-Dispatch DSL
khive exposes one MCP tool: request. Its ops argument holds one or more verb calls. The tool parses those calls, routes each verb to the loaded pack, and returns an outcome for every operation.
One tool keeps the MCP surface stable while packs can add or remove verbs. A client only needs to call request; it selects the operation in ops. Use the API reference for the current verb catalog and each verb’s parameters. This page covers how to compose calls.
Choose an input form
The function-call form is compact when writing a call directly:
request(ops="search(kind=\"entity\", query=\"LoRA\")")
The JSON form represents the same operation when a client is already building structured JSON:
request(ops="{\"tool\":\"search\",\"args\":{\"kind\":\"entity\",\"query\":\"LoRA\"}}")
Both forms produce the same operation for non-chain work. JSON may be a single object as above or an array of objects. Function-call form is generally easier to read and write by hand; JSON avoids manual string construction for deeply nested argument values.
Quote values as JSON
Argument values use JSON literals even in the function-call form. Strings are always double-quoted:
get(id="abc")
Not:
get(id=abc)
Numbers, booleans, null, arrays, and objects use their normal JSON syntax. Argument names remain unquoted identifiers.
Run independent operations in parallel
Wrap independent function-call operations in [...], separated by commas:
request(ops="[stats(), memory.recall(query=\"attention cache behavior\", limit=3)]")
The equivalent JSON batch is:
request(ops="[{\"tool\":\"stats\",\"args\":{}},{\"tool\":\"memory.recall\",\"args\":{\"query\":\"attention cache behavior\",\"limit\":3}}]")
A batch accepts at most 100 operations. Its operations run concurrently, with no ordering guarantee; results are returned in input order. Use it for work that has no dependency between operations. JSON form is always single or parallel, so it is also limited to independent operations.
Pass a result to the next operation
Use | when a later operation needs an earlier result. A chain runs in order, and $prev selects data from the operation immediately before it:
request(ops="create(kind=\"entity\", entity_kind=\"concept\", name=\"LoRA\") | link(source_id=$prev.id, target_id=\"<document-id>\", relation=\"introduced_by\")")
$prev refers only to the immediately preceding result. For example, in a() | b() | c(id=$prev.id), the reference in c reads b’s result, not a’s. If a later operation needs a non-adjacent result, split the work into separate request calls or make that result the immediate predecessor.
$prev is available only in function-call chains. JSON form and parallel batches reject it. A failed chain operation prevents subsequent operations from running; completed operations are not rolled back.
Read the result envelope
By default, request returns a results array and an aggregate summary inline. Each completed operation has one of these shapes:
{ "ok": true, "tool": "search", "result": { "...": "..." } }
{ "ok": false, "tool": "get", "error": "not found: ..." }
For example, a parallel batch can return:
{
"results": [
{ "ok": true, "tool": "stats", "result": { "...": "..." } },
{ "ok": false, "tool": "memory.recall", "error": "..." }
],
"summary": { "total": 2, "succeeded": 1, "failed": 1, "aborted": 0 }
}
A failure in a parallel batch does not stop its siblings. In a chain, entries after the failure are returned as { "ok": false, "tool": "...", "aborted": true }; the summary records their count in aborted.
A successful multi-backend search can still be incomplete when one backend is unavailable. In that case the search entry includes "partial": true and a "missing_backends": [...] list plus bounded "backend_errors": {...} causes beside result. Check this operation-level advisory even when ok and the aggregate request status report success. It survives batch/chain execution, presentation modes, and daemon frame-budget omission. A degraded empty result instead carries the same diagnostics inside error.kind="search_incomplete".
The inline results/summary envelope is the default. Set the optional save_to parameter to sink the full results to a JSONL file instead; request then returns a small manifest (the file path, row count, and integrity fields) rather than the inline envelope. Read results and summary only when save_to is omitted. See the API reference for save_to and its export-destination restriction.
Distinguish syntax errors from operation errors
An invalid DSL string never reaches a verb handler. Lexing and parsing failures such as unterminated strings, malformed JSON, too many operations, or invalid use of $prev are reported by MCP as an invalid_params RPC error. Correct the ops string and submit the request again.
Once the DSL parses, validation or execution failures from an individual verb are returned in that operation’s { "ok": false, "error": ... } entry. This is why a parallel batch can partly succeed: each valid, non-conflicting operation has its own outcome.
Gotchas
- Bareword values are not strings. Write
query="LoRA", notquery=LoRA. - Do not mix top-level separators. Commas inside
[...]mean parallel work;|means a sequential chain. A request that mixes them at the top level is invalid. Nested JSON arrays and objects may still contain commas. - A request boundary has a cost. Each
requestcall adds a transport and dispatch round trip. The server keeps long-lived state warm between calls, but$prevexists only inside one chain and is not a cross-call cache. Batch independent work and chain direct dependencies when that reduces calls. - Choose the form by dependency. A JSON array is not a sequence. Use a function-call chain when one operation needs another operation’s output.
For verb names, required arguments, and return details, see the API reference.
Raw markdown for this page: /md/request-and-dsl.md