Skip to content

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 credential’s name in secrets, store its value with trove secret set, and read it at call time with ctx.requireSecret.

It builds directly on the USGS example — same one-tool shape, plus 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 extension.ts.

nasa-apod/
extension.ts ← the whole toolkit
manifest.json ← generated from it

The one addition versus the no-auth example is secrets: the credential name the handler reads. Names only — the value never appears here, and it must be UPPER_SNAKE_CASE.

extension.ts
import { defineToolkit, tool, z, ToolError } from "@ontrove/extend/toolkit";
export default defineToolkit({
id: "nasa-apod",
name: "NASA Astronomy Picture of the Day",
description: "Fetch NASA's Astronomy Picture of the Day and its explanation.",
icon: "🔭",
version: "1.0.0",
secrets: ["NASA_API_KEY"],
egress: ["api.nasa.gov"],
tools: [
tool({
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.
// `requireSecret`, not `secret`: this tool cannot work without the key,
// and `secret` would resolve `undefined` and send `api_key=undefined`.
const apiKey = await ctx.requireSecret("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,
};
},
}),
],
});

The generated manifest.json beside it, which you commit and never edit:

manifest.json
{
"id": "nasa-apod",
"name": "NASA Astronomy Picture of the Day",
"description": "Fetch NASA's Astronomy Picture of the Day and its explanation.",
"icon": "🔭",
"version": "1.0.0",
"secrets": ["NASA_API_KEY"],
"egress": ["api.nasa.gov"],
"generated": true
}

Deploy first so the server exists, then seal the key into the vault by server slug:

Terminal window
trove toolkit 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-apod

The 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.requireSecret("NASA_API_KEY") raises and the tool returns that as its error. Rotating the key is the same command again — no redeploy.

See Secrets and Auth for the full vault model.

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.