From 5a10172aeb276e91a0a388f2b6316c4fc24de1f7 Mon Sep 17 00:00:00 2001 From: Karl Waldman Date: Tue, 11 Aug 2026 16:07:17 -0400 Subject: [PATCH] fix: use canonical futures routes --- .github/workflows/live-tests.yml | 2 +- .mcp.json | 4 +- README.md | 4 +- manifest.json | 2 +- package-lock.json | 4 +- package.json | 2 +- scripts/live-smoke.mjs | 6 +- server.json | 4 +- src/__tests__/index.test.ts | 148 +++++++++++++++++++++++++++++-- src/index.ts | 138 ++++++++++++++++------------ 10 files changed, 234 insertions(+), 80 deletions(-) diff --git a/.github/workflows/live-tests.yml b/.github/workflows/live-tests.yml index fc96c35..56f7d56 100644 --- a/.github/workflows/live-tests.yml +++ b/.github/workflows/live-tests.yml @@ -93,7 +93,7 @@ jobs: node-version: "22" - run: npm ci - run: npm audit --audit-level=low - - name: Live smoke test (ice-brent latest + curve) + - name: Live smoke test (generic Brent latest + curve) env: OILPRICEAPI_TEST_KEY: ${{ secrets.OILPRICEAPI_TEST_KEY }} OILPRICEAPI_LIVE_REQUIRED: "1" diff --git a/.mcp.json b/.mcp.json index 8be443b..7a03f1f 100644 --- a/.mcp.json +++ b/.mcp.json @@ -1,11 +1,11 @@ { "name": "oilpriceapi", "description": "Source-timestamped energy data and reviewed OilPriceAPI product facts for MCP clients.", - "version": "3.2.2", + "version": "3.2.3", "mcpServers": { "oilpriceapi": { "command": "npx", - "args": ["-y", "oilpriceapi-mcp@3.2.2"] + "args": ["-y", "oilpriceapi-mcp@3.2.3"] } } } diff --git a/README.md b/README.md index 66df9ac..e9d7700 100644 --- a/README.md +++ b/README.md @@ -257,7 +257,7 @@ All tools are prefixed with `opa_` to avoid name collisions when multiple MCP se | `opa_compare_prices` | Side-by-side comparison of 2-5 commodities with spread | | `opa_list_commodities` | Account-visible commodity catalog returned by the live API | | `opa_get_history` | Historical prices with high/low/avg/change (day/week/month/year) | -| `opa_get_futures` | Front-month futures (Brent BZ, WTI CL, ICE Gasoil, TTF, JKM, EUA) | +| `opa_get_futures` | Front-month futures (Brent, WTI, gasoil, TTF, JKM, EU carbon) | | `opa_get_futures_curve` | Full forward curve with contango/backwardation analysis | | `opa_get_marine_fuels` | Bunker fuel prices by port and fuel type (VLSFO/MGO/IFO380) | | `opa_get_rig_counts` | Baker Hughes US rig count with week-over-week change | @@ -315,7 +315,7 @@ The market brief gives a multi-commodity snapshot in one call. Subscriptions ("w "How many well permits were issued in Texas?" "What's the current 3-2-1 crack spread?" "What's the UPS ground fuel surcharge?" -"Show me the ICE Gasoil futures curve" +"Show me the gasoil futures curve" ``` ## Resources diff --git a/manifest.json b/manifest.json index 596c633..a7e44f6 100644 --- a/manifest.json +++ b/manifest.json @@ -3,7 +3,7 @@ "manifest_version": "0.2", "name": "oilpriceapi", "display_name": "OilPriceAPI \u2014 Oil, Gas & Commodity Prices", - "version": "3.2.2", + "version": "3.2.3", "description": "Source-timestamped energy data and reviewed OilPriceAPI product facts for MCP clients.", "long_description": "OilPriceAPI tools and resources for latest available energy values, history, futures, refining spreads, carrier fuel surcharges, selected energy-intelligence datasets, alerts, market briefs, and a versioned public product contract. Keyless demo mode supports a limited commodity set. Dataset access and limits vary by plan and account entitlement.", "author": { diff --git a/package-lock.json b/package-lock.json index 6de55bc..2e223db 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "oilpriceapi-mcp", - "version": "3.2.2", + "version": "3.2.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "oilpriceapi-mcp", - "version": "3.2.2", + "version": "3.2.3", "license": "MIT", "dependencies": { "@modelcontextprotocol/sdk": "^1.25.2", diff --git a/package.json b/package.json index 5dee09b..df6de42 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "oilpriceapi-mcp", - "version": "3.2.2", + "version": "3.2.3", "mcpName": "io.github.OilpriceAPI/mcp-server", "description": "Source-timestamped oil, gas, and related energy data plus reviewed product facts for MCP clients.", "type": "module", diff --git a/scripts/live-smoke.mjs b/scripts/live-smoke.mjs index da056eb..4ed9dce 100644 --- a/scripts/live-smoke.mjs +++ b/scripts/live-smoke.mjs @@ -5,8 +5,8 @@ * * Verifies the futures path fix (v2.2.1) AND the #3245 subscription + * market-brief tools (v2.3.0) end-to-end against PRODUCTION: - * - GET /v1/futures/ice-brent (latest) -> 200 + numeric front-month last_price - * - GET /v1/futures/ice-brent/curve (curve) -> 200 + curve data OR documented no-data state + * - GET /v1/futures/brent (latest) -> 200 + numeric front-month last_price + * - GET /v1/futures/brent/curve (curve) -> 200 + curve data OR documented no-data state * - GET /v1/market-brief?codes=BRENT_CRUDE_USD -> 200 + a numeric price (opa_get_market_brief) * - GET /v1/subscriptions (list) -> 200 + { data: { subscriptions: [...] } } (opa_list_subscriptions) * - POST/GET/DELETE round-trip on /v1/subscriptions (opa_create_price_subscription / events / delete) @@ -37,7 +37,7 @@ const API_BASE = process.env.OILPRICEAPI_BASE_URL || "https://api.oilpriceapi.com"; const KEY = process.env.OILPRICEAPI_TEST_KEY; const REQUIRE_KEY = process.env.OILPRICEAPI_LIVE_REQUIRED === "1"; -const SLUG = "ice-brent"; +const SLUG = "brent"; const RATE_LIMIT_MS = 1100; // > 1 req/sec const RATE_LIMIT_RETRIES = 1; // Opt-in: exercise the WRITE round-trip (create -> events -> delete) against diff --git a/server.json b/server.json index 61dcab2..a6e126a 100644 --- a/server.json +++ b/server.json @@ -6,12 +6,12 @@ "url": "https://github.com/OilpriceAPI/mcp-server", "source": "github" }, - "version": "3.2.2", + "version": "3.2.3", "packages": [ { "registryType": "npm", "identifier": "oilpriceapi-mcp", - "version": "3.2.2", + "version": "3.2.3", "transport": { "type": "stdio" }, diff --git a/src/__tests__/index.test.ts b/src/__tests__/index.test.ts index 671a251..ab1d299 100644 --- a/src/__tests__/index.test.ts +++ b/src/__tests__/index.test.ts @@ -36,8 +36,135 @@ import { SERVER_INSTRUCTIONS, productFactsProvider, formatStartupInventory, + FUTURES_CONTRACTS, + FUTURES_CONTRACT_SLUGS, + FUTURES_CONTRACT_NAMES, } from "../index.js"; +describe("instrument-generic futures routes (#4175)", () => { + const routeCases = [ + ["brent", "brent"], + ["BZ", "brent"], + ["ice-brent", "brent"], + ["wti", "wti"], + ["CL", "wti"], + ["ice-wti", "wti"], + ["gasoil", "gasoil"], + ["G", "gasoil"], + ["ice-gasoil", "gasoil"], + ["eu-carbon", "eu-carbon"], + ["EUA", "eu-carbon"], + ["eua-carbon", "eu-carbon"], + ] as const; + const server = createSandboxServer(); + const tools = ( + server as unknown as { + _registeredTools: Record< + string, + { + handler: ( + args: Record, + extra: Record, + ) => Promise<{ + content: Array<{ type: string; text: string }>; + isError?: boolean; + }>; + } + >; + } + )._registeredTools; + + afterEach(() => { + vi.unstubAllEnvs(); + vi.unstubAllGlobals(); + }); + + it.each(routeCases)( + "normalizes %s to /v1/futures/%s", + async (input, canonical) => { + vi.stubEnv("OILPRICEAPI_KEY", "test-key-123"); + const fetchSpy = vi.fn().mockResolvedValue({ + ok: true, + status: 200, + headers: { get: () => null }, + json: async () => ({ + front_month: { contract_month: "2026-09", last_price: 70 }, + contracts: [{ contract_month: "2026-09", last_price: 70 }], + }), + }); + vi.stubGlobal("fetch", fetchSpy); + + const result = await tools.opa_get_futures.handler( + { contract: input }, + {}, + ); + + expect(result.isError).toBeUndefined(); + expect(new URL(String(fetchSpy.mock.calls[0][0])).pathname).toBe( + `/v1/futures/${canonical}`, + ); + }, + ); + + it.each(routeCases)( + "normalizes curve input %s to /v1/futures/%s/curve", + async (input, canonical) => { + vi.stubEnv("OILPRICEAPI_KEY", "test-key-123"); + const fetchSpy = vi.fn().mockResolvedValue({ + ok: true, + status: 200, + headers: { get: () => null }, + json: async () => ({ + contracts: [ + { contract_month: "2026-09", settlement_price: 70 }, + { contract_month: "2026-10", settlement_price: 71 }, + ], + }), + }); + vi.stubGlobal("fetch", fetchSpy); + + const result = await tools.opa_get_futures_curve.handler( + { contract: input }, + {}, + ); + + expect(result.isError).toBeUndefined(); + expect(new URL(String(fetchSpy.mock.calls[0][0])).pathname).toBe( + `/v1/futures/${canonical}/curve`, + ); + }, + ); + + it("publishes generic slugs while retaining legacy inputs", () => { + for (const slug of ["brent", "wti", "gasoil", "eu-carbon"] as const) { + expect(FUTURES_CONTRACTS).toContain(slug); + expect(FUTURES_CONTRACT_SLUGS[slug]).toBe(slug); + } + expect(FUTURES_CONTRACT_SLUGS["ice-brent"]).toBe("brent"); + expect(FUTURES_CONTRACT_SLUGS["ice-wti"]).toBe("wti"); + expect(FUTURES_CONTRACT_SLUGS["ice-gasoil"]).toBe("gasoil"); + expect(FUTURES_CONTRACT_SLUGS["eua-carbon"]).toBe("eu-carbon"); + expect(FUTURES_CONTRACT_NAMES["ice-brent"]).toBe("Brent Crude"); + expect(FUTURES_CONTRACT_NAMES["ice-wti"]).toBe("WTI Crude"); + expect(FUTURES_CONTRACT_NAMES["ice-gasoil"]).toBe("Gasoil"); + }); + + it("recommends generic slugs and scopes older venue names to compatibility", () => { + const metadata = ( + server as unknown as { + _registeredTools: Record; + } + )._registeredTools; + + for (const name of ["opa_get_futures", "opa_get_futures_curve"]) { + const description = metadata[name].description ?? ""; + expect(description).toContain("canonical instrument slugs"); + expect(description).toContain("compatibility inputs"); + expect(description).not.toMatch(/ICE Brent|ICE WTI|ICE Gasoil/); + } + }); +}); + describe("startup inventory", () => { it("reports enabled and registered counts without a fixed denominator", () => { expect( @@ -109,7 +236,9 @@ describe("wellPermitSearchEndpoint (#57)", () => { operator: "Example", county: "Reeves", }), - ).toEqual(expect.objectContaining({ error: expect.stringMatching(/cannot/) })); + ).toEqual( + expect.objectContaining({ error: expect.stringMatching(/cannot/) }), + ); expect( wellPermitSearchEndpoint({ state: "TX", @@ -119,9 +248,9 @@ describe("wellPermitSearchEndpoint (#57)", () => { ).toEqual( expect.objectContaining({ error: expect.stringMatching(/on or before/) }), ); - expect( - wellPermitSearchEndpoint({ state: "Atlantis" }), - ).toEqual(expect.objectContaining({ error: expect.stringMatching(/state/) })); + expect(wellPermitSearchEndpoint({ state: "Atlantis" })).toEqual( + expect.objectContaining({ error: expect.stringMatching(/state/) }), + ); }); }); @@ -143,7 +272,9 @@ describe("wellLookupEndpoint (#57)", () => { normalizedApi: "423294471300", }); expect(wellLookupEndpoint("4232")).toEqual( - expect.objectContaining({ error: expect.stringMatching(/10-, 12-, or 14/) }), + expect.objectContaining({ + error: expect.stringMatching(/10-, 12-, or 14/), + }), ); }); }); @@ -1141,7 +1272,7 @@ describe("tier-limit gate errors (#17) - makeApiRequest", () => { }); const error = await makeApiRequest( - "/v1/futures/ice-brent", + "/v1/futures/brent", mockFetch as typeof fetch, ).catch((e: unknown) => e); @@ -1355,7 +1486,9 @@ describe("tool registration metadata", () => { expect(description).toContain( "Observation-dated bulk backfills may appear in an earlier as_of result", ); - expect(description).not.toMatch(/as it was knowable|later-collected rows absent/i); + expect(description).not.toMatch( + /as it was knowable|later-collected rows absent/i, + ); }); it("registers all four write tools (creates + deletes)", () => { @@ -1566,7 +1699,6 @@ describe("reviewed plan allowance copy", () => { expect(text).toContain("Dataset access and limits vary by plan"); expect(text).not.toContain("latest prices only"); expect(text).not.toMatch(/200 requests?\/(?:mo|month)/i); - }); }); diff --git a/src/index.ts b/src/index.ts index c795d96..f3a532f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -79,7 +79,7 @@ export { // API Configuration const API_BASE = process.env.OILPRICEAPI_BASE_URL || "https://api.oilpriceapi.com"; -export const MCP_VERSION = "3.2.2"; +export const MCP_VERSION = "3.2.3"; export const CLIENT_MARKER = `oilpriceapi-mcp/${MCP_VERSION}`; export const USER_AGENT = CLIENT_MARKER; @@ -405,16 +405,19 @@ interface FuturesCurveContract { } // Supported futures contracts (used by opa_get_futures + opa_get_futures_curve). -// Accepts both legacy codes (BZ/CL) and friendly API slug names. Each maps to a -// canonical API slug used to build /v1/futures/{slug} and /v1/futures/{slug}/curve. +// Accepts canonical instrument slugs, contract codes, and legacy venue slugs. +// Every spelling maps to the instrument-generic API path. // There is NO generic ?contract= route — paths are per-commodity slugs. export const FUTURES_CONTRACTS = [ // Crude + "brent", + "wti", "BZ", "CL", "ice-brent", "ice-wti", // Gasoil + "gasoil", "G", "QS", "ice-gasoil", @@ -428,6 +431,7 @@ export const FUTURES_CONTRACTS = [ "JKM", "lng-jkm", // Carbon + "eu-carbon", "EUA", "eua-carbon", "UKA", @@ -439,21 +443,25 @@ export const FUTURES_CONTRACT_SLUGS: Record< (typeof FUTURES_CONTRACTS)[number], string > = { - BZ: "ice-brent", - CL: "ice-wti", - "ice-brent": "ice-brent", - "ice-wti": "ice-wti", - G: "ice-gasoil", - QS: "ice-gasoil", - "ice-gasoil": "ice-gasoil", + brent: "brent", + wti: "wti", + BZ: "brent", + CL: "wti", + "ice-brent": "brent", + "ice-wti": "wti", + gasoil: "gasoil", + G: "gasoil", + QS: "gasoil", + "ice-gasoil": "gasoil", NG: "natural-gas", "natural-gas": "natural-gas", TTF: "ttf-gas", "ttf-gas": "ttf-gas", JKM: "lng-jkm", "lng-jkm": "lng-jkm", - EUA: "eua-carbon", - "eua-carbon": "eua-carbon", + "eu-carbon": "eu-carbon", + EUA: "eu-carbon", + "eua-carbon": "eu-carbon", UKA: "uk-carbon", "uk-carbon": "uk-carbon", }; @@ -462,19 +470,23 @@ export const FUTURES_CONTRACT_NAMES: Record< (typeof FUTURES_CONTRACTS)[number], string > = { + brent: "Brent Crude", + wti: "WTI Crude", BZ: "Brent Crude", CL: "WTI Crude", - "ice-brent": "ICE Brent Crude", - "ice-wti": "ICE WTI Crude", - G: "ICE Gasoil", - QS: "ICE Gasoil", - "ice-gasoil": "ICE Gasoil", + "ice-brent": "Brent Crude", + "ice-wti": "WTI Crude", + gasoil: "Gasoil", + G: "Gasoil", + QS: "Gasoil", + "ice-gasoil": "Gasoil", NG: "Natural Gas", "natural-gas": "Natural Gas", TTF: "European TTF Natural Gas", "ttf-gas": "European TTF Natural Gas", JKM: "LNG JKM (Asia)", "lng-jkm": "LNG JKM (Asia)", + "eu-carbon": "EU Carbon Allowance (EUA)", EUA: "EU Carbon Allowance (EUA)", "eua-carbon": "EU Carbon Allowance (EUA)", UKA: "UK Carbon Allowance (UKA)", @@ -2235,7 +2247,7 @@ server.registerTool( title: "Get Price History", description: "Get historical price data for a commodity over a time period. Use when the user asks about price trends, historical prices, or how a commodity has performed over time. Returns high, low, average, change, and data point count. Periods: day (24h), week (7d), month (30d), year (365d). Supports point-in-time filtering via stored observation and revision timestamps. Observation-dated bulk backfills may appear in an earlier as_of result because their actual load time cannot be reconstructed. " + - ACCOUNT_ENTITLEMENT_GUIDANCE, + ACCOUNT_ENTITLEMENT_GUIDANCE, inputSchema: { commodity: z .string() @@ -2314,14 +2326,14 @@ server.registerTool( { title: "Get Futures Price", description: - "Get the latest front-month futures contract price for energy commodities. Use when the user asks about futures, forward prices, or contract prices. Supports crude oil (BZ/ice-brent = Brent, CL/ice-wti = WTI), ICE Gasoil (ice-gasoil), natural gas (natural-gas), European TTF gas (ttf-gas), LNG JKM (lng-jkm), EUA carbon (eua-carbon), and UK carbon (uk-carbon). For the forward curve returned for this account, use opa_get_futures_curve instead. " + - ACCOUNT_ENTITLEMENT_GUIDANCE, + "Get the latest front-month futures contract price for energy commodities. Use canonical instrument slugs: brent, wti, gasoil, natural-gas, ttf-gas, lng-jkm, eu-carbon, or uk-carbon. Contract codes and older venue slugs remain compatibility inputs. For the forward curve returned for this account, use opa_get_futures_curve instead. " + + ACCOUNT_ENTITLEMENT_GUIDANCE, inputSchema: { contract: z .enum(FUTURES_CONTRACTS) - .default("BZ") + .default("brent") .describe( - "Futures contract code or slug: BZ/ice-brent = Brent crude, CL/ice-wti = WTI crude, ice-gasoil (G/QS) = ICE Gasoil, natural-gas (NG) = Natural Gas, ttf-gas (TTF) = European TTF natural gas, lng-jkm (JKM) = LNG JKM (Asia), eua-carbon (EUA) = EU carbon allowance, uk-carbon (UKA) = UK carbon allowance (default: BZ)", + "Canonical instrument slug (recommended): brent, wti, gasoil, natural-gas, ttf-gas, lng-jkm, eu-carbon, or uk-carbon. Contract codes and legacy venue slugs remain accepted for compatibility (default: brent).", ), }, annotations: READ_TOOL_ANNOTATIONS, @@ -2363,14 +2375,14 @@ server.registerTool( { title: "Get Futures Curve", description: - "Get the futures forward-curve contracts returned for this account. Use when the user asks about the forward curve, contango/backwardation, or term structure. Supports crude oil (BZ/ice-brent = Brent, CL/ice-wti = WTI), ICE Gasoil (ice-gasoil), natural gas (natural-gas), European TTF gas (ttf-gas), LNG JKM (lng-jkm), EUA carbon (eua-carbon), and UK carbon (uk-carbon). Returns available contract months with settlement prices plus market-structure analysis. " + - ACCOUNT_ENTITLEMENT_GUIDANCE, + "Get the futures forward-curve contracts returned for this account. Use canonical instrument slugs: brent, wti, gasoil, natural-gas, ttf-gas, lng-jkm, eu-carbon, or uk-carbon. Contract codes and older venue slugs remain compatibility inputs. Returns available contract months with settlement prices plus market-structure analysis. " + + ACCOUNT_ENTITLEMENT_GUIDANCE, inputSchema: { contract: z .enum(FUTURES_CONTRACTS) - .default("BZ") + .default("brent") .describe( - "Futures contract code or slug: BZ/ice-brent = Brent crude, CL/ice-wti = WTI crude, ice-gasoil (G/QS) = ICE Gasoil, natural-gas (NG) = Natural Gas, ttf-gas (TTF) = European TTF natural gas, lng-jkm (JKM) = LNG JKM (Asia), eua-carbon (EUA) = EU carbon allowance, uk-carbon (UKA) = UK carbon allowance (default: BZ)", + "Canonical instrument slug (recommended): brent, wti, gasoil, natural-gas, ttf-gas, lng-jkm, eu-carbon, or uk-carbon. Contract codes and legacy venue slugs remain accepted for compatibility (default: brent).", ), }, annotations: READ_TOOL_ANNOTATIONS, @@ -2412,7 +2424,6 @@ server.registerTool( }, ); - // --------------------------------------------------------------------------- // US physical natural gas hubs (#64) — wraps /v1/natural-gas/hubs (api#1294). // The API endpoint exists precisely because customers searched for hub codes @@ -2437,7 +2448,12 @@ interface HubQuote { } interface HubsIndexData { - benchmark: { code: string; name: string; price: number | null; as_of?: string }; + benchmark: { + code: string; + name: string; + price: number | null; + as_of?: string; + }; hubs: HubQuote[]; meta?: Record; } @@ -2448,7 +2464,7 @@ server.registerTool( title: "Get US Natural Gas Hub Prices", description: "Get US physical natural gas hub prices as basis to Henry Hub (USD/MMBtu). Use when the user asks about regional gas prices or hub basis — Waha (West Texas/Permian), SoCal Citygate, Chicago Citygate, Algonquin Citygate, Eastern Gas South (formerly Dominion South), Houston Ship Channel. Without a hub, returns each live hub available to the account plus its basis; with a hub, returns that hub's latest price, basis, and basis history. Hub series differ in depth — check history_days before requesting a long window. " + - ACCOUNT_ENTITLEMENT_GUIDANCE, + ACCOUNT_ENTITLEMENT_GUIDANCE, inputSchema: { hub: z .string() @@ -2520,7 +2536,7 @@ server.registerTool( title: "Get Marine Fuel Prices", description: "Get latest marine fuel (bunker) prices returned for the account. Use when the user asks about bunker fuel, marine fuel, VLSFO, MGO, IFO380, or shipping fuel costs. Can filter by port (e.g., SINGAPORE, ROTTERDAM, HOUSTON) and/or fuel type (VLSFO, MGO, IFO380). Returns available port prices. " + - ACCOUNT_ENTITLEMENT_GUIDANCE, + ACCOUNT_ENTITLEMENT_GUIDANCE, inputSchema: { port: z .string() @@ -2578,7 +2594,7 @@ server.registerTool( title: "Get US Rig Counts", description: "Get the latest US oil and gas rig count data (Baker Hughes). Use when the user asks about drilling activity, rig counts, or oil field operations. Returns oil rigs, gas rigs, total count, and week-over-week change. No parameters needed. " + - ACCOUNT_ENTITLEMENT_GUIDANCE, + ACCOUNT_ENTITLEMENT_GUIDANCE, inputSchema: {}, annotations: READ_TOOL_ANNOTATIONS, }, @@ -2673,7 +2689,7 @@ server.registerTool( title: "Get Drilling Activity", description: "Get a drilling activity snapshot: US, Canada, and international rig counts, frac spread count, well permits issued in the last 30 days (with a by-state breakdown), and DUC (drilled-uncompleted) well totals. Use when the user asks about drilling activity, rigs vs frac spreads, or upstream operations. " + - ACCOUNT_ENTITLEMENT_GUIDANCE, + ACCOUNT_ENTITLEMENT_GUIDANCE, inputSchema: {}, annotations: READ_TOOL_ANNOTATIONS, }, @@ -2833,7 +2849,7 @@ server.registerTool( title: "Get Oil Storage Levels", description: "Get oil storage and inventory levels for Cushing, Oklahoma (WTI delivery hub) and/or the US Strategic Petroleum Reserve (SPR). Use when the user asks about oil inventories, storage levels, Cushing stocks, or the SPR. Returns current inventory levels with changes. " + - ACCOUNT_ENTITLEMENT_GUIDANCE, + ACCOUNT_ENTITLEMENT_GUIDANCE, inputSchema: { facility: z .enum(["cushing", "spr", "all"]) @@ -2894,7 +2910,7 @@ server.registerTool( title: "Get OPEC Production", description: "Get the latest OPEC oil production data. Use when the user asks about OPEC output, production quotas, supply cuts, or OPEC+ compliance. Returns available country-level production figures. " + - ACCOUNT_ENTITLEMENT_GUIDANCE, + ACCOUNT_ENTITLEMENT_GUIDANCE, inputSchema: {}, annotations: READ_TOOL_ANNOTATIONS, }, @@ -2925,7 +2941,7 @@ server.registerTool( title: "Get Price Forecasts", description: "Get energy price forecasts from EIA Short-Term Energy Outlook (STEO) and other sources. Use when the user asks about price predictions, outlooks, or where oil/gas prices are heading. Returns forecast data available to the account. " + - ACCOUNT_ENTITLEMENT_GUIDANCE, + ACCOUNT_ENTITLEMENT_GUIDANCE, inputSchema: {}, annotations: READ_TOOL_ANNOTATIONS, }, @@ -2961,7 +2977,7 @@ server.registerTool( title: "Get EIA Oil Inventories", description: "Get the latest EIA weekly petroleum inventory (stocks) data. Use when the user asks about oil inventories, crude stocks, weekly EIA stocks, inventory builds/draws, or product-level inventory levels. Returns the latest weekly figures available to the account; optionally a summary view or a breakdown by petroleum product. " + - ACCOUNT_ENTITLEMENT_GUIDANCE, + ACCOUNT_ENTITLEMENT_GUIDANCE, inputSchema: { view: z .enum(["latest", "summary", "by_product"]) @@ -3006,7 +3022,7 @@ server.registerTool( title: "Get Well Permits", description: "Get the latest US oil & gas well drilling permit data. Use when the user asks about well permits, new drilling permits, permitting activity, or upstream permit trends. Returns available permits; optionally filtered/aggregated by state or by operator. " + - ACCOUNT_ENTITLEMENT_GUIDANCE, + ACCOUNT_ENTITLEMENT_GUIDANCE, inputSchema: { view: z .enum(["latest", "by_state", "by_operator"]) @@ -3201,13 +3217,17 @@ server.registerTool( .min(1) .max(100) .optional() - .describe("Optional county-name search. Cannot be combined with operator."), + .describe( + "Optional county-name search. Cannot be combined with operator.", + ), operator: z .string() .min(1) .max(100) .optional() - .describe("Optional operator-name search. Cannot be combined with county."), + .describe( + "Optional operator-name search. Cannot be combined with county.", + ), start_date: z .string() .optional() @@ -3257,9 +3277,9 @@ server.registerTool( ); } - const response = await makeApiRequest< - ApiResponse> - >(mapped.endpoint); + const response = await makeApiRequest>>( + mapped.endpoint, + ); if (!response || response.status !== "success") { return errorResult( `No safe well-permit search result is available for ${mapped.stateCode}. Check account entitlement and retry.`, @@ -3325,11 +3345,15 @@ server.registerTool( api_number: z .string() .min(1) - .describe("10-, 12-, or 14-digit API well number; punctuation is allowed."), + .describe( + "10-, 12-, or 14-digit API well number; punctuation is allowed.", + ), state: z .string() .optional() - .describe("Optional state name/code used to disambiguate source records."), + .describe( + "Optional state name/code used to disambiguate source records.", + ), }, annotations: READ_TOOL_ANNOTATIONS, }, @@ -3448,10 +3472,7 @@ server.registerTool( const states = healthResponse.data?.states && typeof healthResponse.data.states === "object" - ? (healthResponse.data.states as Record< - string, - WellPermitStateHealth - >) + ? (healthResponse.data.states as Record) : {}; const warnings = Object.values(states).filter( (stateHealth) => stateHealth.status !== "available", @@ -3715,7 +3736,7 @@ server.registerTool( title: "Get Well Production", description: "Get US oil & gas well production data (BETA coverage: monthly state-level production from EIA + selected state regulators, and well-level histories for selected states only — NOT complete US well-level production). Views: summary (national + top states), states (reporting states returned by the API, latest month), state (monthly history for one state), well (monthly history for one well by 14-digit API number), top_producers (highest-output wells, optionally by state), cycle_time (permit-to-production cycle time stats, optionally by state), cohorts (cycle times by spud quarter). Use when the user asks about oil/gas production volumes by state or well, top producing wells, or drill-to-production cycle times. " + - ACCOUNT_ENTITLEMENT_GUIDANCE, + ACCOUNT_ENTITLEMENT_GUIDANCE, inputSchema: { view: z .enum(WELL_PRODUCTION_VIEWS) @@ -3774,7 +3795,7 @@ server.registerTool( title: "Get Refining & Trading Spreads", description: "Get refining and trading spreads: crack spreads (refining margin proxy), basis spreads (regional price differentials), and blending/transport margins. Use when the user asks about crack spreads, 3-2-1 crack, refining margins, basis differentials, or blend/transport margins. " + - ACCOUNT_ENTITLEMENT_GUIDANCE, + ACCOUNT_ENTITLEMENT_GUIDANCE, inputSchema: { type: z .enum(["crack", "basis", "margin"]) @@ -3846,7 +3867,6 @@ function formatAlertLine(a: AlertRecord): string { return `- **${a.name || label}** (id: \`${a.id}\`) — ${label} [${status}${triggers}${last}]`; } - // --------------------------------------------------------------------------- // Agent self-service (#63 follow-through): let an agent answer "what plan am I // on, what is left, and what would an upgrade cost" WITHOUT probing gated @@ -3927,9 +3947,10 @@ server.registerTool( annotations: READ_TOOL_ANNOTATIONS, }, async () => { - const response = await makeApiRequest< - ApiResponse<{ plans?: PricingPlan[] }> - >("/v1/pricing"); + const response = + await makeApiRequest>( + "/v1/pricing", + ); const plans = response?.data?.plans; if (!response || response.status !== "success" || !plans?.length) { return errorResult( @@ -3952,7 +3973,6 @@ server.registerTool( }, ); - // --------------------------------------------------------------------------- // Data-quality reports — wraps /v1/data-quality/{summary,reports/:code}. // Provenance surface: per-series grades and dimension scores, so an agent can @@ -3967,7 +3987,7 @@ server.registerTool( title: "Get Data Quality Report", description: "Get OilPriceAPI's own data-quality grades. With a commodity code: that series' quality report — overall grade/score plus dimension scores (completeness, freshness, and more) for the current period. Without a code: the API's available catalog summary (grade distribution by category). Use when the user asks how reliable/complete a series is, or which series carry the highest quality grades. The summary works on any valid key; per-commodity access varies. " + - ACCOUNT_ENTITLEMENT_GUIDANCE, + ACCOUNT_ENTITLEMENT_GUIDANCE, inputSchema: { commodity: z .string() @@ -3993,7 +4013,8 @@ server.registerTool( ); } let text = `# Data Quality — ${resolved.code}\n\n`; - text += "```json\n" + JSON.stringify(response.data.report, null, 2) + "\n```\n"; + text += + "```json\n" + JSON.stringify(response.data.report, null, 2) + "\n```\n"; text += "\n_Grades are computed per period from measured completeness/freshness — not marketing copy. | [OilPriceAPI](https://oilpriceapi.com)_"; return textResult(text); @@ -4006,7 +4027,8 @@ server.registerTool( return errorResult("Data-quality summary not available right now."); } let text = "# OilPriceAPI Data Quality — Catalogue Summary\n\n"; - text += "```json\n" + JSON.stringify(response.data.summary, null, 2) + "\n```\n"; + text += + "```json\n" + JSON.stringify(response.data.summary, null, 2) + "\n```\n"; text += "\n_Per-commodity reports: access varies by account entitlement; use opa_get_plans for current details. | [OilPriceAPI](https://oilpriceapi.com)_"; return textResult(text);