Quickstart — Your First Toolkit
This guide walks you through building and deploying your own toolkit from scratch. (Every toolkit runs as a full MCP server on Trove’s cloud, so “server” below means the code you write — not infrastructure you run.) By the end, a new tool will appear in your Trove connection inside Claude — reachable from desktop, web, and mobile — with no server to operate.
We’ll build an Acme Orders toolkit: a single tool that looks up an order by ID against a fictional public SaaS REST API. The same pattern applies to any REST API, database proxy, or SaaS with a bearer token.
Prerequisites
Section titled “Prerequisites”- Trove CLI installed (
npm install -g @ontrove/cliorbun add -g @ontrove/cli) - Logged in:
trove login - Node 20+ or Bun 1.1+
Step 1 — Scaffold the toolkit
Section titled “Step 1 — Scaffold the toolkit”trove toolkit init acme-orderscd acme-ordersThis creates a folder with two files:
acme-orders/ extension.ts ← the one defineToolkit call: what the toolkit is, and its tools manifest.json ← generated from it; you never edit it by handBoth are written from the same declaration. extension.ts is what you edit; manifest.json is the copy trove toolkit deploy reads off disk, and it carries "generated": true to say so. See the Toolkit Manifest Reference for every field and why the file still exists.
Step 2 — Write the toolkit
Section titled “Step 2 — Write the toolkit”Open extension.ts. Replace the scaffolded tool with:
import { defineToolkit, tool, z, ToolError } from "@ontrove/extend/toolkit";
export default defineToolkit({ // Identity: who the toolkit is. All five are required. id: "acme-orders", name: "Acme Orders", description: "Query Acme order status.", icon: "📦", version: "1.0.0", // Policy: the credential names it reads and the hosts it may reach. secrets: ["ORDERS_API_TOKEN"], egress: ["orders.acme.com"], tools: [ // `tool()` is what keeps `args` typed from this tool's own `input` schema. tool({ name: "lookup_order", // `title` is a friendly display name for client tool pickers. title: "Look up order", description: "Look up the status, line items, and ship date of an Acme order by ID. " + "Use when the user asks about a specific order number.", // This tool only reads, so the SDK auto-derives `readOnlyHint: true` — no // `annotations` needed. (You'd set them only to override, e.g. // `openWorldHint: true` for a public third-party API.) input: z.object({ orderId: z.string().describe("The order ID, e.g. 'ORD-10423'."), }), // An `output` schema compiles to `outputSchema` in `tools/list`, and the // handler's `structured` value is emitted as the spec `structuredContent`. output: z.object({ status: z.string(), shipDate: z.string(), lineItems: z.array(z.object({ sku: z.string(), qty: z.number() })).optional(), }), async handler({ orderId }, ctx) { // Fetch the API token from the encrypted vault — never bundled in the // script. `requireSecret` raises when it isn't set; `secret` resolves // `undefined` for a credential you can work without. const token = await ctx.requireSecret("ORDERS_API_TOKEN");
// ctx.fetch is the only egress path. The host must be in `egress`. const res = await ctx.fetch( `https://orders.acme.com/v1/${orderId}`, { headers: { authorization: `Bearer ${token}` } } );
if (res.status === 404) { throw new ToolError(`Order ${orderId} not found`, { retryable: false }); } if (!res.ok) { throw new ToolError("Orders API error, please try again", { retryable: true }); }
const order = (await res.json()) as { status: string; shipDate: string; lineItems: Array<{ sku: string; qty: number }>; };
return { // `structured` matches the `output` schema → surfaced as structuredContent. text: `Order ${orderId}: ${order.status}, ships ${order.shipDate}.`, structured: order, }; }, }), ],});What this teaches — the best-practice defaults the SDK ships:
- One declaration.
id,name,description,iconandversionare required and live next to the tools.defineToolkitvalidates them when the module is imported, so a missing field is an error at your desk, not on a tool call. tool()around each definition. It returns its argument untouched and costs nothing at run time, but it captures theinputschema’s type, soargsis typed. Without it,argsisunknown.secretsandegressare policy. The platform provisions exactly what you list and blocks everything else. Mirror any change intomanifest.json, which is the copy deploy reads.ctx.requireSecret("ORDERS_API_TOKEN")fetches the token from the vault at call time. The value is never in your bundle or any database column.ctx.fetch(...)is the only way to make outbound HTTP. Any host not inegressis blocked.throw new ToolError(...)returns a clean error to the model. Uncaught exceptions are also caught and sanitized — no stack traces reach Claude.
Step 3 — Keep manifest.json in step
Section titled “Step 3 — Keep manifest.json in step”The scaffold already wrote manifest.json from the same declaration. Because you changed secrets and egress in step 2, mirror them into the file — it is what deploy reads:
{ "id": "acme-orders", "name": "Acme Orders", "description": "Query Acme order status.", "icon": "📦", "version": "1.0.0", "secrets": ["ORDERS_API_TOKEN"], "egress": ["orders.acme.com"], "scopes": [], "visibility": "private", "generated": true}Nothing regenerates the file today, which is the one place this workflow still asks you to repeat yourself. See Why it is generated.
Step 4 — Deploy
Section titled “Step 4 — Deploy”trove toolkit deployYou should see output like:
Bundling extension.ts…✓ deployed acme-orders (version 1.0.0, building)acme-orders__lookup_ordertrove toolkit deploy reads manifest.json from the current directory, bundles extension.ts, and registers/versions the toolkit via the deployServer GraphQL mutation. (trove deploy is a shorthand alias for trove toolkit deploy.)
Step 5 — Set the secret
Section titled “Step 5 — Set the secret”Secrets are scoped to a deployed toolkit, so set them by toolkit slug after the first deploy:
trove secret set acme-orders ORDERS_API_TOKEN --value "sk_live_…"# Or, to keep the value out of shell history:trove secret set acme-orders ORDERS_API_TOKEN --from-stdin # reads the value from stdintrove secret set acme-orders ORDERS_API_TOKEN --from-file ./token.txtThe value is sealed into the encrypted vault (setServerSecret) and never lands in a database column or a log line. Until a declared secret has a value, ctx.requireSecret raises and the tool call returns that error. Rotating a secret is the same command again — no redeploy required.
Step 6 — Try it in Claude
Section titled “Step 6 — Try it in Claude”The next time Claude lists tools on your Trove connection, acme-orders__lookup_order appears alongside trove_search. Ask Claude:
What’s the status of order ORD-10423?
Claude calls acme-orders__lookup_order({ orderId: "ORD-10423" }). The gateway dispatches it to your isolate, the handler fetches the API using the vaulted token, and the result comes back.
Total infrastructure you operate: none.
What’s next
Section titled “What’s next”- SDK Reference —
ctxcapabilities,ToolError, annotations, structured output - Toolkit Manifest Reference — every field you declare, and why the file is generated
- Secrets and Auth — secret rotation, egress allowlisting, OAuth connections
- Deploying — versioning, rollback, lifecycle
- Examples — complete, copy-pasteable toolkits (USGS, NASA, knowledge-base)