From f8794e14008a18d42425f0c6dabc95e1888ebb4a Mon Sep 17 00:00:00 2001 From: Aliu Salaudeen Date: Tue, 23 Jun 2026 17:02:08 +0100 Subject: [PATCH 1/7] fix(catalog): restore shape-discipline + Date-conversion exampleCalls MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Re-applies the substantive content from the merged-then-lost PRs #28 (shape-large-payloads) and #30 (time-series-date-conversion), on top of current main (post-PR-#31). Same code/diff as those original PRs, just authored against the recovered base instead of restoring history via force-push. Background: main was force-pushed at 2026-06-23 12:57 UTC from a compromised maintainer credential, rewinding 10 commits and inserting an obfuscated payload into vitest.config.ts plus an axios downgrade in package.json. The payload was removed by a forward-fix in PR #31, but PR #31 only restored a clean tip — it did not bring back the substantive work from PRs #28 and #30. This PR closes that gap without another force-push. What's restored: * Narrow-vs-broad guidance in descriptions of every broad list endpoint (getProtocols, getChains, getDexsOverview, getFeesOverview, getOptionsOverview, getLatestPools, getStablecoins, getStablecoinChains). * Response shaping in every exampleCall — sort+slice+project for lists, slice-tail+project for time-series, pluck-specific-fields for single-entity summaries, nested coins[key] unwrap for price.* endpoints. * The * 1000 Unix-seconds -> ms conversion on the three /v2-style time-series endpoints (getHistoricalChainTvl, getStablecoinCharts, getStablecoinPrices). The fourth (getHistoricalPoolData) is left alone because its timestamp is already an ISO string; asymmetry noted inline. * Upstream-shape correctness in getProtocol's exampleCall — the /protocol/{slug} endpoint returns tvl as a 2012-entry array, not a number. The example now plucks the latest entry from that array. * getHistoricalPoolData unwrap — response shape is { data: [] }, not a bare array. exampleCall uses (series.data ?? []).slice(). * Null guards on the pool-lookup-then-fetch chain so an empty find() result doesn't TypeError or trip Zod validation. * New always-loaded instructions.md section "Shape responses inside execute() — don't ship raw payloads back". What's deliberately not changed: * vitest.config.ts stays clean (the malicious payload is not re-introduced). * axios stays at ^1.12.2 (no re-downgrade). * No test, service, or build-tool changes. Locally verified: - pnpm exec tsc --noEmit clean - pnpm test -> 171 passed - regenerated embedded-index.ts + instructions.generated.ts Original PRs whose content this restores: #28, #30. Co-Authored-By: Claude Opus 4.7 (1M context) --- .changeset/restore-shape-and-date-fixes.md | 34 +++++++++ src/mcp/catalog/tool-metadata.ts | 75 ++++++++++--------- .../instructions/instructions.generated.ts | 45 +++++++++++ src/mcp/instructions/instructions.md | 45 +++++++++++ src/mcp/search-docs/embedded-index.ts | 75 ++++++++++--------- 5 files changed, 204 insertions(+), 70 deletions(-) create mode 100644 .changeset/restore-shape-and-date-fixes.md diff --git a/.changeset/restore-shape-and-date-fixes.md b/.changeset/restore-shape-and-date-fixes.md new file mode 100644 index 0000000..2626ff1 --- /dev/null +++ b/.changeset/restore-shape-and-date-fixes.md @@ -0,0 +1,34 @@ +--- +"@iqai/defillama-mcp": patch +--- + +Restore the shape-discipline and time-series Date-conversion exampleCall work that was lost when `main` was force-pushed back to a pre-PR-#28 state by a compromised maintainer credential. The malicious commit has already been removed from `main` (in PR #31); this PR re-applies the substantive fixes from the merged-then-lost PR #28 and PR #30 on top of the now-clean main. + +**What was lost (and is now restored):** + +- **Narrow-vs-broad guidance** on broad list endpoints (`getProtocols`, `getChains`, `getDexsOverview`, `getFeesOverview`, `getOptionsOverview`, `getLatestPools`, `getStablecoins`, `getStablecoinChains`). Descriptions now explicitly recommend the narrow `getX({slug})` endpoint when the target is known and require projection/filtering inside `execute()` before returning. `getProtocols` calls out its payload size (~3k entries, several MB) so the size cost is visible up front. + +- **Response shaping in every `exampleCall`.** Each endpoint's example now demonstrates the right size-discipline pattern instead of returning the raw call: + + - Broad lists → `sort → slice(0, 20) → map(p => ({ ...specific fields }))` + - Time-series (`getHistoricalChainTvl`, `getStablecoinCharts`, `getStablecoinPrices`, `getHistoricalPoolData`) → `series.slice(-90).map(p => ({ ...specific fields }))` + - Single-entity summaries (`getProtocol`, `getDexSummary`, `getFeesSummary`, `getOptionsSummary`) → destructure / pluck the 5-7 fields the question typically needs, instead of returning the full 30+ field object + - Price endpoints (all six in `price.*`) → unwrap nested `res.coins?.[key]?.price` / `.prices` instead of returning the raw `{ coins: { ... } }` wrapper + +- **Unix-seconds-vs-milliseconds JS Date fix** on the three `/v2`-style time-series endpoints (`getHistoricalChainTvl`, `getStablecoinCharts`, `getStablecoinPrices`). DefiLlama returns `date` as Unix seconds; JS `new Date(n)` expects milliseconds — so without the conversion the model gets `1970-01-XX` dates. The exampleCalls now demonstrate `new Date(p.date * 1000).toISOString()` inline. `getHistoricalPoolData` is left alone because its `timestamp` is already an ISO string (asymmetry called out via a one-line comment so the model doesn't apply `* 1000` there by mistake). + +- **Upstream-shape correctness in `getProtocol` exampleCall.** The `/protocol/{slug}` endpoint returns `tvl` as a 2012-entry array of `{date, totalLiquidityUSD}`, not a top-level number; `change_1d` / `change_7d` don't exist on this endpoint at all. The exampleCall now plucks `p.tvl?.[p.tvl.length - 1]?.totalLiquidityUSD` so a literal `tvl: p.tvl` projection doesn't ship the full historical series back. (Latent bug in `ProtocolSchema` / `ProtocolData` type — declared `tvl: number` — still tracked separately, out of scope here.) + +- **`getHistoricalPoolData` response unwrap.** Schema declares `{ data: HistoricalPoolItem[] }` — the exampleCall now uses `(series.data ?? []).slice(-90).map(...)` instead of `series.slice(-90)` which would have TypeError'd. + +- **Null/undefined guards** on the pool-lookup-then-fetch chain: `(pools.data ?? []).find(...)?.pool`, then `if (!id) return { error: 'Pool not found' }` before calling `getHistoricalPoolData`, so the Zod `pool: z.string()` validation can't be tripped. + +- **New always-loaded instructions section** "Shape responses inside `execute()` — don't ship raw payloads back". States the rule (the sandbox is for trimming/shaping at the source; the return value should already be the small thing the agent will reason about), gives three labelled patterns (lists / time-series / summaries), notes the nested-`coins[key]` shape, and reiterates the narrow-vs-broad preference. + +**What is unchanged from current `main`:** + +- The malicious `vitest.config.ts` payload removed by PR #31 stays removed. +- `axios` stays at `^1.12.2` (the malicious downgrade to `^1.9.0` is not re-introduced). +- No changes to test files, services, or build tooling. + +Original PRs whose content this restores: #28 (shape-large-payloads), #30 (time-series-date-conversion). See the audit thread on PR #31 / the security incident notes for context on how the work was lost. diff --git a/src/mcp/catalog/tool-metadata.ts b/src/mcp/catalog/tool-metadata.ts index 2fc10b5..330d1f9 100644 --- a/src/mcp/catalog/tool-metadata.ts +++ b/src/mcp/catalog/tool-metadata.ts @@ -124,27 +124,29 @@ export const TOOL_METADATA: ToolMetadata[] = [ qualified: "defillama.protocol.getChains", sandboxImpl: lazyMethod("protocolService", "getChainsRaw"), description: - "List every chain DefiLlama tracks, each with its current TVL. Returns the full array (sort/slice in your execute() script). This is the canonical chain catalog — read the `name` field for api.llama.fi endpoints (case-sensitive display name) and lowercase that same value for coins.llama.fi price/block calls. Prefer `defillama.resolveChain(input)` for translating a human input to the right form; fall back to enumerating this list when the resolver returns null.", + "Broad list of every chain DefiLlama tracks (~200 entries with current TVL). Use for cross-chain aggregate queries (TVL leaderboards, chain discovery) or as the resolver fallback. For 'what is X chain's TVL', prefer `defillama.resolveChain(input)` → `getHistoricalChainTvl({chain})` instead. Read `name` for api.llama.fi endpoints; lowercase the same value for coins.llama.fi. ALWAYS sort/filter/project inside the `execute()` sandbox — never return the raw catalog.", parameters: z.object({}), responseSchema: ChainsSchema, - exampleCall: "await defillama.protocol.getChains()", + exampleCall: + "const chains = await defillama.protocol.getChains(); return chains.sort((a,b) => (b.tvl ?? 0) - (a.tvl ?? 0)).slice(0, 20).map(c => ({name: c.name, tvl: c.tvl}))", }, { name: "defillama_get_protocols", qualified: "defillama.protocol.getProtocols", sandboxImpl: lazyMethod("protocolService", "getProtocolsRaw"), description: - "List every DeFi protocol DefiLlama tracks with its TVL, category, chains and recent change metrics. Returns the full unsorted array (sort/slice/field-pick in your execute() script).", + "Returns the FULL DefiLlama protocol catalog (~3k entries, several MB of JSON). Use ONLY for cross-protocol aggregate queries (leaderboards, category rollups, by-chain counts) or as the discovery fallback when `defillama.resolveProtocol(name)` returns null. For a single-protocol question ('what is X's TVL?'), prefer the resolver → `getProtocol({protocol: slug})` path — calling this endpoint and returning the raw response blows the agent's context. ALWAYS project/filter/sort/slice inside the `execute()` sandbox before returning.", parameters: z.object({}), responseSchema: ProtocolsSchema, - exampleCall: "await defillama.protocol.getProtocols()", + exampleCall: + "const protocols = await defillama.protocol.getProtocols(); return protocols.sort((a,b) => (b.tvl ?? 0) - (a.tvl ?? 0)).slice(0, 20).map(p => ({slug: p.slug, name: p.name, tvl: p.tvl, chains: p.chains, change_7d: p.change_7d}))", }, { name: "defillama_get_protocol", qualified: "defillama.protocol.getProtocol", sandboxImpl: lazyMethod("protocolService", "getProtocolRaw"), description: - "Fetch detailed TVL data for a single DeFi protocol, including per-chain TVL breakdowns and historical series. Pass the canonical protocol slug from the DefiLlama catalog. Slugs are kebab-case lowercase with version suffixes preserved and are NOT derivable from display names — discover via `defillama.resolveProtocol(name)` or by enumerating `defillama.protocol.getProtocols()` and filtering on `name` (see the find-protocol-slug recipe). Don't construct the slug by transforming a display name.", + "Narrow endpoint for a SINGLE protocol's TVL, per-chain breakdown, and historical series. Preferred over `getProtocols()` for any 'what is X' question. The response is large (per-chain breakdowns + full historical TVL series); pluck the fields you actually need rather than returning the whole object. Pass the canonical kebab-case slug from the DefiLlama catalog — discover via `defillama.resolveProtocol(name)` or by enumerating `defillama.protocol.getProtocols()` and filtering on `name` (see the find-protocol-slug recipe). Slugs are NOT derivable from display names.", parameters: z.object({ protocol: z .string() @@ -154,7 +156,7 @@ export const TOOL_METADATA: ToolMetadata[] = [ }), responseSchema: ProtocolSchema, exampleCall: - "const slug = await defillama.resolveProtocol(name); await defillama.protocol.getProtocol({protocol: slug})", + "const slug = await defillama.resolveProtocol(name); const p = await defillama.protocol.getProtocol({protocol: slug}); const last = p.tvl?.[p.tvl.length - 1]; return { name: p.name, currentTvl: last?.totalLiquidityUSD, asOf: last?.date, chains: p.chains }", }, { name: "defillama_get_historical_chain_tvl", @@ -172,7 +174,7 @@ export const TOOL_METADATA: ToolMetadata[] = [ }), responseSchema: HistoricalChainTvlSchema, exampleCall: - "const {name} = await defillama.resolveChain(input); await defillama.protocol.getHistoricalChainTvl({chain: name})", + "const {name} = await defillama.resolveChain(input); const series = await defillama.protocol.getHistoricalChainTvl({chain: name}); return series.slice(-90).map(p => ({date: new Date(p.date * 1000).toISOString() /* p.date is Unix seconds; multiply by 1000 for JS Date */, tvl: p.tvl}))", }, // ── DEX (api.llama.fi) ───────────────────────────────────────────────── { @@ -180,7 +182,7 @@ export const TOOL_METADATA: ToolMetadata[] = [ qualified: "defillama.dex.getDexSummary", sandboxImpl: lazyMethod("dexService", "getDexSummaryRaw"), description: - "Fetch detailed DEX trading-volume data for a single DEX protocol, including totals and (optionally) the volume chart series. Pass the canonical DEX protocol slug from the DefiLlama catalog. Slugs are kebab-case and version-suffixed and NOT derivable from display names — discover via `defillama.resolveProtocol(name)` or by enumerating `defillama.dex.getDexsOverview().protocols` and filtering on `name`. Don't construct the slug by transforming a display name.", + "Narrow endpoint for a SINGLE DEX protocol's trading volume. Response has 30+ fields covering totals (24h/7d/30d/all-time), percentage changes, per-chain breakdowns, and (optionally) the volume chart series — pluck the specific fields you need rather than returning the whole object. Pass the canonical kebab-case DEX slug — discover via `defillama.resolveProtocol(name)` or by enumerating `defillama.dex.getDexsOverview().protocols` and filtering on `name`. Slugs are NOT derivable from display names. Each catalog (DEX, revenue, options) has its own slug namespace — a slug valid in one may not exist in another.", parameters: z.object({ protocol: z .string() @@ -202,14 +204,14 @@ export const TOOL_METADATA: ToolMetadata[] = [ }), responseSchema: DexSummarySchema, exampleCall: - "const slug = await defillama.resolveProtocol(name); await defillama.dex.getDexSummary({protocol: slug})", + "const slug = await defillama.resolveProtocol(name); const s = await defillama.dex.getDexSummary({protocol: slug}); return { name: s.name, total24h: s.total24h, total7d: s.total7d, total30d: s.total30d, totalAllTime: s.totalAllTime, change_1d: s.change_1d, change_7d: s.change_7d }", }, { name: "defillama_get_dexs_overview", qualified: "defillama.dex.getDexsOverview", sandboxImpl: lazyMethod("dexService", "getDexsOverviewRaw"), description: - "Fetch a DEX volume overview across all DEXs, or scoped to one chain when `chain` is provided. Returns the full `protocols` array (sort/slice/field-pick in your execute() script). Use the chain DISPLAY NAME from `defillama.resolveChain(input).name` (case-sensitive).", + "Broad overview of DEX volume across all DEXs, or scoped to one chain. Returns a large `protocols` array — use for leaderboards, cross-DEX comparisons, or as the canonical source for discovering valid DEX slugs (`slug` field on each entry). For a single-DEX question, prefer the resolver → `getDexSummary({protocol: slug})` path. ALWAYS sort/filter/project inside the `execute()` sandbox before returning; never return the raw response.", parameters: z.object({ chain: z .string() @@ -232,7 +234,7 @@ export const TOOL_METADATA: ToolMetadata[] = [ }), responseSchema: DexOverviewSchema, exampleCall: - "const {name} = await defillama.resolveChain(input); await defillama.dex.getDexsOverview({chain: name})", + "const {name} = await defillama.resolveChain(input); const overview = await defillama.dex.getDexsOverview({chain: name}); return (overview.protocols ?? []).sort((a,b) => (b.total24h ?? 0) - (a.total24h ?? 0)).slice(0, 20).map(p => ({slug: p.slug, name: p.name, total24h: p.total24h, change_7d: p.change_7d}))", }, // ── Fees & Revenue (api.llama.fi) ────────────────────────────────────── { @@ -240,7 +242,7 @@ export const TOOL_METADATA: ToolMetadata[] = [ qualified: "defillama.fees.getFeesSummary", sandboxImpl: lazyMethod("feesService", "getFeesSummaryRaw"), description: - "Fetch detailed fees/revenue metrics for a single protocol. Pass the canonical fees-protocol slug from the DefiLlama catalog. The fees catalog often uses version-suffixed slugs (e.g. a single display name like 'Uniswap' maps to multiple fees-tracked deployments like `uniswap-v2`, `uniswap-v3`, `uniswap-labs`), so discovery is required — use `defillama.resolveProtocol(name)` or enumerate `defillama.fees.getFeesOverview().protocols` and filter on `name`. Don't pass an unversioned display-name guess. Use `dataType` to choose which metric series to return.", + "Narrow endpoint for a SINGLE protocol's fees/revenue. Response includes totals (24h/7d/30d/all-time), per-chain breakdowns, and (optionally) the daily chart series — pluck the specific fields you need rather than returning the whole object. The fees catalog often uses version-suffixed slugs (one display name like 'Uniswap' maps to multiple fees-tracked deployments such as `uniswap-v2`, `uniswap-v3`, `uniswap-labs`), so discovery is required — use `defillama.resolveProtocol(name)` or enumerate `defillama.fees.getFeesOverview().protocols` and filter on `name`. Don't pass an unversioned display-name guess. Use `dataType` to choose which metric series to return.", parameters: z.object({ protocol: z .string() @@ -268,14 +270,14 @@ export const TOOL_METADATA: ToolMetadata[] = [ }), responseSchema: FeesSummarySchema, exampleCall: - "const slug = await defillama.resolveProtocol(name); await defillama.fees.getFeesSummary({protocol: slug, dataType: 'dailyRevenue'})", + "const slug = await defillama.resolveProtocol(name); const s = await defillama.fees.getFeesSummary({protocol: slug, dataType: 'dailyRevenue'}); return { name: s.name, total24h: s.total24h, total7d: s.total7d, total30d: s.total30d, totalAllTime: s.totalAllTime, change_1d: s.change_1d, change_7d: s.change_7d }", }, { name: "defillama_get_fees_overview", qualified: "defillama.fees.getFeesOverview", sandboxImpl: lazyMethod("feesService", "getFeesOverviewRaw"), description: - "Fetch a fees/revenue overview across all protocols, or scoped to one chain when `chain` is provided. Returns the full `protocols` array (sort/slice/field-pick in your execute() script). Use the chain DISPLAY NAME from `defillama.resolveChain(input).name` (case-sensitive). This is also the canonical source for discovering valid fees-protocol slugs — read the `slug` field off each entry.", + "Broad overview of protocol fees/revenue across all chains, or scoped to one chain. Returns a large `protocols` array — use for leaderboards, cross-protocol comparisons, or as the canonical source for discovering valid fees-protocol slugs (`slug` field on each entry). For a single-protocol question, prefer the resolver → `getFeesSummary({protocol: slug})` path. ALWAYS sort/filter/project inside the `execute()` sandbox before returning; never return the raw response.", parameters: z.object({ chain: z .string() @@ -304,7 +306,7 @@ export const TOOL_METADATA: ToolMetadata[] = [ }), responseSchema: FeesOverviewSchema, exampleCall: - "const {name} = await defillama.resolveChain(input); await defillama.fees.getFeesOverview({chain: name, dataType: 'dailyFees'})", + "const {name} = await defillama.resolveChain(input); const overview = await defillama.fees.getFeesOverview({chain: name, dataType: 'dailyFees'}); return (overview.protocols ?? []).sort((a,b) => (b.total24h ?? 0) - (a.total24h ?? 0)).slice(0, 20).map(p => ({slug: p.slug, name: p.name, total24h: p.total24h, change_7d: p.change_7d}))", }, // ── Options (api.llama.fi) ───────────────────────────────────────────── { @@ -312,7 +314,7 @@ export const TOOL_METADATA: ToolMetadata[] = [ qualified: "defillama.options.getOptionsSummary", sandboxImpl: lazyMethod("optionsService", "getOptionsSummaryRaw"), description: - "Fetch detailed options-protocol volume data for a single protocol. Pass the canonical options-protocol slug from the DefiLlama catalog — discover via `defillama.resolveProtocol(name)` or by enumerating `defillama.options.getOptionsOverview().protocols` and filtering on `name`. Don't construct the slug by transforming a display name. Use `dataType` to choose premium vs notional volume.", + "Narrow endpoint for a SINGLE options protocol's volume. Response includes totals (24h/7d/30d/all-time), per-chain breakdowns, and (optionally) chart series — pluck the specific fields you need rather than returning the whole object. Pass the canonical options-protocol slug — discover via `defillama.resolveProtocol(name)` or by enumerating `defillama.options.getOptionsOverview().protocols` and filtering on `name`. Don't construct the slug by transforming a display name. Use `dataType` to choose premium vs notional volume.", parameters: z.object({ protocol: z .string() @@ -328,14 +330,14 @@ export const TOOL_METADATA: ToolMetadata[] = [ }), responseSchema: OptionsSummarySchema, exampleCall: - "const slug = await defillama.resolveProtocol(name); await defillama.options.getOptionsSummary({protocol: slug})", + "const slug = await defillama.resolveProtocol(name); const s = await defillama.options.getOptionsSummary({protocol: slug}); return { name: s.name, total24h: s.total24h, total7d: s.total7d, totalAllTime: s.totalAllTime, change_7d: s.change_7d }", }, { name: "defillama_get_options_overview", qualified: "defillama.options.getOptionsOverview", sandboxImpl: lazyMethod("optionsService", "getOptionsOverviewRaw"), description: - "Fetch an options-volume overview across all options protocols, or scoped to one chain when `chain` is provided. Returns the full `protocols` array (sort/slice in your execute() script). Use the chain DISPLAY NAME from `defillama.resolveChain(input).name` (case-sensitive). This is also the canonical source for discovering valid options-protocol slugs — read the `slug` field off each entry.", + "Broad overview of options-protocol volume across all chains, or scoped to one chain. Returns a large `protocols` array — use for leaderboards or as the canonical source for discovering valid options-protocol slugs (`slug` field on each entry). For a single-protocol question, prefer the resolver → `getOptionsSummary({protocol: slug})` path. ALWAYS sort/filter/project inside the `execute()` sandbox before returning; never return the raw response.", parameters: z.object({ chain: z .string() @@ -364,7 +366,7 @@ export const TOOL_METADATA: ToolMetadata[] = [ }), responseSchema: OptionsOverviewSchema, exampleCall: - "const {name} = await defillama.resolveChain(input); await defillama.options.getOptionsOverview({chain: name})", + "const {name} = await defillama.resolveChain(input); const overview = await defillama.options.getOptionsOverview({chain: name}); return (overview.protocols ?? []).sort((a,b) => (b.total24h ?? 0) - (a.total24h ?? 0)).slice(0, 20).map(p => ({slug: p.slug, name: p.name, total24h: p.total24h, change_7d: p.change_7d}))", }, // ── Stablecoins (stablecoins.llama.fi) ───────────────────────────────── { @@ -372,7 +374,7 @@ export const TOOL_METADATA: ToolMetadata[] = [ qualified: "defillama.stablecoin.getStablecoins", sandboxImpl: lazyMethod("stablecoinService", "getStablecoinsRaw"), description: - "List all stablecoins with circulation data (and optionally current prices). Returns the full `peggedAssets` array (sort/slice/field-pick in your execute() script).", + "Broad list of all stablecoins with circulation data (optionally current prices). Returns a large `peggedAssets` array — use for leaderboards or as the canonical source for discovering stablecoin IDs (`id` field). For a single-stablecoin question, prefer `defillama.resolveStablecoin(symbol)` → `getStablecoinCharts({stablecoin: id})`. ALWAYS sort/filter/project inside the `execute()` sandbox before returning; never return the raw response.", parameters: z.object({ includePrices: z .boolean() @@ -381,17 +383,18 @@ export const TOOL_METADATA: ToolMetadata[] = [ }), responseSchema: StablecoinsSchema, exampleCall: - "await defillama.stablecoin.getStablecoins({includePrices: true})", + "const res = await defillama.stablecoin.getStablecoins({includePrices: true}); return (res.peggedAssets ?? []).slice(0, 20).map(s => ({id: s.id, symbol: s.symbol, name: s.name, circulating: s.circulating, price: s.price}))", }, { name: "defillama_get_stablecoin_chains", qualified: "defillama.stablecoin.getStablecoinChains", sandboxImpl: lazyMethod("stablecoinService", "getStablecoinChainsRaw"), description: - "List stablecoin market-cap totals broken down by chain. Returns the full array (slice/sort in your execute() script).", + "Stablecoin market-cap totals broken down by chain. Returns the full array — slice/sort/project inside the `execute()` sandbox; don't return the raw response.", parameters: z.object({}), responseSchema: StablecoinChainsSchema, - exampleCall: "await defillama.stablecoin.getStablecoinChains()", + exampleCall: + "const rows = await defillama.stablecoin.getStablecoinChains(); return rows.slice(0, 30).map(r => ({name: r.name, total: r.totalCirculatingUSD}))", }, { name: "defillama_get_stablecoin_charts", @@ -415,17 +418,18 @@ export const TOOL_METADATA: ToolMetadata[] = [ }), responseSchema: StablecoinChartsSchema, exampleCall: - "const {name} = await defillama.resolveChain(input); const id = await defillama.resolveStablecoin(symbol); await defillama.stablecoin.getStablecoinCharts({chain: name, stablecoin: id})", + "const {name} = await defillama.resolveChain(input); const id = await defillama.resolveStablecoin(symbol); const series = await defillama.stablecoin.getStablecoinCharts({chain: name, stablecoin: id}); return series.slice(-90).map(p => ({date: new Date(p.date * 1000).toISOString() /* p.date is Unix seconds; multiply by 1000 for JS Date */, totalCirculatingUSD: p.totalCirculatingUSD}))", }, { name: "defillama_get_stablecoin_prices", qualified: "defillama.stablecoin.getStablecoinPrices", sandboxImpl: lazyMethod("stablecoinService", "getStablecoinPricesRaw"), description: - "Fetch the historical stablecoin price series (per-asset prices over time). Returns the full series (slice the tail in your execute() script).", + "Historical stablecoin price series across all tracked assets. Returns a large time series — slice the tail (or project per-asset) inside the `execute()` sandbox; don't return the raw response.", parameters: z.object({}), responseSchema: StablecoinPricesSchema, - exampleCall: "await defillama.stablecoin.getStablecoinPrices()", + exampleCall: + "const series = await defillama.stablecoin.getStablecoinPrices(); return series.slice(-90).map(p => ({date: new Date(p.date * 1000).toISOString() /* p.date is Unix seconds; multiply by 1000 for JS Date */, prices: p.prices}))", }, // ── Prices (coins.llama.fi) ──────────────────────────────────────────── { @@ -448,7 +452,7 @@ export const TOOL_METADATA: ToolMetadata[] = [ }), responseSchema: CurrentPricesSchema, exampleCall: - "const {slug} = await defillama.resolveChain(input); await defillama.price.getCurrentPrices({coins: `${slug}:${tokenAddress}`})", + "const {slug} = await defillama.resolveChain(input); const key = `${slug}:${tokenAddress}`; const res = await defillama.price.getCurrentPrices({coins: key}); return res.coins?.[key]?.price", }, { name: "defillama_get_prices_first_coins", @@ -465,7 +469,7 @@ export const TOOL_METADATA: ToolMetadata[] = [ }), responseSchema: FirstPricesSchema, exampleCall: - "const {slug} = await defillama.resolveChain(input); await defillama.price.getFirstPrices({coins: `${slug}:${tokenAddress}`})", + "const {slug} = await defillama.resolveChain(input); const key = `${slug}:${tokenAddress}`; const res = await defillama.price.getFirstPrices({coins: key}); return res.coins?.[key]", }, { name: "defillama_get_batch_historical", @@ -497,7 +501,7 @@ export const TOOL_METADATA: ToolMetadata[] = [ }), responseSchema: BatchHistoricalSchema, exampleCall: - "const {slug} = await defillama.resolveChain(input); await defillama.price.getBatchHistorical({coins: {[`${slug}:${tokenAddress}`]: [timestamp]}})", + "const {slug} = await defillama.resolveChain(input); const key = `${slug}:${tokenAddress}`; const res = await defillama.price.getBatchHistorical({coins: {[key]: [timestamp]}}); return res.coins?.[key]?.prices", }, { name: "defillama_get_historical_prices_by_contract", @@ -522,7 +526,7 @@ export const TOOL_METADATA: ToolMetadata[] = [ }), responseSchema: CurrentPricesSchema, exampleCall: - "const {slug} = await defillama.resolveChain(input); await defillama.price.getHistoricalPrices({coins: `${slug}:${tokenAddress}`, timestamp})", + "const {slug} = await defillama.resolveChain(input); const key = `${slug}:${tokenAddress}`; const res = await defillama.price.getHistoricalPrices({coins: key, timestamp}); return res.coins?.[key]", }, { name: "defillama_get_percentage_coins", @@ -556,7 +560,7 @@ export const TOOL_METADATA: ToolMetadata[] = [ }), responseSchema: PercentageSchema, exampleCall: - "const {slug} = await defillama.resolveChain(input); await defillama.price.getPercentageChange({coins: `${slug}:${tokenAddress}`, period: '7d'})", + "const {slug} = await defillama.resolveChain(input); const key = `${slug}:${tokenAddress}`; const res = await defillama.price.getPercentageChange({coins: key, period: '7d'}); return res.coins?.[key]", }, { name: "defillama_get_chart_coins", @@ -602,7 +606,7 @@ export const TOOL_METADATA: ToolMetadata[] = [ }), responseSchema: PriceChartSchema, exampleCall: - "const {slug} = await defillama.resolveChain(input); await defillama.price.getPriceChart({coins: `${slug}:${tokenAddress}`, span: 10, period: '1d'})", + "const {slug} = await defillama.resolveChain(input); const key = `${slug}:${tokenAddress}`; const res = await defillama.price.getPriceChart({coins: key, span: 30, period: '1d'}); return res.coins?.[key]?.prices ?? []", }, // ── Yields (yields.llama.fi) ─────────────────────────────────────────── { @@ -610,10 +614,11 @@ export const TOOL_METADATA: ToolMetadata[] = [ qualified: "defillama.yield.getLatestPools", sandboxImpl: lazyMethod("yieldService", "getLatestPoolsRaw"), description: - "List current yield-farming pools with APY, TVL and reward metrics. Returns the full `data` array (sort/slice/field-pick in your execute() script). Each pool's `pool` UUID feeds getHistoricalPoolData.", + "Broad list of every current yield-farming pool with APY, TVL and reward metrics. The `data` array is large (thousands of pools across all chains/protocols) — use for cross-pool screens, leaderboards, or as the catalog for discovering the `pool` UUID before calling `getHistoricalPoolData`. ALWAYS filter (by chain/project/symbol/apy threshold) + sort + slice + project inside the `execute()` sandbox; never return the raw response.", parameters: z.object({}), responseSchema: PoolsSchema, - exampleCall: "await defillama.yield.getLatestPools()", + exampleCall: + "const pools = await defillama.yield.getLatestPools(); return (pools.data ?? []).sort((a,b) => (b.apy ?? 0) - (a.apy ?? 0)).slice(0, 20).map(p => ({pool: p.pool, project: p.project, symbol: p.symbol, chain: p.chain, apy: p.apy, tvlUsd: p.tvlUsd}))", }, { name: "defillama_get_historical_pool_data", @@ -630,7 +635,7 @@ export const TOOL_METADATA: ToolMetadata[] = [ }), responseSchema: HistoricalPoolSchema, exampleCall: - "const pools = await defillama.yield.getLatestPools(); const id = pools.data.find(p => /* match on project/symbol/chain */).pool; await defillama.yield.getHistoricalPoolData({pool: id})", + "const pools = await defillama.yield.getLatestPools(); const id = (pools.data ?? []).find(p => /* match on project/symbol/chain */)?.pool; if (!id) return { error: 'Pool not found' }; const series = await defillama.yield.getHistoricalPoolData({pool: id}); return (series.data ?? []).slice(-90).map(p => ({timestamp: p.timestamp /* already an ISO string, unlike the date:number fields on /v2 endpoints */, apy: p.apy, tvlUsd: p.tvlUsd}))", }, // ── Blockchain (coins.llama.fi) ──────────────────────────────────────── { diff --git a/src/mcp/instructions/instructions.generated.ts b/src/mcp/instructions/instructions.generated.ts index 84e18ab..b40f29f 100644 --- a/src/mcp/instructions/instructions.generated.ts +++ b/src/mcp/instructions/instructions.generated.ts @@ -99,6 +99,51 @@ const coins = \`\${slug}:\${tokenAddress},coingecko:\${nativeCoinId}\`; await defillama.price.getCurrentPrices({ coins }); \`\`\` +## Shape responses inside \`execute()\` — don't ship raw payloads back + +The \`execute()\` sandbox is where you trim, project, and shape. Its return +value is what crosses back to your context — broad endpoints +(\`getProtocols\`, \`getDexsOverview\`, \`getLatestPools\`, \`getStablecoins\`, all +the \`*Overview\` siblings) routinely return multi-MB payloads with thousands +of entries. **Never return the raw response.** The Q1-class context-overflow +and Q3/Q7-class minutes-spent-post-processing failure modes both come from +shipping unshaped data back. + +Three patterns cover almost every case: + +1. **Lists** — sort, slice, project the fields you need: + \`\`\`js + const protocols = await defillama.protocol.getProtocols(); + return protocols + .sort((a, b) => (b.tvl ?? 0) - (a.tvl ?? 0)) + .slice(0, 20) + .map(p => ({ slug: p.slug, name: p.name, tvl: p.tvl, chains: p.chains })); + \`\`\` + +2. **Time-series** — slice the tail you care about, drop the extra fields: + \`\`\`js + const series = await defillama.protocol.getHistoricalChainTvl({ chain: name }); + return series.slice(-90).map(p => ({ date: p.date, tvl: p.tvl })); + \`\`\` + +3. **Single-entity summaries** — pluck only the fields the question needs: + \`\`\`js + const s = await defillama.dex.getDexSummary({ protocol: slug }); + return { name: s.name, total24h: s.total24h, change_7d: s.change_7d }; + \`\`\` + +For the \`price.*\` family, responses are keyed under \`coins[chainSlug:address]\` +— pluck via \`res.coins?.[key]?.price\` (or \`.prices\` for the chart endpoints). + +**Prefer the narrow endpoint when one exists.** \`getProtocol({slug})\` is the +right tool for "what is X's TVL"; \`getProtocols()\` is for cross-protocol +aggregates and the resolver fallback only. Same pattern: \`getDexSummary\` vs +\`getDexsOverview\`, \`getFeesSummary\` vs \`getFeesOverview\`, \`getOptionsSummary\` +vs \`getOptionsOverview\`. When you do need a broad endpoint (genuine +aggregate question, or \`resolveProtocol(name)\` returned null), still +project/filter inside \`execute()\` before returning so the raw catalog never +crosses back. + ## IQ Gateway vs. direct If \`IQ_GATEWAY_URL\` and \`IQ_GATEWAY_KEY\` are set, upstream calls route through diff --git a/src/mcp/instructions/instructions.md b/src/mcp/instructions/instructions.md index 2a578d7..e3c220b 100644 --- a/src/mcp/instructions/instructions.md +++ b/src/mcp/instructions/instructions.md @@ -96,6 +96,51 @@ const coins = `${slug}:${tokenAddress},coingecko:${nativeCoinId}`; await defillama.price.getCurrentPrices({ coins }); ``` +## Shape responses inside `execute()` — don't ship raw payloads back + +The `execute()` sandbox is where you trim, project, and shape. Its return +value is what crosses back to your context — broad endpoints +(`getProtocols`, `getDexsOverview`, `getLatestPools`, `getStablecoins`, all +the `*Overview` siblings) routinely return multi-MB payloads with thousands +of entries. **Never return the raw response.** The Q1-class context-overflow +and Q3/Q7-class minutes-spent-post-processing failure modes both come from +shipping unshaped data back. + +Three patterns cover almost every case: + +1. **Lists** — sort, slice, project the fields you need: + ```js + const protocols = await defillama.protocol.getProtocols(); + return protocols + .sort((a, b) => (b.tvl ?? 0) - (a.tvl ?? 0)) + .slice(0, 20) + .map(p => ({ slug: p.slug, name: p.name, tvl: p.tvl, chains: p.chains })); + ``` + +2. **Time-series** — slice the tail you care about, drop the extra fields: + ```js + const series = await defillama.protocol.getHistoricalChainTvl({ chain: name }); + return series.slice(-90).map(p => ({ date: p.date, tvl: p.tvl })); + ``` + +3. **Single-entity summaries** — pluck only the fields the question needs: + ```js + const s = await defillama.dex.getDexSummary({ protocol: slug }); + return { name: s.name, total24h: s.total24h, change_7d: s.change_7d }; + ``` + +For the `price.*` family, responses are keyed under `coins[chainSlug:address]` +— pluck via `res.coins?.[key]?.price` (or `.prices` for the chart endpoints). + +**Prefer the narrow endpoint when one exists.** `getProtocol({slug})` is the +right tool for "what is X's TVL"; `getProtocols()` is for cross-protocol +aggregates and the resolver fallback only. Same pattern: `getDexSummary` vs +`getDexsOverview`, `getFeesSummary` vs `getFeesOverview`, `getOptionsSummary` +vs `getOptionsOverview`. When you do need a broad endpoint (genuine +aggregate question, or `resolveProtocol(name)` returned null), still +project/filter inside `execute()` before returning so the raw catalog never +crosses back. + ## IQ Gateway vs. direct If `IQ_GATEWAY_URL` and `IQ_GATEWAY_KEY` are set, upstream calls route through diff --git a/src/mcp/search-docs/embedded-index.ts b/src/mcp/search-docs/embedded-index.ts index 7cfd2c1..45525e9 100644 --- a/src/mcp/search-docs/embedded-index.ts +++ b/src/mcp/search-docs/embedded-index.ts @@ -25,35 +25,37 @@ export const ENTRIES: IndexEntry[] = [ name: "defillama_get_chains", qualified: "defillama.protocol.getChains", description: - "List every chain DefiLlama tracks, each with its current TVL. Returns the full array (sort/slice in your execute() script). This is the canonical chain catalog — read the `name` field for api.llama.fi endpoints (case-sensitive display name) and lowercase that same value for coins.llama.fi price/block calls. Prefer `defillama.resolveChain(input)` for translating a human input to the right form; fall back to enumerating this list when the resolver returns null.", + "Broad list of every chain DefiLlama tracks (~200 entries with current TVL). Use for cross-chain aggregate queries (TVL leaderboards, chain discovery) or as the resolver fallback. For 'what is X chain's TVL', prefer `defillama.resolveChain(input)` → `getHistoricalChainTvl({chain})` instead. Read `name` for api.llama.fi endpoints; lowercase the same value for coins.llama.fi. ALWAYS sort/filter/project inside the `execute()` sandbox — never return the raw catalog.", params: { $schema: "https://json-schema.org/draft/2020-12/schema", type: "object", properties: {}, additionalProperties: false, }, - exampleCall: "await defillama.protocol.getChains()", + exampleCall: + "const chains = await defillama.protocol.getChains(); return chains.sort((a,b) => (b.tvl ?? 0) - (a.tvl ?? 0)).slice(0, 20).map(c => ({name: c.name, tvl: c.tvl}))", }, { kind: "method", name: "defillama_get_protocols", qualified: "defillama.protocol.getProtocols", description: - "List every DeFi protocol DefiLlama tracks with its TVL, category, chains and recent change metrics. Returns the full unsorted array (sort/slice/field-pick in your execute() script).", + "Returns the FULL DefiLlama protocol catalog (~3k entries, several MB of JSON). Use ONLY for cross-protocol aggregate queries (leaderboards, category rollups, by-chain counts) or as the discovery fallback when `defillama.resolveProtocol(name)` returns null. For a single-protocol question ('what is X's TVL?'), prefer the resolver → `getProtocol({protocol: slug})` path — calling this endpoint and returning the raw response blows the agent's context. ALWAYS project/filter/sort/slice inside the `execute()` sandbox before returning.", params: { $schema: "https://json-schema.org/draft/2020-12/schema", type: "object", properties: {}, additionalProperties: false, }, - exampleCall: "await defillama.protocol.getProtocols()", + exampleCall: + "const protocols = await defillama.protocol.getProtocols(); return protocols.sort((a,b) => (b.tvl ?? 0) - (a.tvl ?? 0)).slice(0, 20).map(p => ({slug: p.slug, name: p.name, tvl: p.tvl, chains: p.chains, change_7d: p.change_7d}))", }, { kind: "method", name: "defillama_get_protocol", qualified: "defillama.protocol.getProtocol", description: - "Fetch detailed TVL data for a single DeFi protocol, including per-chain TVL breakdowns and historical series. Pass the canonical protocol slug from the DefiLlama catalog. Slugs are kebab-case lowercase with version suffixes preserved and are NOT derivable from display names — discover via `defillama.resolveProtocol(name)` or by enumerating `defillama.protocol.getProtocols()` and filtering on `name` (see the find-protocol-slug recipe). Don't construct the slug by transforming a display name.", + "Narrow endpoint for a SINGLE protocol's TVL, per-chain breakdown, and historical series. Preferred over `getProtocols()` for any 'what is X' question. The response is large (per-chain breakdowns + full historical TVL series); pluck the fields you actually need rather than returning the whole object. Pass the canonical kebab-case slug from the DefiLlama catalog — discover via `defillama.resolveProtocol(name)` or by enumerating `defillama.protocol.getProtocols()` and filtering on `name` (see the find-protocol-slug recipe). Slugs are NOT derivable from display names.", params: { $schema: "https://json-schema.org/draft/2020-12/schema", type: "object", @@ -68,7 +70,7 @@ export const ENTRIES: IndexEntry[] = [ additionalProperties: false, }, exampleCall: - "const slug = await defillama.resolveProtocol(name); await defillama.protocol.getProtocol({protocol: slug})", + "const slug = await defillama.resolveProtocol(name); const p = await defillama.protocol.getProtocol({protocol: slug}); const last = p.tvl?.[p.tvl.length - 1]; return { name: p.name, currentTvl: last?.totalLiquidityUSD, asOf: last?.date, chains: p.chains }", }, { kind: "method", @@ -89,14 +91,14 @@ export const ENTRIES: IndexEntry[] = [ additionalProperties: false, }, exampleCall: - "const {name} = await defillama.resolveChain(input); await defillama.protocol.getHistoricalChainTvl({chain: name})", + "const {name} = await defillama.resolveChain(input); const series = await defillama.protocol.getHistoricalChainTvl({chain: name}); return series.slice(-90).map(p => ({date: new Date(p.date * 1000).toISOString() /* p.date is Unix seconds; multiply by 1000 for JS Date */, tvl: p.tvl}))", }, { kind: "method", name: "defillama_get_dex_summary", qualified: "defillama.dex.getDexSummary", description: - "Fetch detailed DEX trading-volume data for a single DEX protocol, including totals and (optionally) the volume chart series. Pass the canonical DEX protocol slug from the DefiLlama catalog. Slugs are kebab-case and version-suffixed and NOT derivable from display names — discover via `defillama.resolveProtocol(name)` or by enumerating `defillama.dex.getDexsOverview().protocols` and filtering on `name`. Don't construct the slug by transforming a display name.", + "Narrow endpoint for a SINGLE DEX protocol's trading volume. Response has 30+ fields covering totals (24h/7d/30d/all-time), percentage changes, per-chain breakdowns, and (optionally) the volume chart series — pluck the specific fields you need rather than returning the whole object. Pass the canonical kebab-case DEX slug — discover via `defillama.resolveProtocol(name)` or by enumerating `defillama.dex.getDexsOverview().protocols` and filtering on `name`. Slugs are NOT derivable from display names. Each catalog (DEX, revenue, options) has its own slug namespace — a slug valid in one may not exist in another.", params: { $schema: "https://json-schema.org/draft/2020-12/schema", type: "object", @@ -121,14 +123,14 @@ export const ENTRIES: IndexEntry[] = [ additionalProperties: false, }, exampleCall: - "const slug = await defillama.resolveProtocol(name); await defillama.dex.getDexSummary({protocol: slug})", + "const slug = await defillama.resolveProtocol(name); const s = await defillama.dex.getDexSummary({protocol: slug}); return { name: s.name, total24h: s.total24h, total7d: s.total7d, total30d: s.total30d, totalAllTime: s.totalAllTime, change_1d: s.change_1d, change_7d: s.change_7d }", }, { kind: "method", name: "defillama_get_dexs_overview", qualified: "defillama.dex.getDexsOverview", description: - "Fetch a DEX volume overview across all DEXs, or scoped to one chain when `chain` is provided. Returns the full `protocols` array (sort/slice/field-pick in your execute() script). Use the chain DISPLAY NAME from `defillama.resolveChain(input).name` (case-sensitive).", + "Broad overview of DEX volume across all DEXs, or scoped to one chain. Returns a large `protocols` array — use for leaderboards, cross-DEX comparisons, or as the canonical source for discovering valid DEX slugs (`slug` field on each entry). For a single-DEX question, prefer the resolver → `getDexSummary({protocol: slug})` path. ALWAYS sort/filter/project inside the `execute()` sandbox before returning; never return the raw response.", params: { $schema: "https://json-schema.org/draft/2020-12/schema", type: "object", @@ -152,14 +154,14 @@ export const ENTRIES: IndexEntry[] = [ additionalProperties: false, }, exampleCall: - "const {name} = await defillama.resolveChain(input); await defillama.dex.getDexsOverview({chain: name})", + "const {name} = await defillama.resolveChain(input); const overview = await defillama.dex.getDexsOverview({chain: name}); return (overview.protocols ?? []).sort((a,b) => (b.total24h ?? 0) - (a.total24h ?? 0)).slice(0, 20).map(p => ({slug: p.slug, name: p.name, total24h: p.total24h, change_7d: p.change_7d}))", }, { kind: "method", name: "defillama_get_fees_summary", qualified: "defillama.fees.getFeesSummary", description: - "Fetch detailed fees/revenue metrics for a single protocol. Pass the canonical fees-protocol slug from the DefiLlama catalog. The fees catalog often uses version-suffixed slugs (e.g. a single display name like 'Uniswap' maps to multiple fees-tracked deployments like `uniswap-v2`, `uniswap-v3`, `uniswap-labs`), so discovery is required — use `defillama.resolveProtocol(name)` or enumerate `defillama.fees.getFeesOverview().protocols` and filter on `name`. Don't pass an unversioned display-name guess. Use `dataType` to choose which metric series to return.", + "Narrow endpoint for a SINGLE protocol's fees/revenue. Response includes totals (24h/7d/30d/all-time), per-chain breakdowns, and (optionally) the daily chart series — pluck the specific fields you need rather than returning the whole object. The fees catalog often uses version-suffixed slugs (one display name like 'Uniswap' maps to multiple fees-tracked deployments such as `uniswap-v2`, `uniswap-v3`, `uniswap-labs`), so discovery is required — use `defillama.resolveProtocol(name)` or enumerate `defillama.fees.getFeesOverview().protocols` and filter on `name`. Don't pass an unversioned display-name guess. Use `dataType` to choose which metric series to return.", params: { $schema: "https://json-schema.org/draft/2020-12/schema", type: "object", @@ -190,14 +192,14 @@ export const ENTRIES: IndexEntry[] = [ additionalProperties: false, }, exampleCall: - "const slug = await defillama.resolveProtocol(name); await defillama.fees.getFeesSummary({protocol: slug, dataType: 'dailyRevenue'})", + "const slug = await defillama.resolveProtocol(name); const s = await defillama.fees.getFeesSummary({protocol: slug, dataType: 'dailyRevenue'}); return { name: s.name, total24h: s.total24h, total7d: s.total7d, total30d: s.total30d, totalAllTime: s.totalAllTime, change_1d: s.change_1d, change_7d: s.change_7d }", }, { kind: "method", name: "defillama_get_fees_overview", qualified: "defillama.fees.getFeesOverview", description: - "Fetch a fees/revenue overview across all protocols, or scoped to one chain when `chain` is provided. Returns the full `protocols` array (sort/slice/field-pick in your execute() script). Use the chain DISPLAY NAME from `defillama.resolveChain(input).name` (case-sensitive). This is also the canonical source for discovering valid fees-protocol slugs — read the `slug` field off each entry.", + "Broad overview of protocol fees/revenue across all chains, or scoped to one chain. Returns a large `protocols` array — use for leaderboards, cross-protocol comparisons, or as the canonical source for discovering valid fees-protocol slugs (`slug` field on each entry). For a single-protocol question, prefer the resolver → `getFeesSummary({protocol: slug})` path. ALWAYS sort/filter/project inside the `execute()` sandbox before returning; never return the raw response.", params: { $schema: "https://json-schema.org/draft/2020-12/schema", type: "object", @@ -227,14 +229,14 @@ export const ENTRIES: IndexEntry[] = [ additionalProperties: false, }, exampleCall: - "const {name} = await defillama.resolveChain(input); await defillama.fees.getFeesOverview({chain: name, dataType: 'dailyFees'})", + "const {name} = await defillama.resolveChain(input); const overview = await defillama.fees.getFeesOverview({chain: name, dataType: 'dailyFees'}); return (overview.protocols ?? []).sort((a,b) => (b.total24h ?? 0) - (a.total24h ?? 0)).slice(0, 20).map(p => ({slug: p.slug, name: p.name, total24h: p.total24h, change_7d: p.change_7d}))", }, { kind: "method", name: "defillama_get_options_summary", qualified: "defillama.options.getOptionsSummary", description: - "Fetch detailed options-protocol volume data for a single protocol. Pass the canonical options-protocol slug from the DefiLlama catalog — discover via `defillama.resolveProtocol(name)` or by enumerating `defillama.options.getOptionsOverview().protocols` and filtering on `name`. Don't construct the slug by transforming a display name. Use `dataType` to choose premium vs notional volume.", + "Narrow endpoint for a SINGLE options protocol's volume. Response includes totals (24h/7d/30d/all-time), per-chain breakdowns, and (optionally) chart series — pluck the specific fields you need rather than returning the whole object. Pass the canonical options-protocol slug — discover via `defillama.resolveProtocol(name)` or by enumerating `defillama.options.getOptionsOverview().protocols` and filtering on `name`. Don't construct the slug by transforming a display name. Use `dataType` to choose premium vs notional volume.", params: { $schema: "https://json-schema.org/draft/2020-12/schema", type: "object", @@ -255,14 +257,14 @@ export const ENTRIES: IndexEntry[] = [ additionalProperties: false, }, exampleCall: - "const slug = await defillama.resolveProtocol(name); await defillama.options.getOptionsSummary({protocol: slug})", + "const slug = await defillama.resolveProtocol(name); const s = await defillama.options.getOptionsSummary({protocol: slug}); return { name: s.name, total24h: s.total24h, total7d: s.total7d, totalAllTime: s.totalAllTime, change_7d: s.change_7d }", }, { kind: "method", name: "defillama_get_options_overview", qualified: "defillama.options.getOptionsOverview", description: - "Fetch an options-volume overview across all options protocols, or scoped to one chain when `chain` is provided. Returns the full `protocols` array (sort/slice in your execute() script). Use the chain DISPLAY NAME from `defillama.resolveChain(input).name` (case-sensitive). This is also the canonical source for discovering valid options-protocol slugs — read the `slug` field off each entry.", + "Broad overview of options-protocol volume across all chains, or scoped to one chain. Returns a large `protocols` array — use for leaderboards or as the canonical source for discovering valid options-protocol slugs (`slug` field on each entry). For a single-protocol question, prefer the resolver → `getOptionsSummary({protocol: slug})` path. ALWAYS sort/filter/project inside the `execute()` sandbox before returning; never return the raw response.", params: { $schema: "https://json-schema.org/draft/2020-12/schema", type: "object", @@ -292,14 +294,14 @@ export const ENTRIES: IndexEntry[] = [ additionalProperties: false, }, exampleCall: - "const {name} = await defillama.resolveChain(input); await defillama.options.getOptionsOverview({chain: name})", + "const {name} = await defillama.resolveChain(input); const overview = await defillama.options.getOptionsOverview({chain: name}); return (overview.protocols ?? []).sort((a,b) => (b.total24h ?? 0) - (a.total24h ?? 0)).slice(0, 20).map(p => ({slug: p.slug, name: p.name, total24h: p.total24h, change_7d: p.change_7d}))", }, { kind: "method", name: "defillama_get_stablecoin", qualified: "defillama.stablecoin.getStablecoins", description: - "List all stablecoins with circulation data (and optionally current prices). Returns the full `peggedAssets` array (sort/slice/field-pick in your execute() script).", + "Broad list of all stablecoins with circulation data (optionally current prices). Returns a large `peggedAssets` array — use for leaderboards or as the canonical source for discovering stablecoin IDs (`id` field). For a single-stablecoin question, prefer `defillama.resolveStablecoin(symbol)` → `getStablecoinCharts({stablecoin: id})`. ALWAYS sort/filter/project inside the `execute()` sandbox before returning; never return the raw response.", params: { $schema: "https://json-schema.org/draft/2020-12/schema", type: "object", @@ -312,21 +314,22 @@ export const ENTRIES: IndexEntry[] = [ additionalProperties: false, }, exampleCall: - "await defillama.stablecoin.getStablecoins({includePrices: true})", + "const res = await defillama.stablecoin.getStablecoins({includePrices: true}); return (res.peggedAssets ?? []).slice(0, 20).map(s => ({id: s.id, symbol: s.symbol, name: s.name, circulating: s.circulating, price: s.price}))", }, { kind: "method", name: "defillama_get_stablecoin_chains", qualified: "defillama.stablecoin.getStablecoinChains", description: - "List stablecoin market-cap totals broken down by chain. Returns the full array (slice/sort in your execute() script).", + "Stablecoin market-cap totals broken down by chain. Returns the full array — slice/sort/project inside the `execute()` sandbox; don't return the raw response.", params: { $schema: "https://json-schema.org/draft/2020-12/schema", type: "object", properties: {}, additionalProperties: false, }, - exampleCall: "await defillama.stablecoin.getStablecoinChains()", + exampleCall: + "const rows = await defillama.stablecoin.getStablecoinChains(); return rows.slice(0, 30).map(r => ({name: r.name, total: r.totalCirculatingUSD}))", }, { kind: "method", @@ -361,21 +364,22 @@ export const ENTRIES: IndexEntry[] = [ additionalProperties: false, }, exampleCall: - "const {name} = await defillama.resolveChain(input); const id = await defillama.resolveStablecoin(symbol); await defillama.stablecoin.getStablecoinCharts({chain: name, stablecoin: id})", + "const {name} = await defillama.resolveChain(input); const id = await defillama.resolveStablecoin(symbol); const series = await defillama.stablecoin.getStablecoinCharts({chain: name, stablecoin: id}); return series.slice(-90).map(p => ({date: new Date(p.date * 1000).toISOString() /* p.date is Unix seconds; multiply by 1000 for JS Date */, totalCirculatingUSD: p.totalCirculatingUSD}))", }, { kind: "method", name: "defillama_get_stablecoin_prices", qualified: "defillama.stablecoin.getStablecoinPrices", description: - "Fetch the historical stablecoin price series (per-asset prices over time). Returns the full series (slice the tail in your execute() script).", + "Historical stablecoin price series across all tracked assets. Returns a large time series — slice the tail (or project per-asset) inside the `execute()` sandbox; don't return the raw response.", params: { $schema: "https://json-schema.org/draft/2020-12/schema", type: "object", properties: {}, additionalProperties: false, }, - exampleCall: "await defillama.stablecoin.getStablecoinPrices()", + exampleCall: + "const series = await defillama.stablecoin.getStablecoinPrices(); return series.slice(-90).map(p => ({date: new Date(p.date * 1000).toISOString() /* p.date is Unix seconds; multiply by 1000 for JS Date */, prices: p.prices}))", }, { kind: "method", @@ -409,7 +413,7 @@ export const ENTRIES: IndexEntry[] = [ additionalProperties: false, }, exampleCall: - "const {slug} = await defillama.resolveChain(input); await defillama.price.getCurrentPrices({coins: `${slug}:${tokenAddress}`})", + "const {slug} = await defillama.resolveChain(input); const key = `${slug}:${tokenAddress}`; const res = await defillama.price.getCurrentPrices({coins: key}); return res.coins?.[key]?.price", }, { kind: "method", @@ -431,7 +435,7 @@ export const ENTRIES: IndexEntry[] = [ additionalProperties: false, }, exampleCall: - "const {slug} = await defillama.resolveChain(input); await defillama.price.getFirstPrices({coins: `${slug}:${tokenAddress}`})", + "const {slug} = await defillama.resolveChain(input); const key = `${slug}:${tokenAddress}`; const res = await defillama.price.getFirstPrices({coins: key}); return res.coins?.[key]", }, { kind: "method", @@ -493,7 +497,7 @@ export const ENTRIES: IndexEntry[] = [ additionalProperties: false, }, exampleCall: - "const {slug} = await defillama.resolveChain(input); await defillama.price.getBatchHistorical({coins: {[`${slug}:${tokenAddress}`]: [timestamp]}})", + "const {slug} = await defillama.resolveChain(input); const key = `${slug}:${tokenAddress}`; const res = await defillama.price.getBatchHistorical({coins: {[key]: [timestamp]}}); return res.coins?.[key]?.prices", }, { kind: "method", @@ -542,7 +546,7 @@ export const ENTRIES: IndexEntry[] = [ additionalProperties: false, }, exampleCall: - "const {slug} = await defillama.resolveChain(input); await defillama.price.getHistoricalPrices({coins: `${slug}:${tokenAddress}`, timestamp})", + "const {slug} = await defillama.resolveChain(input); const key = `${slug}:${tokenAddress}`; const res = await defillama.price.getHistoricalPrices({coins: key, timestamp}); return res.coins?.[key]", }, { kind: "method", @@ -589,7 +593,7 @@ export const ENTRIES: IndexEntry[] = [ additionalProperties: false, }, exampleCall: - "const {slug} = await defillama.resolveChain(input); await defillama.price.getPercentageChange({coins: `${slug}:${tokenAddress}`, period: '7d'})", + "const {slug} = await defillama.resolveChain(input); const key = `${slug}:${tokenAddress}`; const res = await defillama.price.getPercentageChange({coins: key, period: '7d'}); return res.coins?.[key]", }, { kind: "method", @@ -665,21 +669,22 @@ export const ENTRIES: IndexEntry[] = [ additionalProperties: false, }, exampleCall: - "const {slug} = await defillama.resolveChain(input); await defillama.price.getPriceChart({coins: `${slug}:${tokenAddress}`, span: 10, period: '1d'})", + "const {slug} = await defillama.resolveChain(input); const key = `${slug}:${tokenAddress}`; const res = await defillama.price.getPriceChart({coins: key, span: 30, period: '1d'}); return res.coins?.[key]?.prices ?? []", }, { kind: "method", name: "defillama_get_latest_pool_data", qualified: "defillama.yield.getLatestPools", description: - "List current yield-farming pools with APY, TVL and reward metrics. Returns the full `data` array (sort/slice/field-pick in your execute() script). Each pool's `pool` UUID feeds getHistoricalPoolData.", + "Broad list of every current yield-farming pool with APY, TVL and reward metrics. The `data` array is large (thousands of pools across all chains/protocols) — use for cross-pool screens, leaderboards, or as the catalog for discovering the `pool` UUID before calling `getHistoricalPoolData`. ALWAYS filter (by chain/project/symbol/apy threshold) + sort + slice + project inside the `execute()` sandbox; never return the raw response.", params: { $schema: "https://json-schema.org/draft/2020-12/schema", type: "object", properties: {}, additionalProperties: false, }, - exampleCall: "await defillama.yield.getLatestPools()", + exampleCall: + "const pools = await defillama.yield.getLatestPools(); return (pools.data ?? []).sort((a,b) => (b.apy ?? 0) - (a.apy ?? 0)).slice(0, 20).map(p => ({pool: p.pool, project: p.project, symbol: p.symbol, chain: p.chain, apy: p.apy, tvlUsd: p.tvlUsd}))", }, { kind: "method", @@ -701,7 +706,7 @@ export const ENTRIES: IndexEntry[] = [ additionalProperties: false, }, exampleCall: - "const pools = await defillama.yield.getLatestPools(); const id = pools.data.find(p => /* match on project/symbol/chain */).pool; await defillama.yield.getHistoricalPoolData({pool: id})", + "const pools = await defillama.yield.getLatestPools(); const id = (pools.data ?? []).find(p => /* match on project/symbol/chain */)?.pool; if (!id) return { error: 'Pool not found' }; const series = await defillama.yield.getHistoricalPoolData({pool: id}); return (series.data ?? []).slice(-90).map(p => ({timestamp: p.timestamp /* already an ISO string, unlike the date:number fields on /v2 endpoints */, apy: p.apy, tvlUsd: p.tvlUsd}))", }, { kind: "method", From c4352ad96c330f44679e9f5d59163415b5a509f4 Mon Sep 17 00:00:00 2001 From: Aliu Salaudeen Date: Tue, 23 Jun 2026 17:08:19 +0100 Subject: [PATCH 2/7] style(catalog): use Array.prototype.at(-1) in getProtocol example MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Switches `p.tvl?.[p.tvl.length - 1]` to the cleaner `p.tvl?.at(-1)` in the getProtocol exampleCall. Two improvements: - Doesn't reference `p.tvl` twice (one optional chain instead of two reads). - Uses the canonical ES2022 idiom for "last element". Note on the reviewer's stated reason — the bot claimed the original `p.tvl?.[p.tvl.length - 1]` would TypeError when `p.tvl` is nullish. That's incorrect: optional chaining short-circuits BEFORE evaluating the index expression (verified empirically with `node -e` against null / undefined / empty array / populated array cases — all four return undefined or the expected entry, no throw). But the cleaner form is still worth taking on code-quality grounds. 171 tests still pass. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/mcp/catalog/tool-metadata.ts | 2 +- src/mcp/search-docs/embedded-index.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/mcp/catalog/tool-metadata.ts b/src/mcp/catalog/tool-metadata.ts index 330d1f9..97ad144 100644 --- a/src/mcp/catalog/tool-metadata.ts +++ b/src/mcp/catalog/tool-metadata.ts @@ -156,7 +156,7 @@ export const TOOL_METADATA: ToolMetadata[] = [ }), responseSchema: ProtocolSchema, exampleCall: - "const slug = await defillama.resolveProtocol(name); const p = await defillama.protocol.getProtocol({protocol: slug}); const last = p.tvl?.[p.tvl.length - 1]; return { name: p.name, currentTvl: last?.totalLiquidityUSD, asOf: last?.date, chains: p.chains }", + "const slug = await defillama.resolveProtocol(name); const p = await defillama.protocol.getProtocol({protocol: slug}); const last = p.tvl?.at(-1); return { name: p.name, currentTvl: last?.totalLiquidityUSD, asOf: last?.date, chains: p.chains }", }, { name: "defillama_get_historical_chain_tvl", diff --git a/src/mcp/search-docs/embedded-index.ts b/src/mcp/search-docs/embedded-index.ts index 45525e9..b51b226 100644 --- a/src/mcp/search-docs/embedded-index.ts +++ b/src/mcp/search-docs/embedded-index.ts @@ -70,7 +70,7 @@ export const ENTRIES: IndexEntry[] = [ additionalProperties: false, }, exampleCall: - "const slug = await defillama.resolveProtocol(name); const p = await defillama.protocol.getProtocol({protocol: slug}); const last = p.tvl?.[p.tvl.length - 1]; return { name: p.name, currentTvl: last?.totalLiquidityUSD, asOf: last?.date, chains: p.chains }", + "const slug = await defillama.resolveProtocol(name); const p = await defillama.protocol.getProtocol({protocol: slug}); const last = p.tvl?.at(-1); return { name: p.name, currentTvl: last?.totalLiquidityUSD, asOf: last?.date, chains: p.chains }", }, { kind: "method", From a5f38dc67a9014e7b41559156ab64030191e87eb Mon Sep 17 00:00:00 2001 From: Aliu Salaudeen Date: Tue, 23 Jun 2026 17:20:35 +0100 Subject: [PATCH 3/7] fix(catalog): guard every resolver-using example against null returns MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit defillama.resolveChain / resolveProtocol / resolveStablecoin all return null when the input doesn't match the upstream catalog. The exampleCalls were either destructuring the result (which TypeErrors on null) or passing it straight through to a Zod-typed endpoint (which fails validation). Either way the agent ends up with a confusing error envelope instead of a graceful short-circuit. Apply the early-return guard uniformly across all 16 resolver-using exampleCalls so the pattern is consistent end-to-end and matches the one already established for the pool-UUID lookup in getHistoricalPoolData: Protocol slug: const slug = await defillama.resolveProtocol(name); if (!slug) return { error: 'Protocol not found' }; ... Chain (replaces destructure form): const chain = await defillama.resolveChain(input); if (!chain) return { error: 'Chain not found' }; ... chain.name / chain.slug ... Stablecoin id: const id = await defillama.resolveStablecoin(symbol); if (!id) return { error: 'Stablecoin not found' }; ... Sites updated (16 exampleCalls): - getProtocol, getDexSummary, getFeesSummary, getOptionsSummary (resolveProtocol guards) - getHistoricalChainTvl, getDexsOverview, getFeesOverview, getOptionsOverview (resolveChain — destructure replaced) - getStablecoinCharts (resolveChain + resolveStablecoin, two guards) - getCurrentPrices, getFirstPrices, getBatchHistorical, getHistoricalPrices, getPercentageChange, getPriceChart (all six price.* endpoints — resolveChain destructure replaced) - getBlockAtTimestamp (resolveChain destructure replaced) Bot also called out a `return` was missing before the `getBlockAtTimestamp` call (it wasn't returning a value previously — example dropped the result). Added the explicit `return` while in the area so the example actually surfaces the block height to the agent. Counts verified: 12 resolveChain guards, 4 resolveProtocol guards, 1 resolveStablecoin guard. Zero destructure-from-null patterns remaining in exampleCalls. Caught by gemini-code-assist[bot] on PR #32 (3 of the 16 sites explicitly; the rest applied for consistency). Same null-guard discipline as the pool-UUID guard from PR #28. 171 tests still pass. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/mcp/catalog/tool-metadata.ts | 32 +++++++++++++-------------- src/mcp/search-docs/embedded-index.ts | 32 +++++++++++++-------------- 2 files changed, 32 insertions(+), 32 deletions(-) diff --git a/src/mcp/catalog/tool-metadata.ts b/src/mcp/catalog/tool-metadata.ts index 97ad144..8da5e1d 100644 --- a/src/mcp/catalog/tool-metadata.ts +++ b/src/mcp/catalog/tool-metadata.ts @@ -156,7 +156,7 @@ export const TOOL_METADATA: ToolMetadata[] = [ }), responseSchema: ProtocolSchema, exampleCall: - "const slug = await defillama.resolveProtocol(name); const p = await defillama.protocol.getProtocol({protocol: slug}); const last = p.tvl?.at(-1); return { name: p.name, currentTvl: last?.totalLiquidityUSD, asOf: last?.date, chains: p.chains }", + "const slug = await defillama.resolveProtocol(name); if (!slug) return { error: 'Protocol not found' }; const p = await defillama.protocol.getProtocol({protocol: slug}); const last = p.tvl?.at(-1); return { name: p.name, currentTvl: last?.totalLiquidityUSD, asOf: last?.date, chains: p.chains }", }, { name: "defillama_get_historical_chain_tvl", @@ -174,7 +174,7 @@ export const TOOL_METADATA: ToolMetadata[] = [ }), responseSchema: HistoricalChainTvlSchema, exampleCall: - "const {name} = await defillama.resolveChain(input); const series = await defillama.protocol.getHistoricalChainTvl({chain: name}); return series.slice(-90).map(p => ({date: new Date(p.date * 1000).toISOString() /* p.date is Unix seconds; multiply by 1000 for JS Date */, tvl: p.tvl}))", + "const chain = await defillama.resolveChain(input); if (!chain) return { error: 'Chain not found' }; const series = await defillama.protocol.getHistoricalChainTvl({chain: chain.name}); return series.slice(-90).map(p => ({date: new Date(p.date * 1000).toISOString() /* p.date is Unix seconds; multiply by 1000 for JS Date */, tvl: p.tvl}))", }, // ── DEX (api.llama.fi) ───────────────────────────────────────────────── { @@ -204,7 +204,7 @@ export const TOOL_METADATA: ToolMetadata[] = [ }), responseSchema: DexSummarySchema, exampleCall: - "const slug = await defillama.resolveProtocol(name); const s = await defillama.dex.getDexSummary({protocol: slug}); return { name: s.name, total24h: s.total24h, total7d: s.total7d, total30d: s.total30d, totalAllTime: s.totalAllTime, change_1d: s.change_1d, change_7d: s.change_7d }", + "const slug = await defillama.resolveProtocol(name); if (!slug) return { error: 'Protocol not found' }; const s = await defillama.dex.getDexSummary({protocol: slug}); return { name: s.name, total24h: s.total24h, total7d: s.total7d, total30d: s.total30d, totalAllTime: s.totalAllTime, change_1d: s.change_1d, change_7d: s.change_7d }", }, { name: "defillama_get_dexs_overview", @@ -234,7 +234,7 @@ export const TOOL_METADATA: ToolMetadata[] = [ }), responseSchema: DexOverviewSchema, exampleCall: - "const {name} = await defillama.resolveChain(input); const overview = await defillama.dex.getDexsOverview({chain: name}); return (overview.protocols ?? []).sort((a,b) => (b.total24h ?? 0) - (a.total24h ?? 0)).slice(0, 20).map(p => ({slug: p.slug, name: p.name, total24h: p.total24h, change_7d: p.change_7d}))", + "const chain = await defillama.resolveChain(input); if (!chain) return { error: 'Chain not found' }; const overview = await defillama.dex.getDexsOverview({chain: chain.name}); return (overview.protocols ?? []).sort((a,b) => (b.total24h ?? 0) - (a.total24h ?? 0)).slice(0, 20).map(p => ({slug: p.slug, name: p.name, total24h: p.total24h, change_7d: p.change_7d}))", }, // ── Fees & Revenue (api.llama.fi) ────────────────────────────────────── { @@ -270,7 +270,7 @@ export const TOOL_METADATA: ToolMetadata[] = [ }), responseSchema: FeesSummarySchema, exampleCall: - "const slug = await defillama.resolveProtocol(name); const s = await defillama.fees.getFeesSummary({protocol: slug, dataType: 'dailyRevenue'}); return { name: s.name, total24h: s.total24h, total7d: s.total7d, total30d: s.total30d, totalAllTime: s.totalAllTime, change_1d: s.change_1d, change_7d: s.change_7d }", + "const slug = await defillama.resolveProtocol(name); if (!slug) return { error: 'Protocol not found' }; const s = await defillama.fees.getFeesSummary({protocol: slug, dataType: 'dailyRevenue'}); return { name: s.name, total24h: s.total24h, total7d: s.total7d, total30d: s.total30d, totalAllTime: s.totalAllTime, change_1d: s.change_1d, change_7d: s.change_7d }", }, { name: "defillama_get_fees_overview", @@ -306,7 +306,7 @@ export const TOOL_METADATA: ToolMetadata[] = [ }), responseSchema: FeesOverviewSchema, exampleCall: - "const {name} = await defillama.resolveChain(input); const overview = await defillama.fees.getFeesOverview({chain: name, dataType: 'dailyFees'}); return (overview.protocols ?? []).sort((a,b) => (b.total24h ?? 0) - (a.total24h ?? 0)).slice(0, 20).map(p => ({slug: p.slug, name: p.name, total24h: p.total24h, change_7d: p.change_7d}))", + "const chain = await defillama.resolveChain(input); if (!chain) return { error: 'Chain not found' }; const overview = await defillama.fees.getFeesOverview({chain: chain.name, dataType: 'dailyFees'}); return (overview.protocols ?? []).sort((a,b) => (b.total24h ?? 0) - (a.total24h ?? 0)).slice(0, 20).map(p => ({slug: p.slug, name: p.name, total24h: p.total24h, change_7d: p.change_7d}))", }, // ── Options (api.llama.fi) ───────────────────────────────────────────── { @@ -330,7 +330,7 @@ export const TOOL_METADATA: ToolMetadata[] = [ }), responseSchema: OptionsSummarySchema, exampleCall: - "const slug = await defillama.resolveProtocol(name); const s = await defillama.options.getOptionsSummary({protocol: slug}); return { name: s.name, total24h: s.total24h, total7d: s.total7d, totalAllTime: s.totalAllTime, change_7d: s.change_7d }", + "const slug = await defillama.resolveProtocol(name); if (!slug) return { error: 'Protocol not found' }; const s = await defillama.options.getOptionsSummary({protocol: slug}); return { name: s.name, total24h: s.total24h, total7d: s.total7d, totalAllTime: s.totalAllTime, change_7d: s.change_7d }", }, { name: "defillama_get_options_overview", @@ -366,7 +366,7 @@ export const TOOL_METADATA: ToolMetadata[] = [ }), responseSchema: OptionsOverviewSchema, exampleCall: - "const {name} = await defillama.resolveChain(input); const overview = await defillama.options.getOptionsOverview({chain: name}); return (overview.protocols ?? []).sort((a,b) => (b.total24h ?? 0) - (a.total24h ?? 0)).slice(0, 20).map(p => ({slug: p.slug, name: p.name, total24h: p.total24h, change_7d: p.change_7d}))", + "const chain = await defillama.resolveChain(input); if (!chain) return { error: 'Chain not found' }; const overview = await defillama.options.getOptionsOverview({chain: chain.name}); return (overview.protocols ?? []).sort((a,b) => (b.total24h ?? 0) - (a.total24h ?? 0)).slice(0, 20).map(p => ({slug: p.slug, name: p.name, total24h: p.total24h, change_7d: p.change_7d}))", }, // ── Stablecoins (stablecoins.llama.fi) ───────────────────────────────── { @@ -418,7 +418,7 @@ export const TOOL_METADATA: ToolMetadata[] = [ }), responseSchema: StablecoinChartsSchema, exampleCall: - "const {name} = await defillama.resolveChain(input); const id = await defillama.resolveStablecoin(symbol); const series = await defillama.stablecoin.getStablecoinCharts({chain: name, stablecoin: id}); return series.slice(-90).map(p => ({date: new Date(p.date * 1000).toISOString() /* p.date is Unix seconds; multiply by 1000 for JS Date */, totalCirculatingUSD: p.totalCirculatingUSD}))", + "const chain = await defillama.resolveChain(input); if (!chain) return { error: 'Chain not found' }; const id = await defillama.resolveStablecoin(symbol); if (!id) return { error: 'Stablecoin not found' }; const series = await defillama.stablecoin.getStablecoinCharts({chain: chain.name, stablecoin: id}); return series.slice(-90).map(p => ({date: new Date(p.date * 1000).toISOString() /* p.date is Unix seconds; multiply by 1000 for JS Date */, totalCirculatingUSD: p.totalCirculatingUSD}))", }, { name: "defillama_get_stablecoin_prices", @@ -452,7 +452,7 @@ export const TOOL_METADATA: ToolMetadata[] = [ }), responseSchema: CurrentPricesSchema, exampleCall: - "const {slug} = await defillama.resolveChain(input); const key = `${slug}:${tokenAddress}`; const res = await defillama.price.getCurrentPrices({coins: key}); return res.coins?.[key]?.price", + "const chain = await defillama.resolveChain(input); if (!chain) return { error: 'Chain not found' }; const key = `${chain.slug}:${tokenAddress}`; const res = await defillama.price.getCurrentPrices({coins: key}); return res.coins?.[key]?.price", }, { name: "defillama_get_prices_first_coins", @@ -469,7 +469,7 @@ export const TOOL_METADATA: ToolMetadata[] = [ }), responseSchema: FirstPricesSchema, exampleCall: - "const {slug} = await defillama.resolveChain(input); const key = `${slug}:${tokenAddress}`; const res = await defillama.price.getFirstPrices({coins: key}); return res.coins?.[key]", + "const chain = await defillama.resolveChain(input); if (!chain) return { error: 'Chain not found' }; const key = `${chain.slug}:${tokenAddress}`; const res = await defillama.price.getFirstPrices({coins: key}); return res.coins?.[key]", }, { name: "defillama_get_batch_historical", @@ -501,7 +501,7 @@ export const TOOL_METADATA: ToolMetadata[] = [ }), responseSchema: BatchHistoricalSchema, exampleCall: - "const {slug} = await defillama.resolveChain(input); const key = `${slug}:${tokenAddress}`; const res = await defillama.price.getBatchHistorical({coins: {[key]: [timestamp]}}); return res.coins?.[key]?.prices", + "const chain = await defillama.resolveChain(input); if (!chain) return { error: 'Chain not found' }; const key = `${chain.slug}:${tokenAddress}`; const res = await defillama.price.getBatchHistorical({coins: {[key]: [timestamp]}}); return res.coins?.[key]?.prices", }, { name: "defillama_get_historical_prices_by_contract", @@ -526,7 +526,7 @@ export const TOOL_METADATA: ToolMetadata[] = [ }), responseSchema: CurrentPricesSchema, exampleCall: - "const {slug} = await defillama.resolveChain(input); const key = `${slug}:${tokenAddress}`; const res = await defillama.price.getHistoricalPrices({coins: key, timestamp}); return res.coins?.[key]", + "const chain = await defillama.resolveChain(input); if (!chain) return { error: 'Chain not found' }; const key = `${chain.slug}:${tokenAddress}`; const res = await defillama.price.getHistoricalPrices({coins: key, timestamp}); return res.coins?.[key]", }, { name: "defillama_get_percentage_coins", @@ -560,7 +560,7 @@ export const TOOL_METADATA: ToolMetadata[] = [ }), responseSchema: PercentageSchema, exampleCall: - "const {slug} = await defillama.resolveChain(input); const key = `${slug}:${tokenAddress}`; const res = await defillama.price.getPercentageChange({coins: key, period: '7d'}); return res.coins?.[key]", + "const chain = await defillama.resolveChain(input); if (!chain) return { error: 'Chain not found' }; const key = `${chain.slug}:${tokenAddress}`; const res = await defillama.price.getPercentageChange({coins: key, period: '7d'}); return res.coins?.[key]", }, { name: "defillama_get_chart_coins", @@ -606,7 +606,7 @@ export const TOOL_METADATA: ToolMetadata[] = [ }), responseSchema: PriceChartSchema, exampleCall: - "const {slug} = await defillama.resolveChain(input); const key = `${slug}:${tokenAddress}`; const res = await defillama.price.getPriceChart({coins: key, span: 30, period: '1d'}); return res.coins?.[key]?.prices ?? []", + "const chain = await defillama.resolveChain(input); if (!chain) return { error: 'Chain not found' }; const key = `${chain.slug}:${tokenAddress}`; const res = await defillama.price.getPriceChart({coins: key, span: 30, period: '1d'}); return res.coins?.[key]?.prices ?? []", }, // ── Yields (yields.llama.fi) ─────────────────────────────────────────── { @@ -656,6 +656,6 @@ export const TOOL_METADATA: ToolMetadata[] = [ }), responseSchema: BlockSchema, exampleCall: - "const {slug} = await defillama.resolveChain(input); await defillama.blockchain.getBlockAtTimestamp({chain: slug, timestamp: Math.floor(Date.now()/1000)})", + "const chain = await defillama.resolveChain(input); if (!chain) return { error: 'Chain not found' }; return await defillama.blockchain.getBlockAtTimestamp({chain: chain.slug, timestamp: Math.floor(Date.now()/1000)})", }, ]; diff --git a/src/mcp/search-docs/embedded-index.ts b/src/mcp/search-docs/embedded-index.ts index b51b226..85ce5c3 100644 --- a/src/mcp/search-docs/embedded-index.ts +++ b/src/mcp/search-docs/embedded-index.ts @@ -70,7 +70,7 @@ export const ENTRIES: IndexEntry[] = [ additionalProperties: false, }, exampleCall: - "const slug = await defillama.resolveProtocol(name); const p = await defillama.protocol.getProtocol({protocol: slug}); const last = p.tvl?.at(-1); return { name: p.name, currentTvl: last?.totalLiquidityUSD, asOf: last?.date, chains: p.chains }", + "const slug = await defillama.resolveProtocol(name); if (!slug) return { error: 'Protocol not found' }; const p = await defillama.protocol.getProtocol({protocol: slug}); const last = p.tvl?.at(-1); return { name: p.name, currentTvl: last?.totalLiquidityUSD, asOf: last?.date, chains: p.chains }", }, { kind: "method", @@ -91,7 +91,7 @@ export const ENTRIES: IndexEntry[] = [ additionalProperties: false, }, exampleCall: - "const {name} = await defillama.resolveChain(input); const series = await defillama.protocol.getHistoricalChainTvl({chain: name}); return series.slice(-90).map(p => ({date: new Date(p.date * 1000).toISOString() /* p.date is Unix seconds; multiply by 1000 for JS Date */, tvl: p.tvl}))", + "const chain = await defillama.resolveChain(input); if (!chain) return { error: 'Chain not found' }; const series = await defillama.protocol.getHistoricalChainTvl({chain: chain.name}); return series.slice(-90).map(p => ({date: new Date(p.date * 1000).toISOString() /* p.date is Unix seconds; multiply by 1000 for JS Date */, tvl: p.tvl}))", }, { kind: "method", @@ -123,7 +123,7 @@ export const ENTRIES: IndexEntry[] = [ additionalProperties: false, }, exampleCall: - "const slug = await defillama.resolveProtocol(name); const s = await defillama.dex.getDexSummary({protocol: slug}); return { name: s.name, total24h: s.total24h, total7d: s.total7d, total30d: s.total30d, totalAllTime: s.totalAllTime, change_1d: s.change_1d, change_7d: s.change_7d }", + "const slug = await defillama.resolveProtocol(name); if (!slug) return { error: 'Protocol not found' }; const s = await defillama.dex.getDexSummary({protocol: slug}); return { name: s.name, total24h: s.total24h, total7d: s.total7d, total30d: s.total30d, totalAllTime: s.totalAllTime, change_1d: s.change_1d, change_7d: s.change_7d }", }, { kind: "method", @@ -154,7 +154,7 @@ export const ENTRIES: IndexEntry[] = [ additionalProperties: false, }, exampleCall: - "const {name} = await defillama.resolveChain(input); const overview = await defillama.dex.getDexsOverview({chain: name}); return (overview.protocols ?? []).sort((a,b) => (b.total24h ?? 0) - (a.total24h ?? 0)).slice(0, 20).map(p => ({slug: p.slug, name: p.name, total24h: p.total24h, change_7d: p.change_7d}))", + "const chain = await defillama.resolveChain(input); if (!chain) return { error: 'Chain not found' }; const overview = await defillama.dex.getDexsOverview({chain: chain.name}); return (overview.protocols ?? []).sort((a,b) => (b.total24h ?? 0) - (a.total24h ?? 0)).slice(0, 20).map(p => ({slug: p.slug, name: p.name, total24h: p.total24h, change_7d: p.change_7d}))", }, { kind: "method", @@ -192,7 +192,7 @@ export const ENTRIES: IndexEntry[] = [ additionalProperties: false, }, exampleCall: - "const slug = await defillama.resolveProtocol(name); const s = await defillama.fees.getFeesSummary({protocol: slug, dataType: 'dailyRevenue'}); return { name: s.name, total24h: s.total24h, total7d: s.total7d, total30d: s.total30d, totalAllTime: s.totalAllTime, change_1d: s.change_1d, change_7d: s.change_7d }", + "const slug = await defillama.resolveProtocol(name); if (!slug) return { error: 'Protocol not found' }; const s = await defillama.fees.getFeesSummary({protocol: slug, dataType: 'dailyRevenue'}); return { name: s.name, total24h: s.total24h, total7d: s.total7d, total30d: s.total30d, totalAllTime: s.totalAllTime, change_1d: s.change_1d, change_7d: s.change_7d }", }, { kind: "method", @@ -229,7 +229,7 @@ export const ENTRIES: IndexEntry[] = [ additionalProperties: false, }, exampleCall: - "const {name} = await defillama.resolveChain(input); const overview = await defillama.fees.getFeesOverview({chain: name, dataType: 'dailyFees'}); return (overview.protocols ?? []).sort((a,b) => (b.total24h ?? 0) - (a.total24h ?? 0)).slice(0, 20).map(p => ({slug: p.slug, name: p.name, total24h: p.total24h, change_7d: p.change_7d}))", + "const chain = await defillama.resolveChain(input); if (!chain) return { error: 'Chain not found' }; const overview = await defillama.fees.getFeesOverview({chain: chain.name, dataType: 'dailyFees'}); return (overview.protocols ?? []).sort((a,b) => (b.total24h ?? 0) - (a.total24h ?? 0)).slice(0, 20).map(p => ({slug: p.slug, name: p.name, total24h: p.total24h, change_7d: p.change_7d}))", }, { kind: "method", @@ -257,7 +257,7 @@ export const ENTRIES: IndexEntry[] = [ additionalProperties: false, }, exampleCall: - "const slug = await defillama.resolveProtocol(name); const s = await defillama.options.getOptionsSummary({protocol: slug}); return { name: s.name, total24h: s.total24h, total7d: s.total7d, totalAllTime: s.totalAllTime, change_7d: s.change_7d }", + "const slug = await defillama.resolveProtocol(name); if (!slug) return { error: 'Protocol not found' }; const s = await defillama.options.getOptionsSummary({protocol: slug}); return { name: s.name, total24h: s.total24h, total7d: s.total7d, totalAllTime: s.totalAllTime, change_7d: s.change_7d }", }, { kind: "method", @@ -294,7 +294,7 @@ export const ENTRIES: IndexEntry[] = [ additionalProperties: false, }, exampleCall: - "const {name} = await defillama.resolveChain(input); const overview = await defillama.options.getOptionsOverview({chain: name}); return (overview.protocols ?? []).sort((a,b) => (b.total24h ?? 0) - (a.total24h ?? 0)).slice(0, 20).map(p => ({slug: p.slug, name: p.name, total24h: p.total24h, change_7d: p.change_7d}))", + "const chain = await defillama.resolveChain(input); if (!chain) return { error: 'Chain not found' }; const overview = await defillama.options.getOptionsOverview({chain: chain.name}); return (overview.protocols ?? []).sort((a,b) => (b.total24h ?? 0) - (a.total24h ?? 0)).slice(0, 20).map(p => ({slug: p.slug, name: p.name, total24h: p.total24h, change_7d: p.change_7d}))", }, { kind: "method", @@ -364,7 +364,7 @@ export const ENTRIES: IndexEntry[] = [ additionalProperties: false, }, exampleCall: - "const {name} = await defillama.resolveChain(input); const id = await defillama.resolveStablecoin(symbol); const series = await defillama.stablecoin.getStablecoinCharts({chain: name, stablecoin: id}); return series.slice(-90).map(p => ({date: new Date(p.date * 1000).toISOString() /* p.date is Unix seconds; multiply by 1000 for JS Date */, totalCirculatingUSD: p.totalCirculatingUSD}))", + "const chain = await defillama.resolveChain(input); if (!chain) return { error: 'Chain not found' }; const id = await defillama.resolveStablecoin(symbol); if (!id) return { error: 'Stablecoin not found' }; const series = await defillama.stablecoin.getStablecoinCharts({chain: chain.name, stablecoin: id}); return series.slice(-90).map(p => ({date: new Date(p.date * 1000).toISOString() /* p.date is Unix seconds; multiply by 1000 for JS Date */, totalCirculatingUSD: p.totalCirculatingUSD}))", }, { kind: "method", @@ -413,7 +413,7 @@ export const ENTRIES: IndexEntry[] = [ additionalProperties: false, }, exampleCall: - "const {slug} = await defillama.resolveChain(input); const key = `${slug}:${tokenAddress}`; const res = await defillama.price.getCurrentPrices({coins: key}); return res.coins?.[key]?.price", + "const chain = await defillama.resolveChain(input); if (!chain) return { error: 'Chain not found' }; const key = `${chain.slug}:${tokenAddress}`; const res = await defillama.price.getCurrentPrices({coins: key}); return res.coins?.[key]?.price", }, { kind: "method", @@ -435,7 +435,7 @@ export const ENTRIES: IndexEntry[] = [ additionalProperties: false, }, exampleCall: - "const {slug} = await defillama.resolveChain(input); const key = `${slug}:${tokenAddress}`; const res = await defillama.price.getFirstPrices({coins: key}); return res.coins?.[key]", + "const chain = await defillama.resolveChain(input); if (!chain) return { error: 'Chain not found' }; const key = `${chain.slug}:${tokenAddress}`; const res = await defillama.price.getFirstPrices({coins: key}); return res.coins?.[key]", }, { kind: "method", @@ -497,7 +497,7 @@ export const ENTRIES: IndexEntry[] = [ additionalProperties: false, }, exampleCall: - "const {slug} = await defillama.resolveChain(input); const key = `${slug}:${tokenAddress}`; const res = await defillama.price.getBatchHistorical({coins: {[key]: [timestamp]}}); return res.coins?.[key]?.prices", + "const chain = await defillama.resolveChain(input); if (!chain) return { error: 'Chain not found' }; const key = `${chain.slug}:${tokenAddress}`; const res = await defillama.price.getBatchHistorical({coins: {[key]: [timestamp]}}); return res.coins?.[key]?.prices", }, { kind: "method", @@ -546,7 +546,7 @@ export const ENTRIES: IndexEntry[] = [ additionalProperties: false, }, exampleCall: - "const {slug} = await defillama.resolveChain(input); const key = `${slug}:${tokenAddress}`; const res = await defillama.price.getHistoricalPrices({coins: key, timestamp}); return res.coins?.[key]", + "const chain = await defillama.resolveChain(input); if (!chain) return { error: 'Chain not found' }; const key = `${chain.slug}:${tokenAddress}`; const res = await defillama.price.getHistoricalPrices({coins: key, timestamp}); return res.coins?.[key]", }, { kind: "method", @@ -593,7 +593,7 @@ export const ENTRIES: IndexEntry[] = [ additionalProperties: false, }, exampleCall: - "const {slug} = await defillama.resolveChain(input); const key = `${slug}:${tokenAddress}`; const res = await defillama.price.getPercentageChange({coins: key, period: '7d'}); return res.coins?.[key]", + "const chain = await defillama.resolveChain(input); if (!chain) return { error: 'Chain not found' }; const key = `${chain.slug}:${tokenAddress}`; const res = await defillama.price.getPercentageChange({coins: key, period: '7d'}); return res.coins?.[key]", }, { kind: "method", @@ -669,7 +669,7 @@ export const ENTRIES: IndexEntry[] = [ additionalProperties: false, }, exampleCall: - "const {slug} = await defillama.resolveChain(input); const key = `${slug}:${tokenAddress}`; const res = await defillama.price.getPriceChart({coins: key, span: 30, period: '1d'}); return res.coins?.[key]?.prices ?? []", + "const chain = await defillama.resolveChain(input); if (!chain) return { error: 'Chain not found' }; const key = `${chain.slug}:${tokenAddress}`; const res = await defillama.price.getPriceChart({coins: key, span: 30, period: '1d'}); return res.coins?.[key]?.prices ?? []", }, { kind: "method", @@ -743,7 +743,7 @@ export const ENTRIES: IndexEntry[] = [ additionalProperties: false, }, exampleCall: - "const {slug} = await defillama.resolveChain(input); await defillama.blockchain.getBlockAtTimestamp({chain: slug, timestamp: Math.floor(Date.now()/1000)})", + "const chain = await defillama.resolveChain(input); if (!chain) return { error: 'Chain not found' }; return await defillama.blockchain.getBlockAtTimestamp({chain: chain.slug, timestamp: Math.floor(Date.now()/1000)})", }, { kind: "prose", From eaa42802630930164837d8c09e88376d7b24b10a Mon Sep 17 00:00:00 2001 From: Aliu Salaudeen Date: Tue, 23 Jun 2026 17:27:04 +0100 Subject: [PATCH 4/7] fix(docs): apply * 1000 Date conversion to instructions.md time-series example MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The "Time-series" pattern in the always-loaded "Shape responses" section of instructions.md was using bare `p.date` without the * 1000 conversion — inconsistent with the per-endpoint exampleCalls in tool-metadata.ts (which DO convert), and would produce 1970-01-XX dates if the agent followed the instruction verbatim. This is a latent bug from the original PR #28: the instruction code block was written before PR #30 introduced the Date-conversion discipline, and PR #30 only updated the per-endpoint examples — not this canonical instruction. The restoration PR brought the instruction back verbatim along with the inconsistency. Fix: the time-series pattern now demonstrates `new Date(p.date * 1000).toISOString()` and has a one-line note explicitly calling out the Unix-seconds-vs-ms gotcha so the rule is teachable from the instructions surface, not only from the per-endpoint examples. Regenerated instructions.generated.ts to match. Caught by gemini-code-assist[bot] cross-referencing the instruction example against the tool-metadata exampleCalls — same cross-check pattern that caught the changeset-vs-code mismatch earlier in PR #28. 171 tests still pass. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/mcp/instructions/instructions.generated.ts | 7 +++++-- src/mcp/instructions/instructions.md | 7 +++++-- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/src/mcp/instructions/instructions.generated.ts b/src/mcp/instructions/instructions.generated.ts index b40f29f..67d180f 100644 --- a/src/mcp/instructions/instructions.generated.ts +++ b/src/mcp/instructions/instructions.generated.ts @@ -120,10 +120,13 @@ Three patterns cover almost every case: .map(p => ({ slug: p.slug, name: p.name, tvl: p.tvl, chains: p.chains })); \`\`\` -2. **Time-series** — slice the tail you care about, drop the extra fields: +2. **Time-series** — slice the tail you care about, drop the extra fields. Note that \`/v2\`-style endpoints return \`date\` as Unix **seconds**, while JS \`new Date(n)\` expects **milliseconds** — multiply by 1000 before constructing the Date: \`\`\`js const series = await defillama.protocol.getHistoricalChainTvl({ chain: name }); - return series.slice(-90).map(p => ({ date: p.date, tvl: p.tvl })); + return series.slice(-90).map(p => ({ + date: new Date(p.date * 1000).toISOString(), + tvl: p.tvl, + })); \`\`\` 3. **Single-entity summaries** — pluck only the fields the question needs: diff --git a/src/mcp/instructions/instructions.md b/src/mcp/instructions/instructions.md index e3c220b..8f668f8 100644 --- a/src/mcp/instructions/instructions.md +++ b/src/mcp/instructions/instructions.md @@ -117,10 +117,13 @@ Three patterns cover almost every case: .map(p => ({ slug: p.slug, name: p.name, tvl: p.tvl, chains: p.chains })); ``` -2. **Time-series** — slice the tail you care about, drop the extra fields: +2. **Time-series** — slice the tail you care about, drop the extra fields. Note that `/v2`-style endpoints return `date` as Unix **seconds**, while JS `new Date(n)` expects **milliseconds** — multiply by 1000 before constructing the Date: ```js const series = await defillama.protocol.getHistoricalChainTvl({ chain: name }); - return series.slice(-90).map(p => ({ date: p.date, tvl: p.tvl })); + return series.slice(-90).map(p => ({ + date: new Date(p.date * 1000).toISOString(), + tvl: p.tvl, + })); ``` 3. **Single-entity summaries** — pluck only the fields the question needs: From 4c8c398d425142d46c6235c3966f62ae32e03032 Mon Sep 17 00:00:00 2001 From: Aliu Salaudeen Date: Tue, 23 Jun 2026 17:39:51 +0100 Subject: [PATCH 5/7] fix(catalog): real syntax error + immutable-sort pattern across broad-list examples MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two distinct concerns surfaced by gemini-code-assist[bot] on PR #32. 1. SYNTAX ERROR (real, critical). The getHistoricalPoolData exampleCall had an arrow function with no body: (pools.data ?? []).find(p => /* match on project/symbol/chain */) Verified empirically: SyntaxError: Unexpected token ')' An agent following this example verbatim would crash at parse time. Replaced the bare comment with a real matching expression using the *Input naming convention used elsewhere: (pools.data ?? []).find(p => p.project === projectInput && p.symbol === symbolInput && p.chain === chainInput )?.pool 2. IMMUTABLE-SORT PATTERN (style fix, wrong rationale, right code). The bot claimed `chains.sort()` would mutate the cached host-side response. That's incorrect for this codebase's architecture: execute/client.ts wraps every service response in `ivm.ExternalCopy(...).copyInto(...)`, so the sandbox gets a structured clone — mutating it inside the sandbox cannot reach the host-side cache. Verified by inspecting envelopeOk in src/mcp/execute/client.ts. But the suggested `[...arr].sort(...)` form is still a better teaching pattern: - Signals immutability intent. - Safe to copy-paste into non-sandbox contexts (a Node script, a different MCP host, etc.) where `.sort()` WOULD mutate. - Same well-known JS idiom. Accepted on style/teaching grounds. Applied uniformly to all six broad-list sort sites (the two the bot flagged plus four it didn't, for consistency): - getChains, getProtocols, getDexsOverview, getFeesOverview, getOptionsOverview, getLatestPools. Also updated the matching "Lists" example in instructions.md's "Shape responses inside execute()" section so the canonical instruction demonstrates the same pattern, with a one-line note explaining when/why the spread matters. 171 tests still pass. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/mcp/catalog/tool-metadata.ts | 14 +++++++------- src/mcp/instructions/instructions.generated.ts | 4 ++-- src/mcp/instructions/instructions.md | 4 ++-- src/mcp/search-docs/embedded-index.ts | 14 +++++++------- 4 files changed, 18 insertions(+), 18 deletions(-) diff --git a/src/mcp/catalog/tool-metadata.ts b/src/mcp/catalog/tool-metadata.ts index 8da5e1d..7c0df78 100644 --- a/src/mcp/catalog/tool-metadata.ts +++ b/src/mcp/catalog/tool-metadata.ts @@ -128,7 +128,7 @@ export const TOOL_METADATA: ToolMetadata[] = [ parameters: z.object({}), responseSchema: ChainsSchema, exampleCall: - "const chains = await defillama.protocol.getChains(); return chains.sort((a,b) => (b.tvl ?? 0) - (a.tvl ?? 0)).slice(0, 20).map(c => ({name: c.name, tvl: c.tvl}))", + "const chains = await defillama.protocol.getChains(); return [...chains].sort((a,b) => (b.tvl ?? 0) - (a.tvl ?? 0)).slice(0, 20).map(c => ({name: c.name, tvl: c.tvl}))", }, { name: "defillama_get_protocols", @@ -139,7 +139,7 @@ export const TOOL_METADATA: ToolMetadata[] = [ parameters: z.object({}), responseSchema: ProtocolsSchema, exampleCall: - "const protocols = await defillama.protocol.getProtocols(); return protocols.sort((a,b) => (b.tvl ?? 0) - (a.tvl ?? 0)).slice(0, 20).map(p => ({slug: p.slug, name: p.name, tvl: p.tvl, chains: p.chains, change_7d: p.change_7d}))", + "const protocols = await defillama.protocol.getProtocols(); return [...protocols].sort((a,b) => (b.tvl ?? 0) - (a.tvl ?? 0)).slice(0, 20).map(p => ({slug: p.slug, name: p.name, tvl: p.tvl, chains: p.chains, change_7d: p.change_7d}))", }, { name: "defillama_get_protocol", @@ -234,7 +234,7 @@ export const TOOL_METADATA: ToolMetadata[] = [ }), responseSchema: DexOverviewSchema, exampleCall: - "const chain = await defillama.resolveChain(input); if (!chain) return { error: 'Chain not found' }; const overview = await defillama.dex.getDexsOverview({chain: chain.name}); return (overview.protocols ?? []).sort((a,b) => (b.total24h ?? 0) - (a.total24h ?? 0)).slice(0, 20).map(p => ({slug: p.slug, name: p.name, total24h: p.total24h, change_7d: p.change_7d}))", + "const chain = await defillama.resolveChain(input); if (!chain) return { error: 'Chain not found' }; const overview = await defillama.dex.getDexsOverview({chain: chain.name}); return [...(overview.protocols ?? [])].sort((a,b) => (b.total24h ?? 0) - (a.total24h ?? 0)).slice(0, 20).map(p => ({slug: p.slug, name: p.name, total24h: p.total24h, change_7d: p.change_7d}))", }, // ── Fees & Revenue (api.llama.fi) ────────────────────────────────────── { @@ -306,7 +306,7 @@ export const TOOL_METADATA: ToolMetadata[] = [ }), responseSchema: FeesOverviewSchema, exampleCall: - "const chain = await defillama.resolveChain(input); if (!chain) return { error: 'Chain not found' }; const overview = await defillama.fees.getFeesOverview({chain: chain.name, dataType: 'dailyFees'}); return (overview.protocols ?? []).sort((a,b) => (b.total24h ?? 0) - (a.total24h ?? 0)).slice(0, 20).map(p => ({slug: p.slug, name: p.name, total24h: p.total24h, change_7d: p.change_7d}))", + "const chain = await defillama.resolveChain(input); if (!chain) return { error: 'Chain not found' }; const overview = await defillama.fees.getFeesOverview({chain: chain.name, dataType: 'dailyFees'}); return [...(overview.protocols ?? [])].sort((a,b) => (b.total24h ?? 0) - (a.total24h ?? 0)).slice(0, 20).map(p => ({slug: p.slug, name: p.name, total24h: p.total24h, change_7d: p.change_7d}))", }, // ── Options (api.llama.fi) ───────────────────────────────────────────── { @@ -366,7 +366,7 @@ export const TOOL_METADATA: ToolMetadata[] = [ }), responseSchema: OptionsOverviewSchema, exampleCall: - "const chain = await defillama.resolveChain(input); if (!chain) return { error: 'Chain not found' }; const overview = await defillama.options.getOptionsOverview({chain: chain.name}); return (overview.protocols ?? []).sort((a,b) => (b.total24h ?? 0) - (a.total24h ?? 0)).slice(0, 20).map(p => ({slug: p.slug, name: p.name, total24h: p.total24h, change_7d: p.change_7d}))", + "const chain = await defillama.resolveChain(input); if (!chain) return { error: 'Chain not found' }; const overview = await defillama.options.getOptionsOverview({chain: chain.name}); return [...(overview.protocols ?? [])].sort((a,b) => (b.total24h ?? 0) - (a.total24h ?? 0)).slice(0, 20).map(p => ({slug: p.slug, name: p.name, total24h: p.total24h, change_7d: p.change_7d}))", }, // ── Stablecoins (stablecoins.llama.fi) ───────────────────────────────── { @@ -618,7 +618,7 @@ export const TOOL_METADATA: ToolMetadata[] = [ parameters: z.object({}), responseSchema: PoolsSchema, exampleCall: - "const pools = await defillama.yield.getLatestPools(); return (pools.data ?? []).sort((a,b) => (b.apy ?? 0) - (a.apy ?? 0)).slice(0, 20).map(p => ({pool: p.pool, project: p.project, symbol: p.symbol, chain: p.chain, apy: p.apy, tvlUsd: p.tvlUsd}))", + "const pools = await defillama.yield.getLatestPools(); return [...(pools.data ?? [])].sort((a,b) => (b.apy ?? 0) - (a.apy ?? 0)).slice(0, 20).map(p => ({pool: p.pool, project: p.project, symbol: p.symbol, chain: p.chain, apy: p.apy, tvlUsd: p.tvlUsd}))", }, { name: "defillama_get_historical_pool_data", @@ -635,7 +635,7 @@ export const TOOL_METADATA: ToolMetadata[] = [ }), responseSchema: HistoricalPoolSchema, exampleCall: - "const pools = await defillama.yield.getLatestPools(); const id = (pools.data ?? []).find(p => /* match on project/symbol/chain */)?.pool; if (!id) return { error: 'Pool not found' }; const series = await defillama.yield.getHistoricalPoolData({pool: id}); return (series.data ?? []).slice(-90).map(p => ({timestamp: p.timestamp /* already an ISO string, unlike the date:number fields on /v2 endpoints */, apy: p.apy, tvlUsd: p.tvlUsd}))", + "const pools = await defillama.yield.getLatestPools(); const id = (pools.data ?? []).find(p => p.project === projectInput && p.symbol === symbolInput && p.chain === chainInput)?.pool; if (!id) return { error: 'Pool not found' }; const series = await defillama.yield.getHistoricalPoolData({pool: id}); return (series.data ?? []).slice(-90).map(p => ({timestamp: p.timestamp /* already an ISO string, unlike the date:number fields on /v2 endpoints */, apy: p.apy, tvlUsd: p.tvlUsd}))", }, // ── Blockchain (coins.llama.fi) ──────────────────────────────────────── { diff --git a/src/mcp/instructions/instructions.generated.ts b/src/mcp/instructions/instructions.generated.ts index 67d180f..08493ff 100644 --- a/src/mcp/instructions/instructions.generated.ts +++ b/src/mcp/instructions/instructions.generated.ts @@ -111,10 +111,10 @@ shipping unshaped data back. Three patterns cover almost every case: -1. **Lists** — sort, slice, project the fields you need: +1. **Lists** — sort, slice, project the fields you need. Use \`[...arr].sort(...)\` (or \`arr.toSorted(...)\`) rather than \`arr.sort(...)\` so the example is safe to copy-paste outside the sandbox, where \`sort\` would mutate the source array in place: \`\`\`js const protocols = await defillama.protocol.getProtocols(); - return protocols + return [...protocols] .sort((a, b) => (b.tvl ?? 0) - (a.tvl ?? 0)) .slice(0, 20) .map(p => ({ slug: p.slug, name: p.name, tvl: p.tvl, chains: p.chains })); diff --git a/src/mcp/instructions/instructions.md b/src/mcp/instructions/instructions.md index 8f668f8..0d933ac 100644 --- a/src/mcp/instructions/instructions.md +++ b/src/mcp/instructions/instructions.md @@ -108,10 +108,10 @@ shipping unshaped data back. Three patterns cover almost every case: -1. **Lists** — sort, slice, project the fields you need: +1. **Lists** — sort, slice, project the fields you need. Use `[...arr].sort(...)` (or `arr.toSorted(...)`) rather than `arr.sort(...)` so the example is safe to copy-paste outside the sandbox, where `sort` would mutate the source array in place: ```js const protocols = await defillama.protocol.getProtocols(); - return protocols + return [...protocols] .sort((a, b) => (b.tvl ?? 0) - (a.tvl ?? 0)) .slice(0, 20) .map(p => ({ slug: p.slug, name: p.name, tvl: p.tvl, chains: p.chains })); diff --git a/src/mcp/search-docs/embedded-index.ts b/src/mcp/search-docs/embedded-index.ts index 85ce5c3..a15646d 100644 --- a/src/mcp/search-docs/embedded-index.ts +++ b/src/mcp/search-docs/embedded-index.ts @@ -33,7 +33,7 @@ export const ENTRIES: IndexEntry[] = [ additionalProperties: false, }, exampleCall: - "const chains = await defillama.protocol.getChains(); return chains.sort((a,b) => (b.tvl ?? 0) - (a.tvl ?? 0)).slice(0, 20).map(c => ({name: c.name, tvl: c.tvl}))", + "const chains = await defillama.protocol.getChains(); return [...chains].sort((a,b) => (b.tvl ?? 0) - (a.tvl ?? 0)).slice(0, 20).map(c => ({name: c.name, tvl: c.tvl}))", }, { kind: "method", @@ -48,7 +48,7 @@ export const ENTRIES: IndexEntry[] = [ additionalProperties: false, }, exampleCall: - "const protocols = await defillama.protocol.getProtocols(); return protocols.sort((a,b) => (b.tvl ?? 0) - (a.tvl ?? 0)).slice(0, 20).map(p => ({slug: p.slug, name: p.name, tvl: p.tvl, chains: p.chains, change_7d: p.change_7d}))", + "const protocols = await defillama.protocol.getProtocols(); return [...protocols].sort((a,b) => (b.tvl ?? 0) - (a.tvl ?? 0)).slice(0, 20).map(p => ({slug: p.slug, name: p.name, tvl: p.tvl, chains: p.chains, change_7d: p.change_7d}))", }, { kind: "method", @@ -154,7 +154,7 @@ export const ENTRIES: IndexEntry[] = [ additionalProperties: false, }, exampleCall: - "const chain = await defillama.resolveChain(input); if (!chain) return { error: 'Chain not found' }; const overview = await defillama.dex.getDexsOverview({chain: chain.name}); return (overview.protocols ?? []).sort((a,b) => (b.total24h ?? 0) - (a.total24h ?? 0)).slice(0, 20).map(p => ({slug: p.slug, name: p.name, total24h: p.total24h, change_7d: p.change_7d}))", + "const chain = await defillama.resolveChain(input); if (!chain) return { error: 'Chain not found' }; const overview = await defillama.dex.getDexsOverview({chain: chain.name}); return [...(overview.protocols ?? [])].sort((a,b) => (b.total24h ?? 0) - (a.total24h ?? 0)).slice(0, 20).map(p => ({slug: p.slug, name: p.name, total24h: p.total24h, change_7d: p.change_7d}))", }, { kind: "method", @@ -229,7 +229,7 @@ export const ENTRIES: IndexEntry[] = [ additionalProperties: false, }, exampleCall: - "const chain = await defillama.resolveChain(input); if (!chain) return { error: 'Chain not found' }; const overview = await defillama.fees.getFeesOverview({chain: chain.name, dataType: 'dailyFees'}); return (overview.protocols ?? []).sort((a,b) => (b.total24h ?? 0) - (a.total24h ?? 0)).slice(0, 20).map(p => ({slug: p.slug, name: p.name, total24h: p.total24h, change_7d: p.change_7d}))", + "const chain = await defillama.resolveChain(input); if (!chain) return { error: 'Chain not found' }; const overview = await defillama.fees.getFeesOverview({chain: chain.name, dataType: 'dailyFees'}); return [...(overview.protocols ?? [])].sort((a,b) => (b.total24h ?? 0) - (a.total24h ?? 0)).slice(0, 20).map(p => ({slug: p.slug, name: p.name, total24h: p.total24h, change_7d: p.change_7d}))", }, { kind: "method", @@ -294,7 +294,7 @@ export const ENTRIES: IndexEntry[] = [ additionalProperties: false, }, exampleCall: - "const chain = await defillama.resolveChain(input); if (!chain) return { error: 'Chain not found' }; const overview = await defillama.options.getOptionsOverview({chain: chain.name}); return (overview.protocols ?? []).sort((a,b) => (b.total24h ?? 0) - (a.total24h ?? 0)).slice(0, 20).map(p => ({slug: p.slug, name: p.name, total24h: p.total24h, change_7d: p.change_7d}))", + "const chain = await defillama.resolveChain(input); if (!chain) return { error: 'Chain not found' }; const overview = await defillama.options.getOptionsOverview({chain: chain.name}); return [...(overview.protocols ?? [])].sort((a,b) => (b.total24h ?? 0) - (a.total24h ?? 0)).slice(0, 20).map(p => ({slug: p.slug, name: p.name, total24h: p.total24h, change_7d: p.change_7d}))", }, { kind: "method", @@ -684,7 +684,7 @@ export const ENTRIES: IndexEntry[] = [ additionalProperties: false, }, exampleCall: - "const pools = await defillama.yield.getLatestPools(); return (pools.data ?? []).sort((a,b) => (b.apy ?? 0) - (a.apy ?? 0)).slice(0, 20).map(p => ({pool: p.pool, project: p.project, symbol: p.symbol, chain: p.chain, apy: p.apy, tvlUsd: p.tvlUsd}))", + "const pools = await defillama.yield.getLatestPools(); return [...(pools.data ?? [])].sort((a,b) => (b.apy ?? 0) - (a.apy ?? 0)).slice(0, 20).map(p => ({pool: p.pool, project: p.project, symbol: p.symbol, chain: p.chain, apy: p.apy, tvlUsd: p.tvlUsd}))", }, { kind: "method", @@ -706,7 +706,7 @@ export const ENTRIES: IndexEntry[] = [ additionalProperties: false, }, exampleCall: - "const pools = await defillama.yield.getLatestPools(); const id = (pools.data ?? []).find(p => /* match on project/symbol/chain */)?.pool; if (!id) return { error: 'Pool not found' }; const series = await defillama.yield.getHistoricalPoolData({pool: id}); return (series.data ?? []).slice(-90).map(p => ({timestamp: p.timestamp /* already an ISO string, unlike the date:number fields on /v2 endpoints */, apy: p.apy, tvlUsd: p.tvlUsd}))", + "const pools = await defillama.yield.getLatestPools(); const id = (pools.data ?? []).find(p => p.project === projectInput && p.symbol === symbolInput && p.chain === chainInput)?.pool; if (!id) return { error: 'Pool not found' }; const series = await defillama.yield.getHistoricalPoolData({pool: id}); return (series.data ?? []).slice(-90).map(p => ({timestamp: p.timestamp /* already an ISO string, unlike the date:number fields on /v2 endpoints */, apy: p.apy, tvlUsd: p.tvlUsd}))", }, { kind: "method", From 672a4aa9784c88e1f1d515e1c0d2196921e21069 Mon Sep 17 00:00:00 2001 From: Aliu Salaudeen Date: Tue, 23 Jun 2026 17:57:40 +0100 Subject: [PATCH 6/7] fix(catalog): convert asOf Unix-seconds to ISO string in getProtocol example MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The `getProtocol` exampleCall returned `asOf: last?.date` as a raw Unix-seconds number — inconsistent with the other time-series examples in the catalog (getHistoricalChainTvl, getStablecoinCharts, getStablecoinPrices) which all convert via `new Date(p.date * 1000) .toISOString()`. Same class of inconsistency as the latent bug fixed earlier in this PR for instructions.md's time-series example. Fix: `asOf: last?.date ? new Date(last.date * 1000).toISOString() : undefined` The conditional preserves `undefined` when there is no last entry, which matches the existing pattern where `last?.totalLiquidityUSD` and other plucked fields can also be `undefined` for an empty tvl array. Caught by gemini-code-assist[bot] on PR #32 via the same catalog-wide cross-check pattern that caught the instructions.md and changeset-vs-code mismatches earlier. 171 tests still pass. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/mcp/catalog/tool-metadata.ts | 2 +- src/mcp/search-docs/embedded-index.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/mcp/catalog/tool-metadata.ts b/src/mcp/catalog/tool-metadata.ts index 7c0df78..89e8e40 100644 --- a/src/mcp/catalog/tool-metadata.ts +++ b/src/mcp/catalog/tool-metadata.ts @@ -156,7 +156,7 @@ export const TOOL_METADATA: ToolMetadata[] = [ }), responseSchema: ProtocolSchema, exampleCall: - "const slug = await defillama.resolveProtocol(name); if (!slug) return { error: 'Protocol not found' }; const p = await defillama.protocol.getProtocol({protocol: slug}); const last = p.tvl?.at(-1); return { name: p.name, currentTvl: last?.totalLiquidityUSD, asOf: last?.date, chains: p.chains }", + "const slug = await defillama.resolveProtocol(name); if (!slug) return { error: 'Protocol not found' }; const p = await defillama.protocol.getProtocol({protocol: slug}); const last = p.tvl?.at(-1); return { name: p.name, currentTvl: last?.totalLiquidityUSD, asOf: last?.date ? new Date(last.date * 1000).toISOString() : undefined, chains: p.chains }", }, { name: "defillama_get_historical_chain_tvl", diff --git a/src/mcp/search-docs/embedded-index.ts b/src/mcp/search-docs/embedded-index.ts index a15646d..5358113 100644 --- a/src/mcp/search-docs/embedded-index.ts +++ b/src/mcp/search-docs/embedded-index.ts @@ -70,7 +70,7 @@ export const ENTRIES: IndexEntry[] = [ additionalProperties: false, }, exampleCall: - "const slug = await defillama.resolveProtocol(name); if (!slug) return { error: 'Protocol not found' }; const p = await defillama.protocol.getProtocol({protocol: slug}); const last = p.tvl?.at(-1); return { name: p.name, currentTvl: last?.totalLiquidityUSD, asOf: last?.date, chains: p.chains }", + "const slug = await defillama.resolveProtocol(name); if (!slug) return { error: 'Protocol not found' }; const p = await defillama.protocol.getProtocol({protocol: slug}); const last = p.tvl?.at(-1); return { name: p.name, currentTvl: last?.totalLiquidityUSD, asOf: last?.date ? new Date(last.date * 1000).toISOString() : undefined, chains: p.chains }", }, { kind: "method", From e860e0359dbbb4dbb0a5e2579593b3ae7e022eea Mon Sep 17 00:00:00 2001 From: Aliu Salaudeen Date: Tue, 23 Jun 2026 18:24:10 +0100 Subject: [PATCH 7/7] fix(catalog): unwrap peggedUSD on stablecoin .circulating / .totalCirculatingUSD MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two related upstream-shape inconsistencies in the stablecoin exampleCalls — both confirmed against StablecoinChainsSchema's declaration AND a live probe of stablecoins.llama.fi: GET /stablecoins?includePrices=true peggedAssets[].circulating = { peggedUSD: number } // NOT a primitive GET /stablecoinchains [].totalCirculatingUSD = { peggedUSD: number } // NOT a primitive Before this fix, the exampleCalls projected the wrapper object through verbatim — `circulating: s.circulating` returned `{peggedUSD: 186079265199.30408}`, and `total: r.totalCirculatingUSD` returned `{peggedUSD: 6926947.82...}`. That defeats the "flat projected shape" goal of the surrounding shape-discipline work, and gives the agent a confusingly-shaped object where it would expect a number. Apply `?.peggedUSD` unwrap to both. Inline comment notes the upstream wrapper convention so the next person editing these examples doesn't re-introduce the bug. Caught by gemini-code-assist[bot] on commit 672a4aa via the same schema-vs-example cross-check pattern that found the getProtocol tvl-array and getHistoricalPoolData {data} wrapper bugs earlier on this PR. 171 tests still pass. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/mcp/catalog/tool-metadata.ts | 4 ++-- src/mcp/search-docs/embedded-index.ts | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/mcp/catalog/tool-metadata.ts b/src/mcp/catalog/tool-metadata.ts index 89e8e40..5f24011 100644 --- a/src/mcp/catalog/tool-metadata.ts +++ b/src/mcp/catalog/tool-metadata.ts @@ -383,7 +383,7 @@ export const TOOL_METADATA: ToolMetadata[] = [ }), responseSchema: StablecoinsSchema, exampleCall: - "const res = await defillama.stablecoin.getStablecoins({includePrices: true}); return (res.peggedAssets ?? []).slice(0, 20).map(s => ({id: s.id, symbol: s.symbol, name: s.name, circulating: s.circulating, price: s.price}))", + "const res = await defillama.stablecoin.getStablecoins({includePrices: true}); return (res.peggedAssets ?? []).slice(0, 20).map(s => ({id: s.id, symbol: s.symbol, name: s.name, circulating: s.circulating?.peggedUSD /* upstream wraps as {peggedUSD: n}; unwrap for a flat numeric */, price: s.price}))", }, { name: "defillama_get_stablecoin_chains", @@ -394,7 +394,7 @@ export const TOOL_METADATA: ToolMetadata[] = [ parameters: z.object({}), responseSchema: StablecoinChainsSchema, exampleCall: - "const rows = await defillama.stablecoin.getStablecoinChains(); return rows.slice(0, 30).map(r => ({name: r.name, total: r.totalCirculatingUSD}))", + "const rows = await defillama.stablecoin.getStablecoinChains(); return rows.slice(0, 30).map(r => ({name: r.name, total: r.totalCirculatingUSD?.peggedUSD /* upstream wraps as {peggedUSD: n}; unwrap for a flat numeric */}))", }, { name: "defillama_get_stablecoin_charts", diff --git a/src/mcp/search-docs/embedded-index.ts b/src/mcp/search-docs/embedded-index.ts index 5358113..eaff8e1 100644 --- a/src/mcp/search-docs/embedded-index.ts +++ b/src/mcp/search-docs/embedded-index.ts @@ -314,7 +314,7 @@ export const ENTRIES: IndexEntry[] = [ additionalProperties: false, }, exampleCall: - "const res = await defillama.stablecoin.getStablecoins({includePrices: true}); return (res.peggedAssets ?? []).slice(0, 20).map(s => ({id: s.id, symbol: s.symbol, name: s.name, circulating: s.circulating, price: s.price}))", + "const res = await defillama.stablecoin.getStablecoins({includePrices: true}); return (res.peggedAssets ?? []).slice(0, 20).map(s => ({id: s.id, symbol: s.symbol, name: s.name, circulating: s.circulating?.peggedUSD /* upstream wraps as {peggedUSD: n}; unwrap for a flat numeric */, price: s.price}))", }, { kind: "method", @@ -329,7 +329,7 @@ export const ENTRIES: IndexEntry[] = [ additionalProperties: false, }, exampleCall: - "const rows = await defillama.stablecoin.getStablecoinChains(); return rows.slice(0, 30).map(r => ({name: r.name, total: r.totalCirculatingUSD}))", + "const rows = await defillama.stablecoin.getStablecoinChains(); return rows.slice(0, 30).map(r => ({name: r.name, total: r.totalCirculatingUSD?.peggedUSD /* upstream wraps as {peggedUSD: n}; unwrap for a flat numeric */}))", }, { kind: "method",