Tutorial 2: a keyed engine
What you will have at the end. Two engines answering each search, Wikipedia and Brave Search, with the key kept out of the file.
What you need. Tutorial 1 done, and a Brave Search subscription token.
1. Add the engine
# Tutorial 2: Wikipedia plus Brave Search. The key is read from the
# environment variable named here; an inline key is refused at startup.
config_version = 1
[search]
languages = ["en", "ja"]
[engines.wikipedia]
type = "wikipedia"
categories = ["general", "science"]
preference = 10
[engines.brave]
type = "brave"
categories = ["general", "news"]
preference = 20
api_key_env = "BRAVE_API_KEY"
Three things to notice:
api_key_envnames the environment variable that holds the key.api_key_fileis the alternative: a file whose content is the key. Writing the key itself into the file (api_key = "...") is refused at startup, so a configuration file is safe to share.categoriessays what each engine answers. A search names a category (generalby default); only engines serving it are asked.preferenceorders engines when results tie; lower comes first.
2. Check twice
Without the key:
rypsok check --config rypsok.toml
The Brave line says inactive: environment variable BRAVE_API_KEY is unset or empty, and the exit code is still 0: an engine without its key is
inactive, not an error, so the same file runs on a machine without the
key. Now with it:
export BRAVE_API_KEY=...
rypsok check --config rypsok.toml
The Brave line now ends with credential resolved.
3. Search, and read the engines’ part
Run the pipe of Tutorial 1 again. Two engines answer; a result that both
returned appears once, ranked higher. Ask for the engines explicitly with
engine_status:
{
"request": {
"jsonrpc": "2.0",
"id": 3,
"method": "tools/call",
"params": {
"name": "engine_status",
"arguments": {},
"_meta": {
"io.modelcontextprotocol/protocolVersion": "2026-07-28",
"io.modelcontextprotocol/clientInfo": {
"name": "tutorial",
"version": "1"
},
"io.modelcontextprotocol/clientCapabilities": {}
}
}
},
"response": {
"jsonrpc": "2.0",
"id": 3,
"result": {
"resultType": "complete",
"content": [
{
"type": "text",
"text": "{\"engines\":[{\"id\":\"wikipedia\",\"latency_ms\":null,\"state\":\"closed\"}],\"notices\":[],\"outcome\":\"ok\"}"
}
],
"structuredContent": {
"engines": [
{
"id": "wikipedia",
"latency_ms": null,
"state": "closed"
}
],
"notices": [],
"outcome": "ok"
},
"isError": false
}
}
}
Each engine’s state is closed (in use), open (failing lately and
skipped for a while), half_open (one probe in flight) or disabled
(by configuration or for lack of a key; a disabled engine also has a
reason, disabled or no_credential, which tells the two apart).
latency_ms is the median of its recent requests.
4. When Brave refuses the key
A wrong or expired key gives every search the outcome partial with the
notice {"code": "engine_unavailable", "engine": "brave", "reason": "auth_failure"}, and Wikipedia’s results. The key is read at startup; a
corrected key takes effect at the next restart, and the engine keeps being
asked meanwhile, so no cooldown delays it.
What Brave’s terms mean for you
Brave’s results may be used for inference and must not be stored beyond the call or redistributed; rypsok therefore never caches them, whatever the cache setting. Wikipedia’s results may be cached. Each engine’s record of its provider’s terms is in the maintainers’ part of this book.
Next: Tutorial 3: reading a page.