Skip to main content

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.

An agent's own tools have no network

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:

agent.py
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:

ToolWhat it does
search_docsVector search — returns candidates
rerank_passagesScores those candidates against the query
fetch_documentReassembles 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:

RankPassageSimilarity
1support/troubleshooting.md#330.6288
2agents/troubleshooting.md#220.5780

After reranking, the same ten candidates:

RankPassageRelevance
1agents/troubleshooting.md#220.9977
2support/troubleshooting.md#330.9974
3agents/invoke.md#240.9689
4mcp-servers/connect-agents-and-clients.md#300.9208
5serverless/overview.md#60.3621
6agents/frameworks/adk.md#410.2393
7console/quotas-and-audit.md#190.0051
8-10three 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.

Your numbers will differ, and the shape is the point

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

agent.py
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 schema is inferred, not written

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

WhatNumber
Warm turn: search + rerank + answer~30 s
Of which, the two tool calls~2-4 s
Cold start, first invoke after idle20 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.

The first invoke after a deploy is slow

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.

The poller runs every 60 seconds

Nothing happens instantly. Drop a file, wait a minute, then ask.

Traps, at the point you hit them

TrapWhat you seeWhy
Retrieval written as an agent toolConnection timeouts inside the toolTool code has no egress. Retrieval belongs in an MCP server.
mcp_toolsets() with no nameThe agent gains a colleague's toolsThe default attaches every server in the project. Name yours.
A name that is not attachedUnknownMCPServer at startupDeliberate — better than a healthy agent with no tools.
One combined answer_question toolYou always pay for all three stagesAtomic tools let the model stop after the rerank.
No rerankingConfident wrong answers0.5468 vs 0.4262 cannot be thresholded.
Timeout on the first invoke504Cold start plus a model turn. Warm it, or raise --timeout.
Documents updated, answers staleThe agent quotes the old textThe 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