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=...)instalwart_agents.langchainmakes into a catalogue in your process (chapter 27); - TypeScript: the exported functions of a TypeScript project, as the
@stalwart-agents/cataloguepackage 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(orread_onlyin_meta); a tool markeddestructiveHintis a write whatever else it says; an unmarked tool is a write --archive_orderabove. An unmarked tool costs a confirmation, never a wrong call; - OpenAPI:
GET,HEADandOPTIONSare reads, and every other method a write, unless the operation saysx-stalwart-read: true-- a search sent as aPOST, likesearch_ordersabove; - TypeScript: each function says
@reador@writein its doc comment. One that says neither is not guessed at all: the import reportstool_kind_undeclaredfor 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.