Integration examples
The framework guides cover one agent at a time. Real workloads combine an agent with the other services: Vectors for document retrieval, Memory Store for shared state, Secrets for custom credentials, and MCP Servers for tools shared between agents. Each example below is complete enough to adapt — a working shape, the configuration it needs, and the links to the full walkthroughs.
Agent + Vectors: RAG over your own documents
What it is. The built-in memory bank remembers conversations. This pattern retrieves from your documents instead: you create a Vectors index, load it with chunks of your content, and a tool embeds the user's question and queries that index. Retrieval-augmented generation, in one tool call.
An agent's own tools do not run in the agent. Each call is shipped to a one-use sandbox built from your agent's image with the environment stripped — PYTHONUNBUFFERED and PORT, nothing else — and with egress that excludes every private range, the platform's own internal addresses included.
So a retrieval tool written as an agent tool deploys cleanly and then fails on its first call, quietly. A module-level os.environ["CAI_VECTORDB_URL"] fails at import and reaches the model as ERROR: could not import agent: ... KeyError: 'CAI_VECTORDB_URL'. Moving that read inside the function does not fix it — the sandbox's environment is empty either way, so it only becomes ERROR: 'CAI_VECTORDB_URL' at call time. An internal platform address simply times out. All three arrive as ordinary tool results, so the model answers from its own knowledge and the symptom reads like a prompting problem rather than a networking one.
Publish the tool on an MCP server instead: that container is given the addresses and the credential and the egress. The only other route, if the tool must stay in the agent, is platformctl secrets set handbook-bot TOOL_SANDBOX=false — a per-agent secret, because the env route refuses that name — which runs your tool code next to MODEL_API_KEY.
Two variables that look alike. The platform-injected CRUSOE_VECTORDB_URL is the memory bank's raw storage address and will not answer index queries. CAI_VECTORDB_URL is the Vectors REST API at its internal address, injected into every workload and admitted by name in the project's egress policy. Read that variable rather than writing an address down, and do not reach for the public API hostname from inside a project: it resolves, but the traffic is rewritten to an internal address the egress policy denies, so the call hangs until the request budget expires.
The credential has to be a real one. A workload's own CAI_PROJECT_KEY is deliberately refused by project APIs such as Vectors; its one power is minting the short-lived token crusoe.secret() reads through. Create a service account, mint a key, and store it as a project secret the tool declares:
printf %s "$SERVICE_ACCOUNT_KEY" | platformctl secrets put handbook-search-key
The tool, published on an MCP server. 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
import os
import urllib.request
import crusoe_mcp as crusoe
INDEX = "handbook"
SECRET = "handbook-search-key"
def _post(url, payload, token):
request = urllib.request.Request(
url,
data=json.dumps(payload).encode(),
headers={"Content-Type": "application/json",
"Authorization": "Bearer " + token},
method="POST",
)
with urllib.request.urlopen(request, timeout=45) as response:
return json.loads(response.read().decode() or "{}")
@crusoe.tool(credential_keys=[SECRET])
def search_handbook(question: str, top_k: int = 3) -> dict:
"""Search the company handbook for passages that answer a question.
Args:
question: The question to look up, in plain English.
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 Vectors 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) # fetched 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, "with_payload": True},
key).get("results", [])
if not hits:
return {"passages": [], "note": "nothing in the handbook matched that question"}
return {"passages": [{"source": hit.get("payload", {}).get("source", "handbook"),
"text": hit.get("payload", {}).get("text", "")} for hit in hits]}
Publish it, then wait for the server before you deploy any agent against it:
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
platformctl mcp get handbook-search # wait until state is ready
The agent that attaches it. No tool code, no credential, no address — the agent decides when to search and writes the answer:
- ADK
- LangGraph
- CrewAI
from google.adk.agents import Agent
from crusoe_adk import foundry_model, mcp_toolsets
root_agent = Agent(
name="handbook_bot",
model=foundry_model(),
instruction=(
"Always call search_handbook first, and answer only from the passages "
"it returns. If they do not contain the answer, say so plainly."
),
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=(
"Answer questions about the company handbook. Always call search_handbook "
"first and answer only from its passages."
),
)
from crewai import Agent, Crew, Process, Task
import crusoe_crewai as crusoe
handbook_bot = Agent(
role="Handbook Bot",
goal="Answer questions about the company handbook from retrieved passages.",
backstory="A concise assistant running on the Crusoe AI Platform.",
llm=crusoe.foundry_model(),
tools=crusoe.mcp_tools(names="handbook-search"),
verbose=False,
)
respond = Task(
description=(
"Prior conversation (may be empty):\n{history}\n\n"
"Respond to:\n{message}\n\n"
"Always call search_handbook first and answer only from its passages."
),
expected_output="An answer grounded in the retrieved handbook passages.",
agent=handbook_bot,
)
crew = Crew(agents=[handbook_bot], tasks=[respond], process=Process.sequential, verbose=False)
names="handbook-search" narrows the attachment to that one server. Leave it out and the agent attaches every ready MCP server the project has, which is usually what you want. A name that is not attached raises crusoe_core.UnknownMCPServer listing what is attached, rather than quietly attaching nothing.
Configuration. There is none on the agent: no env vars, no secrets, no addresses. MCP_SERVERS is written into the agent's revision at deploy, so the only ordering rule is the one above — the server must be ready before you deploy the agent, or the agent starts cleanly with no tools and says nothing about it.
End to end. Creating the index, chunking documents, upserting vectors, and handling every error this pattern produces is a full tutorial of its own: RAG chatbot. One thing to know before you create the index: dimensions are fixed at creation. Omit dimensions and the index is sized for the platform's own qwen-embedding model, which is what you want when you embed through POST /v1/embeddings — see Agent + Vectors + Secrets. The boundary between your own indexes and the built-in memory bank is use VectorDB with agents.
Agent + Memory Store: sessions are automatic — cache your own data
What it is. Conversation history needs no integration: the harness keeps every session in the platform-managed store, and turn 2 replays turn 1 with nothing to configure, in any framework. That is the sessions guide, and it is separate from your application Memory Store instances. Sessions live in a platform-managed shared store, with Memory Store's wire protocol running underneath.
What does need an integration is your agent's own application data: cached API results, counters that span invocations, state shared between an agent and another service. That is a Memory Store instance you create.
The shape. Create the instance with its TLS endpoint turned on (create-time only), stash its password in Secrets, and connect from the agent with a standard client library. The platform-injected CRUSOE_MEMORYSTORE_ADDR points at the shared session store, not your instance — treating it as your store is the classic mistake, and the reason your own connection must be explicit.
The tool below is sandboxed by default, and a sandboxed tool has neither environment nor a route to the secrets API: os.environ["CACHE_HOST"] raises KeyError there and secret() raises SecretError: crusoe.secret is not configured. Run this agent with platformctl secrets set lookup-bot TOOL_SANDBOX=false, or move the cache lookup into an MCP server. The sandbox is also cut off from RFC1918 and the platform's own internal ranges, so check that your instance's endpoint is reachable from wherever the tool ends up running:
import os
from google.adk.agents import Agent
from crusoe_adk.foundry import foundry_model
from crusoe_adk import secret
import redis # standard client library for the Memory Store wire protocol
_cache = None
def _client():
global _cache
if _cache is None:
_cache = redis.Redis(
host=os.environ["CACHE_HOST"], # your instance's TLS endpoint
port=int(os.environ.get("CACHE_PORT", "6379")),
password=secret("cache-password"), # read at call time, so a rotation needs no redeploy
ssl=True,
decode_responses=True,
)
return _cache
def cached_lookup(key: str) -> str:
"""Look up a key in the shared cache, computing it cheaply on a miss."""
hit = _client().get(f"lookup:{key}")
if hit is not None:
return hit
value = f"computed:{key}" # real work goes here
_client().setex(f"lookup:{key}", 3600, value)
return value
root_agent = Agent(
name="lookup_bot",
model=foundry_model(),
instruction="Use cached_lookup so repeated questions never redo work.",
tools=[cached_lookup],
)
Choose instance settings for the job: allkeys-lru with persistence off for caches, noeviction with persistence on for counters and queues. The decision table, and why agent sessions do not appear in any instance you list: Memory Store with agents. The connect walkthrough itself: connect from workloads.
Agent + Secrets: custom LLM keys and downstream credentials
What it is. Four layering levels, from broadest to narrowest:
- The project's model key — one inference credential every agent inherits, saved once. Deploying a model-calling agent is refused without it. Console Project Settings, or
PUT /v1/projects/{id}/inference. - A per-agent override — the secret
MODEL_API_KEYon the agent replaces the project key for that one agent: separate bill, separate quota. - A per-agent secret for anything else —
platformctl secrets set my-agent DEMO_TOKEN=abc123, read withos.environ["DEMO_TOKEN"]. Write-only: you can list names, never values. - A project Secret read at call time — the Secrets store, for values shared across agents, kept with versions, and audited on read. The frameworks reach it identically:
- ADK
- LangGraph
- CrewAI
from crusoe_adk import secret
api_key = secret("weather-api-key") # latest version
pinned = secret("weather-api-key", 3) # a pinned version
import crusoe_langchain as crusoe
api_key = crusoe.secret("weather-api-key")
pinned = crusoe.secret("weather-api-key", 3)
import crusoe_crewai as crusoe
api_key = crusoe.secret("weather-api-key")
pinned = crusoe.secret("weather-api-key", 3)
Call-time reads keep the value out of the image and let a rotation reach every agent without a redeploy. What they do not do is work inside a sandboxed tool: that sandbox has no environment and no route to the secrets API, so crusoe.secret() raises SecretError: crusoe.secret is not configured there. A tool that needs a credential belongs in an MCP server, or the agent must run with TOOL_SANDBOX=false. Bring-your-own model providers work the same way — declare the model natively in the framework and keep its credential in the store.
Full mechanics, key naming rules, the reserved names list, and reserved-name traps: secrets and environment variables and use secrets in workloads.
Agent + MCP Servers: shared tools across agents
What it is. When two or more agents need the same capability — a weather lookup, a customer record fetch — bake it into none of them. Publish it once as an MCP Server (a workload that serves tools over the network), and attach it to every agent that needs it. One server update reaches all of them on their next deploys, and the same server is also callable from clients off the platform.
The shape, per framework. Each one is a one-liner at import time; all three resolve the project's attached servers and their per-server tokens from platform-injected configuration:
- ADK
- LangGraph
- CrewAI
from google.adk.agents import Agent
from crusoe_adk import foundry_model, mcp_toolsets
from crusoe_adk.tools import run_python
root_agent = Agent(
name="adk_mcp",
model=foundry_model(),
instruction="Prefer an attached tool when one fits; use run_python for computation.",
tools=[run_python, *mcp_toolsets()],
)
import asyncio
from langgraph.prebuilt import create_react_agent
import crusoe_langchain as crusoe
_mcp_tools = asyncio.run(crusoe.mcp_tools()) # resolve once, at import
graph = create_react_agent(
crusoe.foundry_model(),
tools=[crusoe.run_python, *_mcp_tools],
prompt="Prefer an attached tool when one fits; use run_python for computation.",
)
mcp_tools() is async and the tools must exist before the graph compiles — the asyncio.run at import is deliberate and safe there.
from crewai import Agent
import crusoe_crewai as crusoe
assistant = Agent(
role="Assistant",
goal="Answer the user's request, preferring attached tools when they fit.",
backstory="An assistant on the Crusoe AI Platform.",
llm=crusoe.foundry_model(),
tools=[crusoe.RunPython()] + crusoe.mcp_tools(),
verbose=False,
)
Properties to plan around:
- Fail-closed at startup — on two frameworks of three. CrewAI's
mcp_tools()and LangGraph'sawait mcp_tools()connect eagerly, so an unreachable or unauthorized server crashes the agent loudly at startup rather than silently dropping its tools. ADK'smcp_toolsets()only builds toolset objects and connects lazily, so on ADK the failure surfaces on the first tool call instead; what fails at ADK startup is a base image missing ADK'smcpextra. With no servers attached, all three helpers return empty and the agent runs fine. MCP_SERVERSis captured at deploy. The platform writes it into the agent's revision from the project'sreadyservers at the moment you deploy. A server that becomesreadyafterwards does not reach that agent until you redeploy it, and the agent says nothing about the tools it does not have. Build the server first, wait forplatformctl mcp get <server>to readready, then deploy the agent.names=narrows the attachment. All three helpers takenames="one-server"ornames=["a", "b"]; the default attaches everyreadyserver in the project. A name that is not attached raisescrusoe_core.UnknownMCPServer, listing what is attached.- MCP tools are never sandboxed, and that is correct. The tool runs on the server; what sits in your agent is only the client that calls it. The trade-off: that client code holds your credentials, and a compromised server talks to it — attach servers you control.
- Verified call path. A call to a server tool shows up in
tool_callsexactly like a built-in tool, and in the transcript'sfunction_callparts — you verify attachment the same way you verify any other tool, plus the server's own logs.
End to end: publish tools builds the server, connect agents and clients wires it to agents, and the MCP weather tutorial runs the whole loop.
Tie them together
These compose cleanly because they meet at the agent, not at each other. A support-bot, for instance: search_handbook (Vectors) for the policy answers, your Memory Store instance caching ticket-status lookups, the ticketing system's token read from Secrets at call time, and the shared refund_calculator MCP Server attached rather than copied. The framework guides keep their debugging checklists valid under every combination — a deployed agent that misbehaves in a stack like this is still debugged from platformctl status, then the startup log lines, then the transcript.