Example: NASA APOD (authed toolkit)
A toolkit that fetches NASA’s Astronomy Picture of the Day (APOD). NASA’s API is free but requires an API key, so this is the minimal example of the authed pattern: declare the secret in the manifest, store its value with trove secret set, and read it at call time with ctx.secret.
It builds directly on the USGS example — same one-tool shape, plus a key.
Get a key
Section titled “Get a key”A free NASA API key takes ten seconds at api.nasa.gov (the demo key DEMO_KEY also works for light testing, but is heavily rate-limited). Keep the key out of your code — it goes in the Trove vault, not server.ts.
Project layout
Section titled “Project layout”nasa-apod/ manifest.json server.tsmanifest.json
Section titled “manifest.json”The two new fields versus the no-auth example are secrets (the name you’ll read with ctx.secret) and the NASA host in egress.
{ "id": "nasa-apod", "name": "NASA Astronomy Picture of the Day", "description": "Fetch NASA's Astronomy Picture of the Day and its explanation.", "version": "1.0.0", "sdk": "@ontrove/mcp@^1", "secrets": ["NASA_API_KEY"], "egress": ["api.nasa.gov"], "scopes": [], "visibility": "private"}server.ts
Section titled “server.ts”import { defineMcpServer, z, ToolError } from "@ontrove/mcp";
export default defineMcpServer({ tools: [ { name: "astronomy_picture", title: "Astronomy Picture of the Day", description: "Fetch NASA's Astronomy Picture of the Day (APOD) for a given date (or today), " + "including the title, explanation, and image/video URL.", // Read-only over a public third-party API. annotations: { readOnlyHint: true, openWorldHint: true }, input: z.object({ date: z .string() .regex(/^\d{4}-\d{2}-\d{2}$/, "Use YYYY-MM-DD.") .optional() .describe("The APOD date (YYYY-MM-DD). Defaults to today."), }), output: z.object({ title: z.string(), date: z.string(), explanation: z.string(), mediaType: z.enum(["image", "video"]), url: z.string(), copyright: z.string().optional(), }), async handler({ date }, ctx) { // Pull the API key from the encrypted vault at call time — never bundled. const apiKey = await ctx.secret("NASA_API_KEY");
const params = new URLSearchParams({ api_key: apiKey }); if (date) params.set("date", date);
const res = await ctx.fetch( `https://api.nasa.gov/planetary/apod?${params.toString()}`, { headers: { accept: "application/json" } }, );
if (res.status === 403) { throw new ToolError("NASA rejected the API key. Check NASA_API_KEY.", { retryable: false, }); } if (res.status === 429) { throw new ToolError("NASA rate limit reached, try again shortly.", { retryable: true, }); } if (!res.ok) { throw new ToolError("NASA APOD is temporarily unavailable.", { retryable: true }); }
const apod = (await res.json()) as { title: string; date: string; explanation: string; media_type: "image" | "video"; url: string; copyright?: string; };
const structured = { title: apod.title, date: apod.date, explanation: apod.explanation, mediaType: apod.media_type, url: apod.url, ...(apod.copyright ? { copyright: apod.copyright.trim() } : {}), };
return { text: `${apod.title} (${apod.date})\n\n${apod.explanation}\n\n${apod.url}`, structured, }; }, }, ],});Deploy, then set the secret
Section titled “Deploy, then set the secret”Deploy first so the server exists, then seal the key into the vault by server slug:
trove mcp deploy# ✓ deployed nasa-apod (version 1.0.0, building)# nasa-apod__astronomy_picture
# Set the secret value (scoped to this server). Keep it out of shell history:printf '%s' "$NASA_KEY" | trove secret set nasa-apod NASA_API_KEY --from-stdin# ✓ secret 'NASA_API_KEY' sealed for nasa-apodThe value goes to the encrypted vault via the setServerSecret mutation — never to a database column or a log line. Until the secret is set, ctx.secret("NASA_API_KEY") (and therefore the tool) fails. Rotating the key is the same command again — no redeploy.
See Secrets and Auth for the full vault model.
What the user sees in Claude
Section titled “What the user sees in Claude”Show me NASA’s picture of the day.
Claude calls nasa-apod__astronomy_picture({}). The handler reads the vaulted key, calls api.nasa.gov, and returns the title, explanation, and image URL — plus the structured object for any client that renders it.
Next steps
Section titled “Next steps”- USGS Earthquakes — the no-auth version of this shape
- Knowledge-base tool — reading and writing the user’s own Trove
- Secrets and Auth — rotation, egress, and the vault model