ABI mapping
An application’s ABI manifest is turned into a zod schema per method, which the MCP
SDK advertises as JSON Schema. That schema is what stops an agent guessing at argument
shapes - a wrong type fails locally, before anything reaches the node.
The author’s /// doc comments, when the ABI carries them, become the tool, parameter, type and output descriptions.
Tool naming
Section titled “Tool naming”Generated tools are named:
<app>_<method> single-service application<app>_<service>_<method> multi-service application<app> is the trailing dot-separated segment of the package name, lowercased with
every run of non-[a-z0-9_] characters collapsed to _. So com.calimero.kv-store
gives kv_store, and its get method gives kv_store_get.
The slug, including a multi-service app’s _<service>, is cut to 20 characters.
A full name longer than 49 characters (64 minus the 15-character mcp__mero-mcp__ prefix Claude Code adds) keeps its first 42 characters and ends in _ plus the first 6 hex of its sha256.
The slug is derived from the resolved application, not the string you typed, so one application always names its tools the same way.
If two apps sanitise to the same slug, the second in package order gets _ plus the first 6 alphanumerics of its application id, so names are stable across sessions.
The same applies to one package from two signers, which are two apps: the one later in application id order takes the suffix.
A whole name is also kept unique: when a slug plus a method spells a fixed tool (com.x.node with status would be node_status) or another app’s tool, that tool takes the application-id suffix too.
Scalars
Section titled “Scalars”| ABI kind | JSON Schema |
|---|---|
bool |
boolean |
string |
string |
unit |
null |
i32, i64 |
integer |
u32, u64 |
integer, >= 0 |
f32, f64 |
number |
Composites
Section titled “Composites”| ABI kind | JSON Schema |
|---|---|
list |
array of the item schema |
map |
object with string keys and the value schema |
tuple |
fixed-length array, positionally typed |
record |
object with one property per field |
A field marked nullable in the ABI becomes nullable in the schema. For method
parameters, nullable also means optional — a nullable parameter can be omitted
entirely, not just passed as null.
bytes accepts either form:
- a hex string —
"deadbeef"— which is the only string form the Calimero toolchain reads, or - an array of integers 0–255, which is what the node itself wants.
Hex input is transformed to a byte array before it goes out. Fixed-size bytes carry
their length in both forms: the array is length-constrained, and the hex pattern
requires exactly 2 × size hex characters — so the size travels in the advertised
JSON Schema rather than living only in the validator.
Each bytes field is described as bytes: a hex string or a 32-byte array, so an agent
reading the schema knows both are accepted.
Variants (enums)
Section titled “Variants (enums)”Rust enums arrive as variant definitions.
With no tagging keys they map to serde’s default externally tagged representation:
- Unit variants ride as bare names —
"Pending". - Payload variants ride as a single-key object —
{"Approved": {"by": "…"}}.
A variant with no payloads anywhere becomes a plain string enum, unless a unit variant carries a doc, in which case it becomes a oneOf of { const, description } entries instead (see Docs and hints).
A mix becomes a oneOf of the unit variants (as an enum or as documented consts) and one object schema per payload variant.
An enum with another serde representation carries tag, content or untagged on its variant definition, and its schema follows that wire shape:
| ABI keys | serde attribute | Unit variant | Payload variant | JSON Schema |
|---|---|---|---|---|
| none | default | "Name" |
{"Name": payload} |
enum or oneOf, as above |
tag |
#[serde(tag = "kind")] |
{"kind": "Name"} |
{"kind": "Name", ...payload fields} |
oneOf |
tag + content |
#[serde(tag = "t", content = "c")] |
{"t": "Name"} |
{"t": "Name", "c": payload} |
oneOf |
untagged |
#[serde(untagged)] |
null |
the bare payload | anyOf |
An enum with a single variant is that variant’s schema, not a union.
An internally tagged payload is one object holding the tag and the payload’s fields, found through a newtype struct too.
A map payload keeps its entries beside the tag, each checked against the map’s value type.
A payload that is itself an internally or adjacently tagged enum adds its own tag and payload beside the outer tag.
Any other payload, including an enum reusing the outer tag’s name, only requires the tag and leaves the rest to the node.
A tagged value is matched by its tag, so a refusal names the problem of the variant the tag chose.
Untagged members can overlap, so they are advertised as anyOf, and a value is matched against them in order, the first match winning, as serde does.
Bytes inside an untagged member take no hex string form, because serde would read that string as a later string member instead.
A value in another representation, such as {"Text": {...}} where the ABI says tag, is refused before the node is called.
CRDT collection wrappers
Section titled “CRDT collection wrappers”A record carrying both crdt_type and inner_type is a transparent wrapper: the
wire value is the inner type, not the wrapper’s fields. The schema unwraps it, and the
rendered signature unwraps it the same way — the signature an agent reads has to match
what the tool actually accepts.
Type references and recursion
Section titled “Type references and recursion”Named ABI types are advertised once under $defs and referenced with $ref, so a recursive type is described exactly rather than degrading to unknown.
A $ref naming a built-in absent from the type table degrades to unknown rather than failing the whole application.
unknown means the argument is accepted unvalidated and passed through — the call can
still succeed, it just is not checked locally.
The app_handle argument
Section titled “The app_handle argument”Every generated tool takes one required argument besides the method’s own:
app_handle: string The app_handle select_app returned for this application; it names the context the call runs in.The context a call runs in comes from the handle, so there is no separate context argument.
The handle is the only argument mero-mcp adds, so an application’s own context parameter is an ordinary parameter.
A method that declares its own parameter named app_handle would collide with it, so it has no generated tool and is reached with call, whose args object keeps the two apart.
Outgoing arguments are built by filtering the input down to the method’s declared parameters, so the handle never reaches the application.
Method signatures
Section titled “Method signatures”describe_app and select_app render each method as a signature, which is a documented method’s tool description too, below its doc (see Docs and hints):
[view] get(key: string) -> string | null[mut] set(key: string, value: string) -> unit[view] entries() -> map<string, string>[view]— the ABI marks itread_only; the tool carriesreadOnlyHint.[mut]— everything else.?after a parameter name means nullable/optional.| nullafter the return type means the return is nullable.- A missing return type renders as
unit.
Type names in signatures follow the same rules as the schemas: T[] for lists,
map<K, V> for maps, (A, B) for tuples, $ref names for referenced types, and CRDT
wrappers unwrapped to their inner type.
A method that returns something also gets an outputSchema derived from its return type, where bytes is a number array, since that is what the node returns.
For 2025-11-25 clients the SDK wraps a return that is not an object as { result }; 2026-07-28 clients get the return schema itself.
Docs and hints
Section titled “Docs and hints”An app’s /// doc comments travel in its ABI as optional doc fields.
A documented method’s tool description is the doc, a blank line, then the signature:
Apply a batch of block edits.
# ErrorsFails past 512 edits.
[mut] set_blocks(edits: Edit[], now: u64) -> u32A documented parameter or record field carries its doc as the JSON Schema description; on a bytes value the doc comes first and the bytes hint follows in parentheses.
A documented named type carries its doc on its $defs entry, and documented enum variants become oneOf entries of { const, description }.
A method’s # Returns section becomes the outputSchema description.
#[app::destructive] and #[app::idempotent] set destructiveHint and idempotentHint; a read-only method is always idempotent.
describe_app lists each method’s description with one indented name: doc line per documented parameter.
With no docs in the ABI (an app built before docs existed, or a merod that drops them), descriptions are the bare signatures and parameters carry no description.