Secrets and Auth
Toolkits frequently need to authenticate to a third-party system of record — an internal API, a SaaS endpoint, a database proxy. This page explains how Trove handles that safely.
The two auth problems
Section titled “The two auth problems”There are two distinct authentication questions in a toolkit, and it is worth keeping them separate:
-
Is the caller who they say they are? (Claude → Trove) Already solved. The MCP client authenticates to Trove with Clerk OAuth 2.1 before any tool call reaches your toolkit. Your handler receives
ctx.userId— a verified identity. You did not write any of that authentication logic; it is inherited for free. -
Can the toolkit reach the system of record? (your toolkit → third party) This is what this page covers. The two main patterns are static credentials (API keys, bearer tokens) and OAuth-brokered connections.
Static credentials — the vault model
Section titled “Static credentials — the vault model”Most internal APIs and many SaaS services authenticate with a static credential: an API key, a bearer token, a service-account password. Trove stores these in an encrypted Secrets Vault that is separate from every database column the platform reads for normal operation.
The architecture in plain terms:
- Secret values live in the vault. The vault is an encrypted store keyed per user and per toolkit. Values never appear in a D1 column, never pass through the GraphQL data layer, and are never included in your deployed bundle.
- The registry holds references only. The platform records that your toolkit
acme-ordershas declared a secret namedORDERS_API_TOKEN. It does not record the value. - Decryption happens at invocation. When your handler calls
ctx.secret("ORDERS_API_TOKEN"), the platform decrypts the value for that specific call, injects it into the isolate, and it exists only for the lifetime of that isolate — never written back, never logged. - Logs are redacted. Trove redacts all known secret values from log output before it is stored, so
ctx.log(token)(please don’t do this, but if you do) will not expose the value.
Setting a secret
Section titled “Setting a secret”Secrets are scoped to a deployed toolkit, so the first argument is the toolkit slug and the second is the secret name. Deploy the toolkit first, then set its secrets. The value comes from a flag (never an interactive prompt), so it can stay out of shell history:
# Inline (visible in shell history — fine for scratch tokens):trove secret set acme-orders ORDERS_API_TOKEN --value "sk_live_…"
# From stdin (keeps the value out of argv/history):printf '%s' "$ORDERS_TOKEN" | trove secret set acme-orders ORDERS_API_TOKEN --from-stdin
# From a file:trove secret set acme-orders ORDERS_API_TOKEN --from-file ./token.txtThe value is sealed into the encrypted vault via the setServerSecret(serverId, name, value) mutation — it is never echoed, never logged, and never written to a D1 column.
Rotating a secret
Section titled “Rotating a secret”Run the same trove secret set command again with the new value. It takes effect on the next invocation — no redeploy required, because the vault is decrypted fresh on every tool call.
Listing secret names
Section titled “Listing secret names”trove secret ls acme-ordersThe control plane does not yet expose secret references as a schema field, so trove secret ls reports the toolkit’s tool surface as the closest available reference and notes the gap. Secret values are never readable by design.
Declaring secrets in the manifest
Section titled “Declaring secrets in the manifest”Every secret name your toolkit accesses must be declared in manifest.json secrets:
{ "secrets": ["ORDERS_API_TOKEN", "METRICS_READ_KEY"]}At runtime, ctx.secret(name) throws if name is not in the declared list — even if a value exists in the vault under that name. This prevents a toolkit from accidentally reading a secret intended for a different toolkit.
See manifest.json Reference for the full field spec.
Egress allowlisting
Section titled “Egress allowlisting”Outbound HTTP from your toolkit is denied by default. Every hostname your handler calls via ctx.fetch must be declared in manifest.json egress:
{ "egress": ["orders.acme.com", "api.example.com"]}Trove enforces this allowlist at the network level. A request to any host not in the list — including a redirect that resolves to an unlisted host — is blocked with a network error, not a 403. Your handler receives a fetch failure.
This means a malicious dependency in your server.ts bundle cannot exfiltrate your secrets to an external host that is not in your allowlist, even if it intercepts a decrypted value inside the isolate.
Public-internet hosts only
Section titled “Public-internet hosts only”egress lists public-internet hosts only. Even a host you explicitly allow is rejected if it resolves to a private, local, or reserved address — localhost / 127.0.0.0/8, the private ranges 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, the link-local range 169.254.0.0/16 (including the cloud-metadata IP 169.254.169.254), and the IPv6 equivalents. The platform checks the resolved address on every request and redirect, so a host that DNS-rebinds to an internal IP is still blocked. This is a server-side request forgery (SSRF) defense for a platform that runs untrusted code.
The practical consequence: a toolkit cannot reach your own machine — no localhost, no LAN or home network, no local files, no Mac apps. It runs in Trove’s cloud, not on your laptop. If you need local or private data, that’s a job for a source (which syncs on your Mac and collects data in) or a local stdio MCP server you run yourself — see What toolkits can and can’t reach.
What counts as a “host”
Section titled “What counts as a “host””Entries are matched on hostname only — no port, no path. "orders.acme.com" covers https://orders.acme.com/v1/... and https://orders.acme.com:8443/.... It does not cover https://api.orders.acme.com/... — list subdomains explicitly.
Combining secrets and egress — the full pattern
Section titled “Combining secrets and egress — the full pattern”A typical toolkit-to-API pattern:
async handler({ orderId }, ctx) { // 1. Fetch the credential from the vault. const token = await ctx.secret("ORDERS_API_TOKEN");
// 2. Call the external API via ctx.fetch (egress-controlled). const res = await ctx.fetch( `https://orders.acme.com/v1/${orderId}`, { headers: { authorization: `Bearer ${token}` } } );
if (!res.ok) { throw new ToolError(`Order ${orderId} not found`, { retryable: false }); }
return { text: `...` };}And the corresponding manifest:
{ "secrets": ["ORDERS_API_TOKEN"], "egress": ["orders.acme.com"]}The two declarations are independent — you can have a secret with no egress (if you use the value to sign a request body, not an outbound call) and egress with no secret (if the API is public). In practice they usually travel together.
OAuth-brokered connections
Section titled “OAuth-brokered connections”Some systems of record are themselves OAuth providers — Linear, Google Workspace, GitHub, Salesforce. For these, you don’t hold a static token; you need a user-authorized OAuth flow.
In a future release, Trove will broker OAuth connections on your behalf:
- You declare the provider and scopes in
manifest.json:"oauth": { "provider": "linear", "scopes": ["read:issues", "write:comments"] } - The user connects the account in the Trove web app or Mac app — same surface as existing sources.
- Trove manages the token lifecycle (refresh, rotation) and stores the resulting tokens in the vault as a managed secret.
- In your handler,
ctx.secret("LINEAR_ACCESS_TOKEN")returns the current valid access token — no OAuth flow in your code.
Until OAuth-brokered connections are available, you can bridge the gap by manually fetching an OAuth access token, storing it as a static secret, and refreshing it with a separate script or cron job.
Security summary
Section titled “Security summary”| What | Where it lives | Who can read it |
|---|---|---|
| Secret value | Encrypted vault (not D1) | Platform only, at invocation time, for the owning user |
| Secret reference (name + vault key) | Registry D1 | Control plane only — not the GraphQL data layer |
| Decrypted value | Isolate memory | Your handler only, for the lifetime of the call |
| Log output | Per-toolkit log store | You (via trove logs), redacted of secret values |
No secret value ever appears in a database column, a GraphQL response, a deployment bundle, or a log line.