Skip to main content

Agent + Vectors + Secrets integration

This integration is the safest way to give an Agent a private knowledge base. Vectors stores the searchable embeddings, Secrets keeps the retrieval credential private, and an MCP server is where the search actually runs.

What you get

  • Private retrieval from your own documents.
  • Credential separation so the deployed artifact does not need to embed a key.
  • Controlled answers with grounded sources and explicit refusal when the corpus cannot answer.

The pieces

  • Vectors — stores the embeddings and returns nearest matches.
  • MCP server — runs the retrieval tool: embed the question, query the index, hand back passages.
  • Secrets — holds the credential that tool fetches at call time.
  • Agent — attaches the MCP server, decides when to search, and writes the user-facing answer.
Retrieval cannot live in an agent's own tool

An agent's own tools do not run in the agent process. Each call is shipped to a one-use sandbox built from the agent's image with the environment stripped: no vector database URL, no credential, and no egress. Tool code is your code, and that is the boundary.

So a retrieval tool written as an agent tool deploys cleanly and then fails on its first call — and the model answers from memory instead, which reads as a prompting problem rather than a networking one. An MCP server has both a credential and a network, which is why the retrieval tool goes there.

Step 1: Load documents into Vectors

Load your documents with the embedder you intend to query with. The embedding model must match at query time, or the vectors will not line up.

The platform's embedding model, qwen-embedding, decides the width. Leave dimensions unset and the index is created at exactly that width, in the API, the CLI and the console alike — so embedding through POST /v1/embeddings and writing the result fits with nothing configured.

Set a width explicitly only when your vectors come from a different model. Dimensions are fixed at creation — the API refuses a change with dimensions is immutable: existing points cannot be re-embedded, so a different width is a new index, not an edit — so the only repair for a mismatch is deleting the index and re-embedding the whole corpus.

platformctl vectordb create handbook --dimensions 4096 --distance cosine
platformctl vectordb upsert handbook --points @handbook.json

--points takes JSON — a literal, @file, or - for stdin — and the whole argument has to parse, so a newline-delimited .jsonl file is refused before any request goes out: --points is not valid JSON (pass a literal, @file, or - for stdin). The file is either a bare array of points or the full request object with a "points" key, which is the same body the REST call takes, so one file serves both tabs.

Each point is {"id": ..., "vector": [...], "payload": {...}}. Omit id and the server mints a UUID, which is what you want for chunks with no natural key. Every vector must be exactly as wide as the index; one that is not names itself and both widths — point 3 has 1024 dimensions; index "handbook" expects 4096.

Step 2: Keep the retrieval credential in Secrets

The credential that lets the tool query the vector index belongs in Secrets, not in the source.

It has to be a real credential. A workload's own identity, CAI_PROJECT_KEY, is deliberately refused by project APIs like VectorDB; its one power is minting the short-lived token that crusoe.secret() reads through. So create a service account, mint a key, and store it as a project secret:

printf %s "$SERVICE_ACCOUNT_KEY" | platformctl secrets put handbook-search-key

There is no --value flag, on purpose: a credential passed as an argument lands in shell history and in the process table. Read it from a pipe, or from a file with --value-file.

Then declare the key on the tool, so the server records what it may fetch, and read it inside the function rather than at import — the value is resolved per invocation against a token minted for that call, so a rotation takes effect without a rebuild and nothing is stored on the server.

Step 3: Put the retrieval tool in an MCP server

The handler embeds the question, queries the index, and returns passages. Its environment is injected: EMBED_BASE_URL (already ending in /v1), EMBED_MODEL, CAI_VECTORDB_URL, CAI_PROJECT_ID and CAI_PROJECT_KEY.

import json, os, urllib.request

import crusoe_mcp as crusoe

INDEX = "handbook"
SECRET = "handbook-search-key"


def _post(url, body, token):
req = urllib.request.Request(
url,
data=json.dumps(body).encode("utf-8"),
headers={"Content-Type": "application/json",
"Authorization": "Bearer " + token},
method="POST")
with urllib.request.urlopen(req, timeout=45) as resp:
return json.loads(resp.read().decode("utf-8") or "{}")


@crusoe.tool(credential_keys=[SECRET])
def search_handbook(question: str, top_k: int = 8) -> dict:
"""Search the company handbook for passages answering a question.

Args:
question: The user's question, in their own words.
top_k: How many passages to return.
"""
embed_base = os.environ["EMBED_BASE_URL"].rstrip("/")
vectordb = os.environ["CAI_VECTORDB_URL"].rstrip("/")
project = os.environ["CAI_PROJECT_ID"]

# Inference accepts the workload's own key. The VectorDB call below does not.
vector = _post(embed_base + "/embeddings",
{"model": os.environ.get("EMBED_MODEL", "qwen-embedding"),
"input": [question]},
os.environ["CAI_PROJECT_KEY"])["data"][0]["embedding"]

key = crusoe.secret(SECRET) # per call, with a short-lived token
hits = _post("%s/v1/projects/%s/indexes/%s:query" % (vectordb, project, INDEX),
{"vector": vector, "top_k": top_k}, key).get("results", [])
if not hits:
return {"passages": [], "note": "nothing in the handbook matched that question"}
return {"passages": [{"source": hit["payload"].get("source"),
"score": round(hit["score"], 4),
"text": hit["payload"].get("text", "")} for hit in hits]}

The docstring is not decoration: the platform builds the schema MCP clients see from the signature and the Args: block.

Publish it, declaring the secret the tool may fetch:

platformctl mcp create handbook-search
platformctl mcp tools set handbook-search search_handbook \
--handler @search_handbook.py \
--description "Search the company handbook" \
--credential-key handbook-search-key

--credential-key is repeatable, and leaving it unset keeps the tool's current keys rather than clearing them.

Step 4: Attach the server to the agent

from google.adk.agents import Agent

from crusoe_adk.foundry import foundry_model
from crusoe_adk.mcp import mcp_toolsets

root_agent = Agent(
name="handbook_agent",
model=foundry_model(),
instruction=("Call search_handbook before answering.\n\n"
"Answer only from the passages it returns, name the source file, "
"and say the handbook does not cover it when they do not support an answer."),
tools=[*mcp_toolsets(names="handbook-search")],
)

names= narrows the attachment to the servers you name — a string, or a list of them. Leave it out and the agent attaches every Ready MCP server in the project, which is usually right; name it when the agent must not have its toolset widened the day somebody else deploys a server. A name that is not attached raises crusoe_core.UnknownMCPServer listing what is attached, rather than quietly attaching nothing.

Order matters. The platform bakes the project's Ready MCP servers into the agent's revision at deploy time as MCP_SERVERS, so publish the tool and wait for the server to go Ready (platformctl mcp get handbook-search) before you deploy — or redeploy — the agent.

Example behavior

  • Question: "What is our laptop refresh policy?"
  • Retrieval: search_handbook returns the nearest passages from the handbook index.
  • Answer: grounded in the laptops.md chunk, with the source file named.
  • No hallucination when the corpus lacks the answer.

Why this pattern holds up

It keeps four boundaries clean:

  • Storage stays in Vectors.
  • Credentials stay in Secrets, fetched per call and never at rest in the server.
  • Retrieval stays in the MCP server, the one place with both a credential and a network.
  • Reasoning stays in the Agent.

That separation is what makes the system auditable and rollback-friendly: the tool is versioned independently of the agent, so a bad retrieval change rolls back without touching the prompt.

Next steps