Skip to main content

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.

The retrieval tool cannot live in the agent

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 strippedPYTHONUNBUFFERED 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:

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")],
)

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:

  1. 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.
  2. A per-agent override — the secret MODEL_API_KEY on the agent replaces the project key for that one agent: separate bill, separate quota.
  3. A per-agent secret for anything elseplatformctl secrets set my-agent DEMO_TOKEN=abc123, read with os.environ["DEMO_TOKEN"]. Write-only: you can list names, never values.
  4. 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:
from crusoe_adk import secret

api_key = secret("weather-api-key") # latest version
pinned = secret("weather-api-key", 3) # a pinned version

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:

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()],
)

Properties to plan around:

  • Fail-closed at startup — on two frameworks of three. CrewAI's mcp_tools() and LangGraph's await mcp_tools() connect eagerly, so an unreachable or unauthorized server crashes the agent loudly at startup rather than silently dropping its tools. ADK's mcp_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's mcp extra. With no servers attached, all three helpers return empty and the agent runs fine.
  • MCP_SERVERS is captured at deploy. The platform writes it into the agent's revision from the project's ready servers at the moment you deploy. A server that becomes ready afterwards 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 for platformctl mcp get <server> to read ready, then deploy the agent.
  • names= narrows the attachment. All three helpers take names="one-server" or names=["a", "b"]; the default attaches every ready server in the project. A name that is not attached raises crusoe_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_calls exactly like a built-in tool, and in the transcript's function_call parts — 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.