MCP servers and secrets
This page documents two command groups: mcp, which manages hosted MCP (Model Context Protocol) tool servers, and secrets, which covers the project secret store.
Read this first: secrets names two different resources
The secrets group carries two resources that have almost nothing in common beyond the word in their name. Get them confused and you will look for a version history that does not exist, or reveal a value that was never there.
platformctl secrets set | Every other secrets subcommand | |
|---|---|---|
| What it writes | An agent environment secret | A project secret |
| Owned by | One agent | The project |
| Shape | KEY=VALUE pairs, one value each | One named value per secret |
| History | None — a write replaces the value | Every write is a new version |
| Who can read it | Only that agent, as an environment variable | Any agent in the project that binds it, plus secrets reveal |
| Reading the value back | Not possible, ever | secrets reveal only — project admin, and audited |
| Documented in | Agents and functions | The project secret store, below |
The CLI's own group help says the same thing:
platformctl secrets --help
'secrets set <agent> KEY=VALUE' writes a per-agent environment secret (one
value, no history).
Every other subcommand operates on the PROJECT secret store: versioned values
owned by the project, which many agents can bind. Values are never returned by
list or show - use 'secrets reveal'.
How both groups behave
Both mcp and the project-secret commands talk to the core platform API, so they resolve their endpoint the usual way — --api, then $CAI_API, then the public API. See the CLI overview.
They require a project. These routes carry the project in the URL path, so there is no server-side default to fall back on. With nothing set:
this command needs a project: pass --project, set $CAI_PROJECT, or run 'platformctl config set-project' (use the id or slug shown by 'platformctl projects list')
A project UUID is used as-is with no extra call. A slug, short id, or name costs one GET /v1/projects lookup on the same connection; if nothing matches:
no project matches "my-proj" among the projects you can access (see 'platformctl projects list')
Roles differ per command. Reading is a project member's right; the irreversible actions are a project admin's.
| Project member may | Project admin is required for |
|---|---|
mcp list, mcp get, mcp tools list, mcp versions | mcp create, mcp delete, mcp tools set, mcp tools delete, mcp rollback, mcp yank, mcp unyank |
secrets list, secrets show, secrets versions, secrets put, secrets issue-token | secrets reveal, secrets delete |
Writing a project secret is deliberately a member's right — a write is versioned and therefore reversible, and a developer who cannot store their own API key stores it in the source instead. Revealing and deleting are neither reversible nor implied by "may use this project's resources", so both are admin.
What a rejection actually looks like. The CLI does not unwrap the server's error envelope. Any 4xx or 5xx prints as workload-api returned <code>: followed by the raw JSON body, and on a 401 or 403 the CLI appends the credential it actually sent. Running secrets reveal without the admin role:
Error: workload-api returned 403: {"error":"this action requires the project admin role","request_id":"4b1e77a0c2d93f58"} [platformctl credential: cached login (/Users/you/Library/Application Support/crusoe-ai/token)]
For readability, the rest of this page quotes only the error field from that body — the part worth reading, and the part you would grep for.
platformctl mcp
An MCP server is a hosted, scale-to-zero container that agents and external MCP clients attach to in order to call tools. Three things are worth holding in your head before you start:
- A server is a container for tools.
mcp createmakes an empty one. It has no tools, no image, and nothing running. - A tool is Python source that the platform builds into the server.
mcp tools setis the only write that produces a running server. - A version is an immutable snapshot. Every publish renders the server's whole current tool set into a new version, pinned to an image digest. That is why
mcp rollbackneeds no rebuild and whymcp versionsis the record of every publish.
There is no "update the server" command, because there is nothing on a server to update apart from its tools.
mcp list
platformctl mcp list
platformctl mcp list
You should see:
NAME STATE EXPOSE VISIBILITY TOOLS URL
weather-tools ready apps private 2 https://mcp-weather-tools-k7m3qz.apps.codyhill.dev
billing-tools pending internal private 0 -
Two columns are easy to misread:
STATEis one ofpending,building,deploying,ready, orfailed. A server that has never had a tool published readspending— the API leaves the field empty in that state, and the CLI prints the word rather than a blank cell. In-o jsonareadyboolean sits beside it, true only atready: the word is what you show, the boolean is what a script branches on, and it is the same field spelled the same way that agents, functions, and the data services answer with.VISIBILITYreadsprivateon every server today. The column exists because the underlying field allowsorgandpublic, but neither is available on the platform. Do not readprivateas "not reachable from other projects" — see connect agents and clients for what actually protects the endpoint.
An empty project prints:
no mcp servers
mcp get
platformctl mcp get <name>
platformctl mcp get weather-tools
The default table is the generic object view: one flattened field per line, keys sorted.
created_at 2026-08-12T18:10:03Z
expose apps
image registry.us-east1-a.ccr.crusoecloudcompute.com/cai-ab12cd-mcp-weather-tools@sha256:0f6b2c4d…
name weather-tools
ready true
state ready
tool_count 2
tool_names forecast, geocode
url https://mcp-weather-tools-k7m3qz.apps.codyhill.dev
version 3
visibility private
Notes on the fields:
imageis the pinned digest the current version was built to. It sits in a repository in your own Crusoe Cloud Registry, created for this server on its first publish. (The digest is abbreviated above; the real output prints it in full.)urlis the base address. MCP clients speak to it at the/mcppath, andGET /healthzis served without a token. See connect agents and clients.version,image, andurlare absent until the server's first successful publish;messageappears only when there is a build or deploy error to report.
Use -o json when you want the credential-key union and the full digest without truncation.
mcp create
Creates an empty server. Nothing is built and nothing is deployed until you publish a tool.
platformctl mcp create <name> [--expose]
| Flag | Default | What it does |
|---|---|---|
--expose | off | Publish the server on the apps domain. Off means internal: reachable from inside the platform and not from the public internet. |
The name must be a lowercase DNS label of 40 characters or fewer; anything else is a 400:
missing or invalid 'name' (must be a lowercase DNS label, <=40 chars)
platformctl mcp create weather-tools --expose
You should see:
created_at 2026-08-12T18:10:03Z
expose apps
name weather-tools
ready false
state pending
tool_count 0
tool_names -
visibility private
There is no mcp update, and no API route behind one. --expose is decided here and cannot be changed afterwards from the CLI, the console, or the API — to switch a server between internal and published you delete it and create it again, which means republishing its tools and losing its version history. Decide before you publish.
Either way the endpoint authenticates every request with a per-server bearer token. "Internal" is not "safe to leave open."
Creating a name that is already taken is a 409:
an mcp server named weather-tools already exists in this project
mcp delete
platformctl mcp delete <name>
platformctl mcp delete billing-tools
deleted billing-tools
This tears down the running server and removes its tools and its version history. The images built for it are not reachable afterwards, so there is nothing left to roll back to. There is no confirmation prompt.
platformctl mcp tools
mcp tools list
platformctl mcp tools list <server>
platformctl mcp tools list weather-tools
You should see:
NAME HANDLER DESCRIPTION
forecast 18 lines Return tomorrow's forecast for a place.
geocode 12 lines Resolve a place name to latitude and longitude.
The HANDLER column reports the source's size, not its content — a handler is a whole Python module, and printing it would destroy the table. -o json carries the full source, the stored schema, and the credential keys. Long descriptions are collapsed onto one line and elided past 60 characters.
An empty server prints:
no tools published on this server
mcp tools set
Publishes a tool — creating it or replacing it — and rebuilds the server.
platformctl mcp tools set <server> <tool> --handler <source|@file|-> [flags]
| Flag | Default | What it does |
|---|---|---|
--handler | none — required | The tool's Python source: a literal string, @path to read a file, or - to read stdin. |
--description | keeps the current value | One line describing the tool, shown to MCP clients in tools/list. |
--credential-key | keeps the current keys | A project-secret name this tool may fetch at call time. Repeatable. |
--schema | keeps the current value | An explicit JSON parameter schema for the catalog, inline. |
--schema-file | keeps the current value | The same value, read from a file. |
Because a tool is a complete module, @file is the form you will use:
platformctl mcp tools set weather-tools forecast \
--handler @tools/forecast.py \
--description "Return tomorrow's forecast for a place." \
--credential-key weather-api-key
You should see:
build_id 6c2f0f5a-6c8f-4b31-9a2e-1c5f0d9a7b44
name forecast
note building a new immutable version from the server's current tool set
published true
server weather-tools
The response is a build id, not a finished deployment. The build runs asynchronously. Poll mcp get <server> until state reaches ready — or, in a script, until ready is true — or watch mcp versions <server> for the new row.
It is a read-modify-write, so an unpassed flag is not a cleared flag
This is the single subtlest thing on the page, and getting it wrong has broken real servers.
The API's tool write is a full replace — every stored column is overwritten from the request body. So before sending, the CLI reads the tool back and re-sends whatever you left out unchanged. A flag you do not pass keeps its current value. It is not cleared.
That behavior is deliberate, and the dangerous field is --credential-key. Publishing recomputes the server's credential-key union from what the write stored, so if omitting the flag meant "no keys", an edit that only touched the description would ship a version that no longer injects the secret — and the tool would start failing at call time, in production, for a change you thought was cosmetic. The console merges the same way, so an edit means the same thing in both places.
Clearing is therefore explicit, and each field has its own spelling:
| To clear | Pass |
|---|---|
| The description | --description "" |
| Every credential key | --credential-key= |
| A pinned schema | --schema '{}' |
Note the = in --credential-key=. A repeatable flag cannot be given with no value at all, so an empty value is the only way to spell "this tool needs no secrets"; blank values are dropped rather than stored as a key whose name is the empty string.
To change the set of keys rather than clear it, pass the complete new set — the flag replaces, it does not append:
platformctl mcp tools set weather-tools forecast \
--handler @tools/forecast.py \
--credential-key weather-api-key \
--credential-key geocode-api-key
Credential keys are what let a tool read a secret
A credential key is the name of a project secret this tool is allowed to fetch while it is running. Values are never mounted into the server, never baked into the image, and never stored on the tool. The declared names travel from the tool, into the version snapshot, into the running server's configuration; at call time the tool asks the platform for the value with a short-lived, scoped token.
In the handler that looks like this:
import crusoe_mcp as crusoe
@crusoe.tool(credential_keys=["weather-api-key"])
def forecast(place: str) -> str:
"""Return tomorrow's forecast for a place.
Args:
place: The place to forecast, e.g. "Reykjavik".
"""
api_key = crusoe.secret("weather-api-key")
...
crusoe.secret(...) fails closed. If the key was not declared when the version was built, the call raises at request time rather than returning an empty string — so the failure shows up as a broken tool call for a user, not as a warning at publish time. The decorator's credential_keys and the CLI's --credential-key must both name the secret. Check mcp versions <server> -o json and read credential_keys on the current row if a tool starts failing to read a secret it used to read.
The secret itself must already exist in the project store — see secrets put below.
Schemas are usually not yours to write
--schema and --schema-file are optional and most servers never use them. The platform infers the parameter schema MCP clients actually see from your handler's signature and docstring at build time. The stored schema is catalog metadata, captured into the version snapshot; it is not what ships to clients. Leave both unset unless you specifically need to pin one.
Passing both is an error:
--schema and --schema-file are two ways to give the same value: pass one
so is malformed JSON, rejected locally before anything is stored:
the schema is not valid JSON (it must be a JSON object describing the tool's parameters)
Other local errors
Omitting the required flag:
Error: required flag(s) "handler" not set
Pointing --handler at something empty:
--handler resolved to an empty handler: it must be the tool's Python source (a complete @crusoe.tool module)
If reading the tool back fails — for example the server name is wrong — the CLI says why it needed to read it in the first place, rather than silently publishing a body missing your other fields:
reading the current tools on weather-tools (needed to keep the fields you did not pass): ...
mcp tools delete
platformctl mcp tools delete <server> <tool>
platformctl mcp tools delete weather-tools geocode
deleted tool geocode from weather-tools; the server is rebuilding without it
The server is rebuilt from its remaining tools and rolled onto a new version, so the tool keeps being served until that version is live.
MCP versions and rollback
mcp versions
platformctl mcp versions <server>
platformctl mcp versions weather-tools
You should see:
VERSION CURRENT YANKED TOOLS CREATED
3 yes no 2 2026-08-12T18:22:41Z
2 no yes 2 2026-08-11T09:04:12Z
1 no no 1 2026-08-10T16:41:55Z
Each row is an immutable snapshot: a pinned image digest, the tool set it was built from, and that set's credential-key union. Nothing about a published version ever changes except its yanked flag. -o json adds image_digest, tool_names, and credential_keys.
Before the first publish:
no versions yet (publish a tool to build one)
mcp rollback
platformctl mcp rollback <server> <version>
platformctl mcp rollback weather-tools 2
You should see:
image registry.us-east1-a.ccr.crusoecloudcompute.com/cai-ab12cd-mcp-weather-tools@sha256:5a41e8b7…
note re-pointed the server at version 2's recorded image; no rebuild
rolled_back true
server weather-tools
version 2
No rebuild happens. The target version's pinned digest is redeployed exactly as it shipped — that is the whole point of versions being immutable, and it is why a rollback is fast and cannot come back subtly different from what ran before. The server's current version advances to the target.
A version number that is not a positive integer is rejected locally, before any request:
invalid version "abc": it must be a positive integer (see 'platformctl mcp versions <server>')
A yanked version is refused as a target with a 409:
version 2 is yanked (retired); unyank it or publish a new version instead
So is a version with nothing to redeploy:
version 2 has no recorded image to roll back to
mcp yank
Retires a version so it cannot be rolled back to — the control for "this build was bad, make sure nobody puts it back."
platformctl mcp yank <server> <version>
platformctl mcp yank weather-tools 3
server weather-tools
version 3
yanked true
A yanked version stays in the history as a record. It is not deleted and its image is not removed; it is simply refused as a rollback target until unyanked.
You cannot yank the version the server is currently running — retiring what is deployed would leave the running image with no live history entry. That is a 409:
version 3 is the one the server currently runs; roll to another version before yanking it
Roll onto another version first, then yank.
mcp unyank
platformctl mcp unyank <server> <version>
platformctl mcp unyank weather-tools 3
server weather-tools
version 3
yanked false
Worked example: publish a tool, then roll it back
Start from an empty project. Store the API key the tool will need, create the server, publish the tool, watch the build, then retire a bad version.
# 1. The secret the tool will read at call time.
printf %s "$WEATHER_API_KEY" | platformctl secrets put weather-api-key
# 2. An empty server, published on the apps domain.
platformctl mcp create weather-tools --expose
# 3. Publish the first tool. This is what builds and deploys the server.
platformctl mcp tools set weather-tools forecast \
--handler @tools/forecast.py \
--description "Return tomorrow's forecast for a place." \
--credential-key weather-api-key
# 4. Wait for it. The publish returned a build id, not a running server.
platformctl mcp get weather-tools
# 5. Publish a second tool. This builds version 2 from BOTH tools.
platformctl mcp tools set weather-tools geocode \
--handler @tools/geocode.py \
--description "Resolve a place name to latitude and longitude."
# 6. Version 2 turned out bad. Go back to 1, then retire 2.
platformctl mcp versions weather-tools
platformctl mcp rollback weather-tools 1
platformctl mcp yank weather-tools 2
Note the order in step 6: roll off version 2 before yanking it. Yanking the running version is refused.
Note also what step 5 did not need to pass. geocode is a new tool, so nothing was read back for it — but had you been editing forecast instead, leaving --credential-key off would have preserved weather-api-key rather than dropping it.
platformctl secrets set for agent environment secrets
platformctl secrets set <agent> KEY=VALUE [KEY=VALUE...]
platformctl secrets set research-buddy DEMO_TOKEN=abc123
set 1 secret(s) for research-buddy
This is the one agent-scoped command in the group. It writes environment secrets onto a single agent, merging with what is already there, and rolls a new revision. There is no version history, and no way to read the values back — platformctl agents secrets get <agent> lists the key names only.
A malformed pair is rejected before anything is sent:
invalid KEY=VALUE pair: "DEMO_TOKEN"
secrets set takes the value as a command-line argument, so it lands in your shell history file and in the process table while the command runs. That is the opposite of the project store's discipline below. Use it for the throwaway values it is meant for; put real credentials in the project store with secrets put, which has no --value flag for exactly this reason.
Full treatment, including how the merge protects platform-managed keys, is on the agents and functions page. See also secrets and environment variables.
The project secret store
Everything below writes and reads project secrets: named, versioned values owned by the project, which many agents can bind and which running workloads fetch at call time.
Two rules shape the whole surface, and both are worth stating plainly before the commands:
- A value is never in a list and never in a show.
secrets revealis the only command that produces plaintext, it is project admin only, and it writes an audit row before the value leaves the server. - Every write is a new version. There is no update-in-place, which is what makes a rotation reversible.
A secret name is 1–63 characters of letters, digits, - or _, starting with a letter or digit. A value must not be empty and is capped at 64 KiB — enough for a certificate chain, small enough that the store does not become a file store.
The name crusoe-cloud-credential is reserved by the platform and is not managed here. It is invisible to secrets list, and every other command reports it as absent with a 404:
no such secret. "crusoe-cloud-credential" is a reserved platform name and is not managed through the project secrets API. If it is a Crusoe Cloud mapping, use GET/PUT/DELETE /v1/projects/{id}/crusoe-cloud instead.
See Crusoe Cloud integration for that surface.
secrets list
platformctl secrets list
platformctl secrets list
You should see:
NAME VERSION UPDATED
stripe-key 4 2026-08-12T18:30:02Z
weather-api-key 1 2026-08-02T11:15:44Z
store path prefix: projects/k7m3qz
Metadata only — name, current version, when it was last written. There is no value in this response and there cannot be, because a routine call that returns plaintext puts plaintext in every log and screenshot of it. Reading a value is a separate, explicit, audited act.
The trailing line names the store path prefix these secrets live under. It answers "which project am I actually looking at", which is the first thing worth knowing in an incident. It is printed only for the table output; -o json and -o yaml carry the items array, matching every other list in the CLI.
An empty store prints:
no project secrets
If the store is not wired up at all, you get a 503 that says so precisely, so you do not read a deployment gap as an empty store:
the project secret store is not configured: workload-api starts it only when BAO_ADDR and BAO_TOKEN are set in its environment, and they are not. This is a deployment gap, NOT an empty secret store - do not read it as 'this project has no secrets'.
secrets show
platformctl secrets show <name>
platformctl secrets show weather-api-key
You should see:
created_at 2026-08-02T11:15:44Z
current_version 1
name weather-api-key
updated_at 2026-08-02T11:15:44Z
versions [{"created_at":"2026-08-02T11:15:44Z","destroyed":false,"version":1}]
It is called show, not get, because it returns no value. There is no flag — here or on the server — that makes it return one.
When agents bind the secret, a used_by row appears listing each binding: the agent's slug, the environment variable name it arrives as, and the pinned version if the binding pins one. -o json is far easier to read for that.
The bindings are the reason to run this before a delete. Nothing in the database points at a secret — the values are not in the database at all — so a delete that breaks an agent would surface much later, as a deploy that fails on a missing environment variable. The server refuses the delete while any binding remains, and this is where you see which ones.
secrets show reports bindings, but there is no platformctl command that creates or removes one. Binding a project secret to an agent's environment variable is console or API only — see use secrets in workloads and the secrets API reference.
secrets put
Writes a new version of a project secret. This one command both creates and rotates.
platformctl secrets put <name> [--value-file <path>]
| Flag | Default | What it does |
|---|---|---|
--value-file | stdin | File to read the value from, or - for stdin. |
--value flag, and that is the pointA credential passed as a command-line argument is in your shell history file, in the process table for as long as the command runs, and in any shell tracing or auditd rule watching argv. A file or a pipe is none of those things. The flag was left out on purpose; it is not an oversight and it will not be added.
Both supported forms:
printf %s "$TOKEN" | platformctl secrets put stripe-key
platformctl secrets put tls-chain --value-file ./chain.pem
The first time:
created stripe-key at version 1
the value is stored. Nothing - this API included - will show it back to you except an explicit, audited reveal.
Every write after that:
rotated stripe-key at version 2
the value is stored. Nothing - this API included - will show it back to you except an explicit, audited reveal.
The verb tells you which happened, and the version number is the durable fact — it is what a pinned binding names and what secrets reveal --version takes.
Trailing newlines differ between the two sources
This is intentional, and it matters for certificates.
--value-fileis taken byte for byte. A PEM chain keeps its trailing newline, because a file is a deliberate artifact and its final byte is part of it.- stdin has exactly one trailing newline removed. That newline is almost always
echo's rather than yours, and an API key with\nwelded to the end fails at the far end of a deploy with an error that says nothing about a newline.
If you need stdin byte-exact, pass --value-file /dev/stdin.
Local errors
Running with a terminal on stdin and no --value-file does not hang waiting for a prompt that is never coming:
no value given: pipe it in (printf %s "$TOKEN" | platformctl secrets put NAME) or pass --value-file <path>
An empty pipe, or a pipe carrying only a newline:
the value read from stdin is empty, and a secret value must not be
An empty or unreadable file, reported before any connection is opened:
/tmp/empty.txt is empty, and a secret value must not be
reading --value-file: open /nope/nothing: no such file or directory
secrets versions
platformctl secrets versions <name>
platformctl secrets versions stripe-key
You should see:
VERSION CREATED CURRENT DESTROYED
4 2026-08-12T18:30:02Z yes no
3 2026-08-05T14:02:11Z no no
2 2026-07-28T08:19:37Z no yes
No values here either. A version number is what secrets reveal --version and a pinned binding take, so this is the command that tells you what to pass them. CURRENT folds in the response's current_version field so the live version is visible without cross-referencing two numbers.
secrets reveal
The only command that produces a value.
platformctl secrets reveal <name> [--version <n>] [--reason <text>]
| Flag | Default | What it does |
|---|---|---|
--version | the current version | Which version to reveal. |
--reason | none | Why you are reading this. Recorded verbatim in the audit row. |
platformctl secrets reveal stripe-key --reason "rotating into the payments vendor console"
You should see:
secret: stripe-key
version: 4
sk_live_EXAMPLE_NOT_A_REAL_KEY
this read is in the audit log with your identity, the version and the time.
Three things about this command:
It requires the project admin role. A member who can write the secret cannot read it back. Writes are reversible; a read is not.
It is audited, and the audit is fail-closed. Before the value is returned, the server records who asked, for which secret and version, when, from where, and the --reason you gave. If that record cannot be written, the value is not returned. There is no reveal without a trace. The server's own note about what it recorded is echoed after the value — the line quoted above is the server's text, not the CLI's.
The row shows up in the project's audit trail like any other action:
platformctl audit --limit 20
TIME ACTOR ACTION TARGET
2026-08-12T18:41:07Z you@example.com project.secret.reveal stripe-key
--reason is optional on purpose. A mandatory free-text field is a field everyone fills in with x. A real one is what makes the row worth reading six months from now. Write the real one.
The output always names the version that was actually served, because "latest" moves and whatever you paste somewhere durable does not. In the rare case the server cannot confirm the number after the read, the CLI says so rather than printing a version that does not exist:
version: current (the server could not confirm the number)
See break-glass and audit for how these records are reviewed.
secrets delete
platformctl secrets delete <name>
platformctl secrets delete stripe-key
deleted project secret stripe-key - every version is destroyed, and this is not recoverable
Project admin only. Every version is destroyed and none of it comes back. There is no confirmation prompt and no soft delete.
The server refuses with a 409 while any binding still names the secret:
stripe-key is still bound by 2 binding(s): research-buddy.STRIPE_KEY, checkout-agent.STRIPE_KEY. Remove them first - nothing in the database prevents this delete, so the failure would otherwise appear later, as an agent that will not start.
That refusal is load-bearing rather than paternalistic. Bindings do not reference the secret in the database — the values are not in the database — so nothing else would stop the delete and nothing would report it. Run secrets show <name> to see what still binds it.
secrets issue-token
Mints a short-lived, read-only token scoped to this project's secrets. It returns a token, never a value.
platformctl secrets issue-token [--secret <name>] [--ttl-seconds <n>]
| Flag | Default | What it does |
|---|---|---|
--secret | every secret in the project | Scope the token to exactly one secret. |
--ttl-seconds | 300, capped at 3600 | Token lifetime. Values above the platform's request-timeout cap are clamped down, not rejected. |
platformctl secrets issue-token --secret weather-api-key --ttl-seconds 60
You should see (the token and the store address are elided here):
<the minted token>
expires in: 60s
read addr: <the secret store's internal address>
read path: secret/data/projects/k7m3qz/weather-api-key
this token is short-lived and scoped to read only. It reads secret VALUES directly from the store until it expires; it is never stored here.
The holder reads values directly from the store, for a bounded time, without going through the platform API. That is what a workload needs in order to fetch its own credentials at call time instead of holding them in its environment for the life of the process — it is the mechanism behind crusoe.secret(...) in MCP tools and agents.
The token is printed on its own line so a careless double-click selects the whole credential and nothing else, followed by the coordinates it reads through — a token with no address and path is not usable, and the server is the only thing that knows them.
Two things to get right:
- Prefer the narrow scope. Without
--secretthe token reads every secret in the project. With it, exactly one. A leaked token is worth exactly what it can reach before it expires. Scoping to one secret is one flag. - Set the TTL to your invocation timeout. Then a secret fetched at any point in a request stays valid to the request's deadline, and the token dies the moment the request could no longer be running.
If the configured store cannot mint scoped tokens, you get a 501:
the configured secret store cannot issue scoped tokens
Worked example: create, rotate, reveal, and delete a secret
# Create. The value comes from a pipe, never an argument.
printf %s "sk_live_..." | platformctl secrets put stripe-key
# created stripe-key at version 1
# Rotate. Same command; the output tells you which happened.
printf %s "sk_live_the_new_one" | platformctl secrets put stripe-key
# rotated stripe-key at version 2
# What versions exist, and which one is live?
platformctl secrets versions stripe-key
# Read it back. Admin only, and this writes an audit row before it answers.
platformctl secrets reveal stripe-key --reason "verifying the rotation landed"
# Read the version you replaced, to compare.
platformctl secrets reveal stripe-key --version 1 --reason "confirming the old key before revoking it upstream"
# Who would break if this went away?
platformctl secrets show stripe-key
# Destroy it. Every version, unrecoverable, and refused while bindings remain.
platformctl secrets delete stripe-key
A PEM chain instead, where the trailing newline must survive:
platformctl secrets put tls-chain --value-file ./fullchain.pem
Related pages
- MCP servers overview — what a hosted tool server is and when to use one.
- Publish tools — writing the handler modules
--handleruploads. - Connect agents and clients — endpoints, the per-server bearer token, and the platform limits on getting it to an external client.
- Versions and rollback — the version model in more depth.
- Build MCP weather tools — an end-to-end tutorial.
- Secrets manager overview and manage secrets.
- Use secrets in workloads — bindings, and
crusoe.secret(...)at call time. - MCP servers API and secrets API.
- CLI overview — endpoint resolution, credentials, output formats.
- Agents and functions — the full
secrets setreference. - Platform and auth — projects, roles, and what remains console or API only.