Tips and Tricks
Practical usage knowledge for agents and operators working with the request MCP tool. This page collects gotchas that are easy to hit once but hard to find documented in one place.
Query craft
Long, keyword-rich queries beat short ones
search and knowledge.search fuse full-text (FTS5 trigram) and vector similarity results with RRF. A short query (one or two words) gives both signals little to work with: FTS5 trigram matching has few n-grams to match against, and the embedding has little context to place in vector space. A longer query that names the language, framework, domain terms, and the shape of the question gives both retrieval legs more to latch onto and produces tighter fusion rankings. Prefer a query built from the same nouns you would use in the document itself over a terse keyword fragment. See Search and Retrieval for how the fused scores are computed.
neighbors and traverse default to both directions
As of the fix for issues #445/#480 (crates/khive-pack-kg/src/handlers/common.rs, parse_direction), omitting direction on neighbors or traverse resolves to Direction::Both, not outgoing-only. An unrecognized direction string (a typo like "inbound") is now a rejected InvalidInput, not silently coerced to any particular direction. This is covered by regression tests in crates/khive-pack-kg/tests/integration.rs asserting that a node reachable only via an incoming edge is still surfaced when direction is omitted.
Some older documentation and ADR prose (predating this fix) describes an out-only default as a live footgun. That description is stale as of this writing. Pass direction="both" explicitly anyway when you want to be unambiguous about intent, or when you specifically want only one direction, pass direction="out" / direction="in":
request(ops="neighbors(node_id=\"<uuid>\", direction=\"in\", relations=[\"extends\"])")
traverse vs context
Both walk the graph, but they answer different question shapes:
traverse(roots=[...], max_depth=N)takes explicit root UUIDs and returns paths reachable via BFS, filtered by relation. Use it when you already have anchor entities and want lineage, dependency chains, or reachability.context(query=..., entity_ids=...)resolves anchors from a natural language query (viahybrid_search), expands 1-2 hops, and assembles the result under a character budget in a single call. Use it when you do not already have anchor UUIDs and want a budgeted neighborhood summary, for example injecting graph context into a model turn without asearch-then-neighborsround trip.
context composes the same runtime ops as search and neighbors (it adds no new storage or index); see ADR-089 for the full parameter and ordering contract. context’s direction also defaults to both, matching neighbors’ current default (see the direction note above).
DSL round-trip tricks
$prev chains only the immediately preceding op
In a chain (v1(...) | v2(...)), $prev resolves to the result of the op directly before it, not to any earlier op in the chain. Each step in a chain overwrites the value $prev will resolve to on the next step. If op C needs a value produced by op A but not passed through by op B, that value is gone by the time C runs. Restructure the chain, or split the calls and pass the value explicitly.
request(ops="create(kind=\"entity\", entity_kind=\"concept\", name=\"NewConcept\") | link(source_id=$prev.id, target_id=\"<existing_id>\", relation=\"extends\")")
Path extraction supports $prev (the whole result), $prev.field (nested object field), $prev.items[0].id (array index into a field), and $prev[2] (top-level array index).
Parallel batches, max 100 ops
request(ops="[search(kind=\"entity\", query=\"LoRA\"), search(kind=\"note\", query=\"LoRA\"), stats()]")
Batches run with no ordering guarantee between ops, and a failed op does not abort the others: each result carries its own ok/error. The batch size ceiling is 100 ops; a larger array is rejected rather than silently truncated.
Create and link in one round trip
Chaining a create into a link avoids the two-call round trip shown in the Prompt Cookbook:
request(ops="create(kind=\"entity\", entity_kind=\"concept\", name=\"GQA\", description=\"Grouped-query attention\") | link(source_id=$prev.id, target_id=\"<existing_paper_id>\", relation=\"introduced_by\")")
This only works when the value you need is produced by the op immediately before the one that consumes it. See the $prev scoping note above.
Param gotchas
A handful of verbs use a parameter name that is easy to guess wrong, and the failure mode differs by pack. The KG pack’s search and comm.thread param structs carry #[serde(deny_unknown_fields)] (crates/khive-pack-kg/src/handlers/params.rs, crates/khive-pack-comm/src/params.rs), so passing a wrong name fails the call outright with an unknown-field error. The knowledge pack’s search, suggest, and delete_atoms param structs (crates/khive-pack-knowledge/src/knowledge/schema.rs) deserialize with plain serde_json::from_value (no deny_unknown_fields), so an extra unrecognized field is silently ignored; what actually fails the call is the required field (query, ids) being missing because you sent the wrong name for it instead:
| Verb | Correct param | Common wrong guess | Failure mode |
|---|---|---|---|
search (KG) | query | q | rejected: unknown field |
comm.thread | id | thread_id | rejected: unknown field |
knowledge.search, knowledge.suggest | query | q | rejected: query missing (required) |
knowledge.delete_atoms | ids | slugs | rejected: ids missing (required) |
String values in the function-call DSL must be double-quoted JSON string literals. The parser reads the raw value slice and feeds it to serde_json::from_str, so a bareword value (an unquoted identifier) fails JSON parsing and the op errors out, even as a standalone argument:
# Wrong: bareword value, fails to parse
request(ops="get(id=abc123)")
# Right: double-quoted string literal
request(ops="get(id=\"abc123\")")
Indexing latency
Writes to the knowledge corpus (knowledge.upsert_atoms) land in SQLite and are visible to knowledge.search’s FTS leg on the next call. The Vamana ANN vector index that provides the semantic leg is different: it is an in-memory, per-namespace structure that is loaded once and cached, and is only invalidated by an explicit knowledge.index() call (or a daemon restart), not automatically after every upsert_atoms. A newly written atom will not surface via the ANN path of knowledge.search until a reindex runs. Treat writes as helping the next indexing pass, not as immediately recallable through the vector leg: batch writes, then call knowledge.index() (or run kkernel reindex) before relying on semantic recall over what you just wrote.
Troubleshooting
request(ops="verbs()")andhelp=trueon any verb are the live ground truth for what is actually registered on the server you are talking to. Prefer them over any cached doc (including this one) when behavior looks off.help=trueshort-circuits before the pack’s handler runs, so it never has side effects.- If an MCP client fails to connect to
kkernel mcpwith an opaque error, see Troubleshooting a connect failure in the configuration guide for the stderr probe that surfaces the real startup error.
See also
- Prompt Cookbook: full verb syntax reference
- Search and Retrieval: scoring, reranking, and decompose
- Configuration: config resolution and connect-failure troubleshooting
- ADR-089:
contextverb design - Proof-Graph Case Study: khive at scale
Raw markdown for this page: /md/tips-and-tricks.md