Advanced: support agent over your own docs (ADK)
An agent that answers from your documentation is the most-requested thing on any platform, and the naive build — embed everything, search, stuff the top five into the prompt — produces an assistant that is confidently wrong just often enough to be unusable.
This guide builds the version that works, and the difference is one measured number.
Source: demo/cody-support-agent
and demo/doc-search-mcp.
What you need
- A project,
platformctl login, and documents in a bucket. - The ingestion pipeline already running — this guide consumes what it writes.
- About 30 minutes.
Why retrieval lives in an MCP server, not in a tool
This is the decision everything else follows from.
Tool code is customer code, so it runs in an isolated sandbox with a stripped environment and no egress. That is deliberate — it is what makes running model-written code safe.
It also means anything that must reach a real service cannot be an agent tool. Retrieval needs VectorDB and the reranker, so it belongs in an MCP server, which has credentials and a network.
The wiring is one call:
from crusoe_adk import foundry_model, mcp_toolsets
root_agent = Agent(
name='cody_support_agent',
model=foundry_model(),
tools=mcp_toolsets(names='doc-search-mcp'),
instruction=INSTRUCTION,
)
The platform injects MCP_SERVERS — the project's ready servers with a
per-server bearer it mints — and mcp_toolsets() turns that into ADK toolsets.
Named, not all
mcp_toolsets() # every attached server
mcp_toolsets(names='doc-search-mcp') # just this one
mcp_toolsets(names=['docs', 'weather']) # both, callable in one turn
The default attaches everything, which is right for a general assistant: publishing a new tool to an attached server then needs no agent change. A support assistant names its server, because it should not silently gain whatever tools a colleague deploys into the same project — a widened toolset changes what the model decides to call.
A name that is not attached raises UnknownMCPServer and lists what is. That is
better than quietly attaching nothing: an agent that starts healthy with no
tools answers from the model alone, and you discover that by reading answers,
not logs.
Three atomic tools, not one pipeline
The server publishes three separate tools:
| Tool | What it does |
|---|---|
search_docs | Vector search — returns candidates |
rerank_passages | Scores those candidates against the query |
fetch_document | Reassembles a whole article from its chunks |
Rather than one answer_question call that does all three.
The model decides how far to go. Most questions stop after the rerank; only
one that needs surrounding context pays for fetch_document. It also makes each
stage reusable — an agent that wants candidates without a reranker calls one
tool, instead of someone writing a second copy of the pipeline.
The cost is round trips: three tool calls where one did. That is the trade, and it is worth stating plainly rather than pretending atomic tools are free.
The number that makes the case
Measured on this platform's own documentation, asking "How do I stop the first request being slow?"
Vector search put the right passage on top by a hair:
| Rank | Passage | Similarity |
|---|---|---|
| 1 | support/troubleshooting.md#33 | 0.6288 |
| 2 | agents/troubleshooting.md#22 | 0.5780 |
After reranking, the same ten candidates:
| Rank | Passage | Relevance |
|---|---|---|
| 1 | agents/troubleshooting.md#22 | 0.9977 |
| 2 | support/troubleshooting.md#33 | 0.9974 |
| 3 | agents/invoke.md#24 | 0.9689 |
| 4 | mcp-servers/connect-agents-and-clients.md#30 | 0.9208 |
| 5 | serverless/overview.md#6 | 0.3621 |
| 6 | agents/frameworks/adk.md#41 | 0.2393 |
| 7 | console/quotas-and-audit.md#19 | 0.0051 |
| 8-10 | three unrelated passages | ~0.0000 |
Look at what changed. The ordering barely moved — vector search already had a right answer on top. What changed is the shape of the scores.
Vector similarity ran 0.6288 down to about 0.53 across ten passages: a gentle slope with no natural cut, and 0.6288 against 0.5780 is a coin toss you cannot threshold. After reranking there are four passages above 0.92 and then a cliff to 0.3621. Four passages really are about cold starts; the rest are not; and the reranker says so unambiguously.
That cliff is what lets the agent decide whether it found anything at all. Without it, "no good answer" and "a good answer" look the same, and the model fills the gap by inventing one. The reranker does not find better passages; it tells you whether the passages you found are any good.
These were measured on one corpus with one question. A different corpus gives different absolute scores. What should reproduce is the shape: a shallow similarity slope with no obvious cut, and a reranked distribution with a clear break in it.
The instruction is XML-tagged
INSTRUCTION = """
<role>
You are Cody, the Crusoe AI Platform support assistant.
</role>
...
"""
The model behind foundry_model() follows tagged sections more reliably than one
long paragraph. The tags are structure, not magic — each block is a separate
rule the model can hold onto.
Deploy it
The MCP server first; the agent attaches to it at startup.
platformctl mcp create doc-search-mcp
# One tool per call: each has its own handler, description and credential keys.
platformctl mcp tools set doc-search-mcp search_docs \
--handler @demo/doc-search-mcp/search_docs.py \
--description 'Vector search over the indexed documentation' \
--credential-key pipeline-key
platformctl mcp tools set doc-search-mcp rerank_passages \
--handler @demo/doc-search-mcp/rerank_passages.py \
--description 'Score candidate passages against the question'
platformctl mcp tools set doc-search-mcp fetch_document \
--handler @demo/doc-search-mcp/fetch_document.py \
--description 'Reassemble a whole document from its indexed chunks'
platformctl deploy ./demo/cody-support-agent \
--name cody-support-agent --framework adk
The platform builds the parameter schema MCP clients see from your handler's
signature and docstring. --schema exists for pinning one deliberately; leave it
unset and the docstring you already wrote becomes the contract.
--credential-key names a secret the tool may fetch at call time, with a
short-lived token. Nothing is stored on the server.
Then ask it something:
platformctl invoke cody-support-agent 'How do I stop the first request being slow?'
search_docs: OK -> 10 candidates, top vectordb.md#2 @ 0.5468
rerank_passages: OK -> top vectordb.md#2 @ 0.9489
The numbers to compare against
| What | Number |
|---|---|
| Warm turn: search + rerank + answer | ~30 s |
| Of which, the two tool calls | ~2-4 s |
| Cold start, first invoke after idle | 20 s+, and it may time out |
Most of that 30 seconds is the model, not retrieval. Three atomic tools mean three model turns — decide to search, decide to rerank, then write the answer — and each turn re-reads the passages it has gathered. That is the cost of letting the model choose how far to go, and it is worth knowing before you copy the pattern into something latency-sensitive.
Cold start plus the model's turn can exceed the default 60-second request timeout on a large corpus. That is not a failure of the agent. Either invoke once to warm it, or raise the budget:
platformctl agents config set cody-support-agent --timeout 180
Keeping the corpus fresh
The ingestion pipeline fires on a bucket drop, so updating the documentation updates what the agent knows — with no agent change and no redeploy.
Nothing happens instantly. Drop a file, wait a minute, then ask.
Traps, at the point you hit them
| Trap | What you see | Why |
|---|---|---|
| Retrieval written as an agent tool | Connection timeouts inside the tool | Tool code has no egress. Retrieval belongs in an MCP server. |
mcp_toolsets() with no name | The agent gains a colleague's tools | The default attaches every server in the project. Name yours. |
| A name that is not attached | UnknownMCPServer at startup | Deliberate — better than a healthy agent with no tools. |
One combined answer_question tool | You always pay for all three stages | Atomic tools let the model stop after the rerank. |
| No reranking | Confident wrong answers | 0.5468 vs 0.4262 cannot be thresholded. |
| Timeout on the first invoke | 504 | Cold start plus a model turn. Warm it, or raise --timeout. |
| Documents updated, answers stale | The agent quotes the old text | The poller takes up to 60 s, and the ingest must succeed. Check its logs. |
Teardown
Agent first — it attaches to the server:
platformctl delete cody-support-agent
platformctl mcp delete doc-search-mcp
The ingestion pipeline and its index have their own teardown, in the Python guide.
What it costs to leave running. The agent scales to zero. The MCP server does not by default — it is set to a minimum of one instance so that consumers outside the platform can reach it without a cold start, so it runs continuously and is the standing cost here. The VectorDB index holds storage for as long as it exists.
Where next
- Document ingestion pipeline (Python) — what fills the corpus.
- Multi-step research agent (LangGraph) — planning before gathering.
- MCP servers — publishing tools of your own.