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.
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
- curl
- Console
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.
curl -sX POST "$VDB/v1/projects/$CAI_PROJECT/indexes" \
-H "Authorization: Bearer ***" \
-H 'Content-Type: application/json' \
-d '{"name":"handbook","dimensions":4096,"distance":"cosine"}'
curl -sX POST "$VDB/v1/projects/$CAI_PROJECT/indexes/handbook:upsert" \
-H "Authorization: Bearer ***" \
-H 'Content-Type: application/json' \
-d @handbook.json
handbook.json is {"points":[{"vector":[…],"payload":{"text":"…","source":"pto.md"}}]} — a JSON object, not JSONL. id is optional; omit it and the server mints a UUID.
- Open Data services → VectorDB.
- Create index and name it. Leave Dimensions blank — the placeholder shows the width of the platform's own embedding model, which is what you want here. Leave Distance on cosine.
- Points are written from your own code or the CLI. The console has no upsert form on purpose; the read side is what it is for. Use its Query panel and Data browser to confirm the index answers, and to delete a single bad point.
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
- Console
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.
- Open Compute → MCP servers and Deploy server, named
handbook-search. - Publish tool, with the handler above and
handbook-search-keyunder Credential keys. - The tool appears under Tools, and what it may fetch under Credential keys, once the build deploys.
Step 4: Attach the server to the agent
- ADK
- LangGraph
- CrewAI
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")],
)
import asyncio
from langgraph.prebuilt import create_react_agent
import crusoe_langchain as crusoe
_tools = asyncio.run(crusoe.mcp_tools(names="handbook-search"))
graph = create_react_agent(
crusoe.foundry_model(),
tools=_tools,
prompt=("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."),
)
from crewai import Agent, Crew, Process
import crusoe_crewai as crusoe
assistant = Agent(
role="Handbook Assistant",
goal="Answer handbook questions from retrieved passages, or say the handbook does not cover it.",
backstory="A concise assistant with one retrieval tool.",
llm=crusoe.foundry_model(),
tools=crusoe.mcp_tools(names="handbook-search"),
verbose=False,
)
crew = Crew(agents=[assistant], tasks=[], process=Process.sequential)
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_handbookreturns the nearest passages from the handbook index. - Answer: grounded in the
laptops.mdchunk, 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.