Publish tools
In this guide you create an MCP server, publish a complete weather tool to it, watch the build go from accepted to ready, and declare a credential key the tool reads at call time.
Before you begin
- You need an account with the admin role on a project. Publishing tools is an admin action; ask your administrator for an account or an invitation link if you do not have one.
- You need the API base URL. There is no public API hostname yet in this alpha — use the endpoint your administrator gives you, or a port-forward.
- The examples use
curlandjq.
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')
Step 1: Create the server
A server starts empty — it is a named container for tools. The create body has exactly two fields:
name— a lowercase DNS label, at most 40 characters (letters, digits, hyphens; must start with a letter, must not end with a hyphen).expose—""for a cluster-local server (reachable only from inside the platform network) or"apps"to publish it on a public HTTPS address.
curl -s -X POST "$CAI_API/v1/projects/$PROJ/mcpservers" \
-H "Authorization: Bearer $TOK" \
-H "Content-Type: application/json" \
-d '{"name":"weather-tools","expose":""}'
You should see:
{"mcp_server":{"name":"weather-tools","expose":"","status":"pending","tool_names":[],"tool_count":0,...}}
status is pending because a server with no tools has nothing to build yet.
Real error messages you might hit:
400—missing or invalid 'name' (must be a lowercase DNS label, <=40 chars)400—'expose' must be "" (cluster-local) or "apps"409—an mcp server named weather-tools already exists in this project
The create endpoint reads only name and expose. Any other fields in the body are silently ignored, and the request body is capped at 4 KiB.
Step 2: Write the tool
A tool is a plain Python function decorated with @crusoe.tool. The platform infers everything else:
- The description an MCP client shows comes from the docstring.
- The input schema (which arguments exist and their types) comes from the function signature.
credential_keyslists the names of project secrets the tool may read at call time.
Save this as get_forecast.py:
import crusoe_mcp as crusoe
@crusoe.tool(credential_keys=["weather-api-key"])
def get_forecast(city: str) -> dict:
"""Current weather for a city.
Args:
city: City name, e.g. "Reykjavik".
"""
api_key = crusoe.secret("weather-api-key") # fetched at call time, never stored
# Call your real weather provider with api_key here.
# This demo returns a stub so the example runs without an external account.
return {"city": city, "forecast": "sunny", "unit": "celsius"}
crusoe.secret("weather-api-key") fetches the secret's value fresh for each tool call, using a short-lived token. The value is never written into the image and never stored on the server. Tools can also call crusoe.run_python(...) to run code in the code sandbox.
Step 3: Publish the tool
Publishing is a PUT to tools/{tool}. The tool name in the URL becomes the module file (tools/get_forecast.py), so it must be a lowercase identifier: letters, digits, and underscores, not starting with an underscore, at most 63 characters.
jq -n --rawfile handler get_forecast.py \
'{handler: $handler, description: "Current weather for a city", credential_keys: ["weather-api-key"]}' \
| curl -s -X PUT "$CAI_API/v1/projects/$PROJ/mcpservers/weather-tools/tools/get_forecast" \
-H "Authorization: Bearer $TOK" \
-H "Content-Type: application/json" \
-d @-
You should see:
{"server":"weather-tools","name":"get_forecast","published":true,"build_id":"<uuid>","note":"building a new immutable version from the server's current tool set"}
That 202 Accepted means the platform took a snapshot of the server's entire tool set and started building version 1 in the background.
Request fields:
| Field | Type | Required | Notes |
|---|---|---|---|
handler | string | yes | The tool's complete Python source (a @crusoe.tool module) |
description | string | no | One-line prose shown in tool listings |
schema | object | no | Explicit JSON Schema for the arguments; omitted = inferred from the signature |
credential_keys | array of strings | no | Names of project secrets the tool may read (credentialKeys is accepted as an alias) |
The publish body is capped at 1 MiB. Real error messages:
400—missing 'handler': the tool's Python source (a complete @crusoe.tool module)400—invalid tool name (must be a lowercase identifier not starting with '_')
Step 4: Watch the build
Builds are asynchronous. Poll the server until status is ready:
curl -s "$CAI_API/v1/projects/$PROJ/mcpservers/weather-tools" \
-H "Authorization: Bearer $TOK" | jq .mcp_server.status
You should see (after a minute or two):
"ready"
The full object now carries the endpoint and version:
curl -s "$CAI_API/v1/projects/$PROJ/mcpservers/weather-tools" \
-H "Authorization: Bearer $TOK" | jq .
You should see:
{"mcp_server":{"name":"weather-tools","status":"ready","url":"...","version":1,"tool_names":["get_forecast"],"tool_count":1,...}}
The lifecycle is pending → building → deploying → ready, or failed. If a build fails, the reason lands in the server's message field — there is no separate build-log endpoint.
Step 5: Set the credential value
Declaring credential_keys names the secret; it does not create it. Store the actual value once in your project's secret store — see manage secrets. At call time the running tool fetches the value by name. If the secret does not exist yet, the tool's crusoe.secret(...) call fails at runtime, not at build time.
Step 6: Verify the published tool set
curl -s "$CAI_API/v1/projects/$PROJ/mcpservers/weather-tools/tools" \
-H "Authorization: Bearer $TOK" | jq .
You should see:
{"server":"weather-tools","tools":[{"name":"get_forecast","description":"Current weather for a city","handler":"...","schema":{},"credential_keys":["weather-api-key"],...}]}
Updating and removing tools
- Update:
PUTthe same tool name again with new source. Every publish builds a fresh immutable version of the whole tool set. - Remove: delete the tool; this also triggers a rebuild:
curl -s -X DELETE "$CAI_API/v1/projects/$PROJ/mcpservers/weather-tools/tools/get_forecast" \
-H "Authorization: Bearer $TOK"
You should see:
{"server":"weather-tools","name":"get_forecast","deleted":true,"build_id":"<uuid>","note":"rebuilding the server without this tool"}
Deleting a tool that does not exist returns 404.
Clean up
Deleting the server removes its tools and its endpoint. Anything pointed at its URL starts failing immediately.
curl -s -X DELETE "$CAI_API/v1/projects/$PROJ/mcpservers/weather-tools" \
-H "Authorization: Bearer $TOK"
You should see:
{"name":"weather-tools","deleted":true}
Next steps
- Versions and rollback — every publish you just did created a version; learn to roll between them.
- Connect agents and clients — call
get_forecastfrom an agent or an external MCP client. - Tutorial: weather tools over MCP — the end-to-end walkthrough.