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 mcp init acme-orderscd acme-ordersThis creates a folder with two files:
acme-orders/ manifest.json ← what the toolkit is (id, name, secrets, egress) server.ts ← your tool handlersStep 2 — Write the tool
Section titled “Step 2 — Write the tool”Open server.ts. Replace the scaffold with:
import { defineMcpServer, z, ToolError } from "@ontrove/mcp";
export default defineMcpServer({ tools: [ { 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. const token = await ctx.secret("ORDERS_API_TOKEN");
// ctx.fetch is the only egress path. The host must be in manifest.json "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:
title+output+ auto-derivedannotations: a friendly picker name, a validatable structured result (outputSchema→structuredContent), and areadOnlyHintthe SDK derives for you — so a read tool needs noannotationsat all.ctx.secret("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 listed inmanifest.jsonegressis 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 — Fill in the manifest
Section titled “Step 3 — Fill in the manifest”Open manifest.json and confirm it matches:
{ "id": "acme-orders", "name": "Acme Orders", "description": "Query Acme order status.", "version": "1.0.0", "sdk": "@ontrove/mcp@^1", "secrets": ["ORDERS_API_TOKEN"], "egress": ["orders.acme.com"], "scopes": [], "visibility": "private"}The secrets and egress arrays are policy declarations: the platform provisions exactly what you list and blocks everything else. See manifest.json Reference for every field.
Step 4 — Deploy
Section titled “Step 4 — Deploy”trove mcp deployYou should see output like:
Bundling server.ts (esbuild)…✓ deployed acme-orders (version 1.0.0, building)acme-orders__lookup_ordertrove mcp deploy reads manifest.json from the current directory, bundles server.ts, and registers/versions the toolkit via the deployServer GraphQL mutation. (trove deploy is a shorthand alias for trove mcp 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, any tool call that requests it returns an 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 - manifest.json Reference — all manifest fields and their effect
- Secrets and Auth — secret rotation, egress allowlisting, OAuth connections
- Deploying — versioning, rollback, lifecycle
- Examples — complete, copy-pasteable toolkits (USGS, NASA, knowledge-base)