Skip to content

21 · Catalogues

The router reads a turn against your tools, and the tool check decides a call by what each tool declares: both read your catalogue. This chapter is how tools get into one -- from the forms you already write them in -- how each is marked a read or a write, the lints stalwart verify reports, and how a tool is taken out of service. The catalogue is a file of yours: nothing of it is kept on Stalwart's servers, and every plan and check carries it.

Language support

The selector reads turns written in English and in Brazilian Portuguese (pt-BR). The language is chosen, never guessed from the turn: the console's Settings page (Language the selector reads), language on a plan request, or the settings.language of your vocabulary file; with none of them, English. A Portuguese turn may be typed with its accents or without them. European Portuguese is not a grammar the selector has, and a turn in any other language is unread.

Making your catalogue

stalwart catalogue import writes the catalogue on your machine, to --out (or prints it). It reads your tools in four forms:

  • MCP: an MCP server's tools, listed from this machine with --mcp <address or command>;
  • OpenAPI: an OpenAPI 3.0 or 3.1 document, one tool per operation;
  • LangChain: your LangChain tool objects, which tool_catalogue(tools, agent=...) in stalwart_agents.langchain makes into a catalogue in your process (chapter 27);
  • TypeScript: the exported functions of a TypeScript project, as the @stalwart-agents/catalogue package reads them from your compiler's declarations (below).

A Part I agent's catalogue is made from agent.yaml each time it starts, and by stalwart verify (chapter 31). The shop's tools, as an MCP server lists them and as an OpenAPI document:

# An MCP server's answer to `tools/list`, as it sends it.
MCP = {"tools": [
    {"name": "get_order", "description": "Show an order: its total, the card on file and its status.",
     "inputSchema": {"type": "object", "properties": {"order_id": {"type": "string"}}, "required": ["order_id"]},
     "annotations": {"readOnlyHint": True}},
    {"name": "charge_card", "description": "Charge an amount to the card on file for an order.",
     "inputSchema": {"type": "object", "properties": {"order_id": {"type": "string"}, "amount": {"type": "number"}},
                     "required": ["order_id", "amount"]},
     "annotations": {"readOnlyHint": False},
     "_meta": {"compensation": {"tool": "refund_card",
                               "arguments": {"order_id": "$args.order_id", "amount": "$args.amount"}}}},
    {"name": "refund_card", "description": "Refund an amount to the card on file for an order.",
     "inputSchema": {"type": "object", "properties": {"order_id": {"type": "string"}, "amount": {"type": "number"}},
                     "required": ["order_id", "amount"]},
     "annotations": {"destructiveHint": True}},
    {"name": "archive_order", "description": "Move an order to the archive.",
     "inputSchema": {"type": "object", "properties": {"order_id": {"type": "string"}}, "required": ["order_id"]}},
]}
# The same shop as an OpenAPI 3.1 document: one tool per operation.
OPENAPI = {"openapi": "3.1.0", "info": {"title": "Shop", "version": "1"}, "paths": {
    "/orders/{order_id}": {"get": {
        "operationId": "get_order", "summary": "Show an order",
        "parameters": [{"name": "order_id", "in": "path", "required": True, "schema": {"type": "string"}}]}},
    "/orders/{order_id}/charges": {"post": {
        "operationId": "charge_card", "summary": "Charge an amount to the card on file",
        "parameters": [{"name": "order_id", "in": "path", "required": True, "schema": {"type": "string"}}],
        "requestBody": {"content": {"application/json": {"schema": {
            "type": "object", "properties": {"amount": {"type": "number"}}, "required": ["amount"]}}}},
        "x-stalwart-idempotency-key": "Idempotency-Key"}},
    "/orders/search": {"post": {
        "operationId": "search_orders", "summary": "Search the orders", "x-stalwart-read": True,
        "requestBody": {"content": {"application/json": {"schema": {
            "type": "object", "properties": {"customer": {"type": "string"}}}}}}}},
}}
stalwart catalogue import --mcp http://localhost:8931/mcp --out catalogue.json
stalwart catalogue import shop-openapi.json --out catalogue.json

A tool's name is read as words: get_order, getOrder and get-order are all get order, the first word its action and the rest the thing it acts on. For an OpenAPI operation with no operationId, the action is taken from its summary and the thing from its path.

Reads and writes

Every write is confirmed by a person before it runs (chapter 24), so whether a tool writes matters, and it is never guessed in the direction that costs you:

  • MCP: a read where it says readOnlyHint: true (or read_only in _meta); a tool marked destructiveHint is a write whatever else it says; an unmarked tool is a write -- archive_order above. An unmarked tool costs a confirmation, never a wrong call;
  • OpenAPI: GET, HEAD and OPTIONS are reads, and every other method a write, unless the operation says x-stalwart-read: true -- a search sent as a POST, like search_orders above;
  • TypeScript: each function says @read or @write in its doc comment. One that says neither is not guessed at all: the import reports tool_kind_undeclared for it, an error (below).

What a tool's form cannot say, the tool declares beside it -- in MCP's annotations or _meta, in an OpenAPI operation's x-stalwart object, or in LangChain metadata, under the same names: the undo that reverses it (compensation, as charge_card declares above), whether it is irreversible, its idempotency key, whether it sends data out (egress) or reads text from outside (reads_outside), the privilege it needs, the sentence its confirmation shows, and the declarations the bug hunter reads (chapter 31). Part I's tool words are these same declarations (chapter 30).

The lint report

stalwart verify lints the catalogue on your machine and prints the report beside its findings. The shop's warnings:

Lint What registration says (as printed)
write_without_compensation a write neither compensated nor marked irreversible
write_not_retry_safe a write with no idempotency, so an ambiguous failure goes to a person
write_without_result_schema a write with no result schema, so its outcome could not be reported
compensation_not_retry_safe '<undo>' undoes it and is not retry-safe

A warning is advice. An error names a tool the reader cannot route safely: two tools it cannot tell apart, naming both -- a turn meant for one could reach the other -- or a name it reads as grammar. Fix it in the tool's definition and import again.

Versions

The version in force is the file: keep it in your repository beside agent.yaml, and its history is the catalogue's. A plan and a check read the catalogue they are given, so an older one is restored by checking the file out again.

Withdrawn tools

Withdrawing a tool stops your agents calling it: list it under withdrawn: in agent.yaml, or in gateway.yaml for the agents behind a gateway (chapter 48). A withdrawn tool is refused before anything else and never listed to the model, until the file no longer names it. The list is read where the agent or the gateway runs: nothing is fetched to keep it.

TypeScript

npm install --save-dev @stalwart-agents/catalogue
npx tsc --declaration --emitDeclarationOnly --outDir dist
stalwart catalogue import ./server --declarations ./server/dist --out catalogue.json

@stalwart-agents/catalogue reads the declarations your own compiler writes -- each exported function one tool, its parameters the arguments, its return type the result -- and runs nothing. stalwart catalogue import then runs the code checks over the project's sources on your machine: the code never leaves your machine, and only the counts are sent to Stalwart. A finding, or a function that says neither @read nor @write, fails the import: the command exits 1. stalwart catalogue import openapi.yaml reads an OpenAPI document the same way, with no code to check.

The limits

Chapter 20's limits hold here, and these besides:

  • A catalogue says what your tools declare. A tool that writes and declares itself a read is read as one: marking reads is your claim, and the check confirms only what it is told writes.
  • A lint reads the declarations, not the tools: a compensation declared is one the check will call, whether or not it undoes anything.