Skip to main content

Connect agents and clients

Your MCP server is built and ready. This page shows you where its endpoint lives, what the per-server token is and why you never see it, and how to call the server's tools from an agent running on the platform or from an MCP client anywhere else.

Before you begin

  • You need a project member role to read a server's endpoint.
  • You need a server whose status is ready. A server that is still pending, building, or deploying has no endpoint yet. See publish tools.
  • There is no public API hostname yet in this alpha. Use the endpoint your administrator gives you, or a port-forward.

Sign in and capture a token and your project id:

export CAI_API=http://localhost:8080 # your admin-provided endpoint or port-forward
export TOK=$(curl -s "$CAI_API/v1/auth/login" \
-d '{"email":"you@example.com","password":"your-password"}' | jq -r .token)
export PROJ=$(curl -s -H "Authorization: Bearer $TOK" "$CAI_API/v1/projects" | jq -r '.projects[0].id')

Find the endpoint

The server object carries a url field once the platform has deployed it:

curl -s "$CAI_API/v1/projects/$PROJ/mcpservers/weather-tools" \
-H "Authorization: Bearer $TOK" | jq '.mcp_server | {status, url, version}'

You should see:

{
"status": "ready",
"url": "http://mcp-weather-tools.cai-p-ab12cd.svc.cluster.local",
"version": 1
}

The address a client actually posts to is that URL with /mcp on the end. The MCP protocol is mounted at /mcp; the bare URL is the service, not the protocol endpoint.

http://mcp-weather-tools.cai-p-ab12cd.svc.cluster.local/mcp

If url is empty, the server has not finished deploying. Wait and poll again — do not guess the address.

Cluster-local or published

You chose the server's exposure when you created it, with the expose field. It is one of exactly two values, and there is no endpoint that changes it afterwards — to switch, delete the server and create it again.

exposeWhat it meansAddress shapeWho can reach it
"" (the default)Cluster-local. The server lives only on the platform's internal network.http://mcp-<name>.cai-p-<short>.svc.cluster.local/mcpWorkloads running on the platform
"apps"Published. The server gets a public HTTPS address.https://mcp-<name>-<short>.apps.codyhill.dev/mcpAnything on the internet that has the token

Here <name> is your server's name and <short> is your project's short id — the immutable id fragment that also names your project's namespace (cai-p-<short>). A project rename never changes it, so these addresses are stable. Published addresses follow the same pattern as every other workload; see public endpoints and domains.

Cluster-local is not the same as unauthenticated

A cluster-local address is not on the internet, but it is reachable from other projects on the same platform, because the network routes requests by hostname. That is exactly why every MCP server demands a bearer token on every request, whichever exposure you picked. Do not treat "internal" as "safe to leave open" — the platform does not.

A cluster-local URL is also not something you can open in a browser. The console prints it as copyable text rather than a link, for that reason.

The per-server bearer token

Every MCP server has its own token. The format is cai_mcp_ followed by 64 hexadecimal characters — 32 random bytes, hex-encoded:

cai_mcp_3f9c1a... (64 hex characters in total after the prefix)

Four things to know about it:

  • It is per server, not per user. It is a shared secret between the server and whoever calls it, not one of your API keys. It carries no identity and no project role.
  • It is minted once and reused. The platform creates it the first time the server deploys, and reuses the same value across every later publish, redeploy, and rollback. A rebuild does not rotate it, so callers that already hold it keep working.
  • The server fails closed. If the token is missing from the server's own environment, the server refuses every request with 503 rather than serving openly.
  • Nothing returns it to you. No API response contains it, and the console shows it only as a placeholder. It travels from the platform into the server (to check) and into attached workloads (to present), and nowhere else.
Alpha honesty: getting the token to an external client

Because no endpoint reveals the token, an MCP client outside the platform can only call your server if an administrator provisions the value for you. There is no self-service way to read or rotate it in this alpha. If you need external access, ask your administrator. Calling from an agent on the platform does not have this problem — read on.

Every request carries it as a normal bearer credential:

Authorization: Bearer cai_mcp_...

Call it from a platform agent

An agent does not hard-code the URL or the token. The platform hands each workload an MCP_SERVERS environment variable: a JSON list of objects, each with a url and a bearer.

[{"url": "http://mcp-weather-tools.cai-p-ab12cd.svc.cluster.local/mcp", "bearer": "cai_mcp_..."}]

Every harness image ships crusoe_core.parse_mcp_servers(), which reads that variable and returns a normalized list. It returns an empty list when the variable is absent or blank — the common case, so an agent with no MCP servers still starts — and raises ValueError on malformed JSON, so a misconfiguration fails loudly at startup instead of silently attaching nothing.

Each framework turns that list into its own kind of tool:

FrameworkWhat to useNotes
ADKMCPToolset with StreamableHTTPConnectionParamsNeeds the mcp extra, which the harness-adk base image installs
LangGraphcrusoe_langchain.mcp_tools() (wraps MultiServerMCPClient)Async; returns LangChain tools
CrewAIcrusoe_crewai.mcp.mcp_tools() (wraps MCPServerAdapter)Returns the crew's native tools

Here is a complete ADK agent that attaches every server it is given, alongside the local sandboxed run_python tool:

from google.adk.agents import Agent

from crusoe_adk.foundry import foundry_model
from crusoe_adk.tools import run_python
from crusoe_core import parse_mcp_servers

# MCPToolset lives behind google-adk's [mcp] extra. Import defensively so the
# agent still deploys - with just run_python - on a base image without it.
try:
from google.adk.tools.mcp_tool import MCPToolset, StreamableHTTPConnectionParams
_MCP_AVAILABLE = True
except ImportError:
_MCP_AVAILABLE = False


def _mcp_toolsets():
if not _MCP_AVAILABLE:
return []
toolsets = []
for srv in parse_mcp_servers():
headers = {}
if srv.get("bearer"):
headers["Authorization"] = f"Bearer {srv['bearer']}"
toolsets.append(
MCPToolset(
connection_params=StreamableHTTPConnectionParams(
url=srv["url"],
headers=headers,
# Comfortably longer than a cold start, so the first call to a
# scaled-to-zero MCP server does not time out.
timeout=120,
)
)
)
return toolsets


root_agent = Agent(
name="adk_mcp",
model=foundry_model(),
instruction=(
"You are an assistant with access to tools from a Crusoe MCP server. "
"Prefer an MCP tool when one fits the request, and use run_python for "
"arbitrary computation."
),
tools=[run_python, *_mcp_toolsets()],
)

The agent code never names a server. Change which servers are attached, deploy again, and the same code picks up the new list.

Alpha honesty: attaching a server to an agent

The harness reads MCP_SERVERS and every framework knows how to attach it, but this alpha has no API call and no console control that binds a specific MCP server to a specific agent. Until that lands, ask your administrator to wire it. Write your agent against parse_mcp_servers() now — it degrades to an empty list, so the agent deploys and runs either way, and gains its MCP tools the moment the variable arrives.

Call it from an external MCP client

The server speaks stateless streamable HTTP. That has one very convenient consequence: there is no initialize handshake. A client can send tools/list or tools/call as its first request, which is what makes a scale-to-zero server safe to run with zero instances and several replicas.

List the tools:

curl -s -X POST "https://mcp-weather-tools-ab12cd.apps.codyhill.dev/mcp" \
-H "Authorization: Bearer $CAI_MCP_AUTH_TOKEN" \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'

You should see (trimmed):

{"jsonrpc":"2.0","id":1,"result":{"tools":[{"name":"get_forecast","description":"Current weather for a city.","inputSchema":{"type":"object","properties":{"city":{"type":"string"}},"required":["city"]}}]}}

The name, description, and input schema all come from the Python function you published — its signature and its docstring.

Call one:

curl -s -X POST "https://mcp-weather-tools-ab12cd.apps.codyhill.dev/mcp" \
-H "Authorization: Bearer $CAI_MCP_AUTH_TOKEN" \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":2,"method":"tools/call",
"params":{"name":"get_forecast","arguments":{"city":"Reykjavik"}}}'

The tool's return value comes back inside the JSON-RPC result. Any project secrets the tool declared as credential keys are fetched during this call and dropped when it returns — see manage secrets.

Do not send MCP-Protocol-Version: 2026-07-28

The MCP library this server is built on does not recognize the 2026-07-28 protocol label and rejects it with 400. Either omit the MCP-Protocol-Version header entirely — the server then assumes a version it supports — or send one of the versions the library supports. Some older material, including the console's own copyable snippet, still shows 2026-07-28; that is stale. For anything beyond a quick curl, use a real MCP client library, which negotiates the version for you.

Check that a server is alive

GET /healthz is served without a token, so a health probe does not need the bearer:

curl -s "https://mcp-weather-tools-ab12cd.apps.codyhill.dev/healthz"

You should see:

{"status":"ok"}

This is the one path on the server that does not authenticate. It reports only that the process is up — it does not run your tools.

Timeouts and cold starts

  • Request timeout: 300 seconds. The platform bounds every MCP request at 5 minutes, and tells the server the same number so a tool can size its own work to it. A tool that runs longer is cut off.
  • Cold starts are real. An idle server runs zero instances and wakes on the next request, so the first call after a quiet period is slower than the rest. Give your client a generous timeout — the ADK example above uses 120 seconds for exactly this reason. See autoscaling and scale to zero.
  • Servers are stateless. Nothing is kept between requests. If a tool needs to remember something, return it to the caller or store it somewhere durable.

Errors you will actually see

These come from the server itself, not the platform API, so they do not use the platform's error plus request_id envelope.

StatusBodyWhat it meansFix
401{"error":"unauthorized","detail":"missing or invalid bearer token"}No Authorization header, or the token does not match. Also sends WWW-Authenticate: Bearer.Send Authorization: Bearer with the server's own token. Another server's token will not work.
503{"error":"mcp auth not configured","detail":"CAI_MCP_AUTH_TOKEN is unset"}The server has no token configured, so it is refusing everyone. This is the fail-closed behavior working.The server did not get its credential at deploy. Publish a tool to redeploy it, and tell your administrator if it persists.
400(from the MCP library)Usually an unrecognized MCP-Protocol-Version header.Omit the header, or send a supported version.

Requests to the platform API about the server — listing it, publishing tools, rolling back — use the normal envelope and the normal rules, including the 404-not-403 rule for projects you hold no grant on. See API authentication.

Next steps