oilpriceapi-mcp 1.2.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/build/index.js CHANGED
@@ -1,9 +1,9 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
- * OilPriceAPI MCP Server
3
+ * OilPriceAPI MCP Server v2.0.0
4
4
  *
5
- * Provides real-time oil, gas, and commodity prices through the Model Context Protocol.
6
- * For use with Claude Desktop, Claude Code, and other MCP-compatible clients.
5
+ * The energy commodity MCP server. Real-time oil, gas, and commodity prices
6
+ * for Claude, Cursor, VS Code, and any MCP-compatible client.
7
7
  *
8
8
  * @see https://oilpriceapi.com
9
9
  * @see https://modelcontextprotocol.io
@@ -12,11 +12,13 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
12
12
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
13
13
  import { z } from "zod";
14
14
  // API Configuration
15
- const API_BASE = "https://api.oilpriceapi.com";
16
- export const USER_AGENT = "oilpriceapi-mcp/1.2.0";
15
+ const API_BASE = process.env.OILPRICEAPI_BASE_URL || "https://api.oilpriceapi.com";
16
+ export const USER_AGENT = "oilpriceapi-mcp/2.0.0";
17
17
  // Get API key from environment
18
18
  const API_KEY = process.env.OILPRICEAPI_KEY || process.env.OIL_PRICE_API_KEY;
19
+ // ---------------------------------------------------------------------------
19
20
  // Natural language to commodity code mapping
21
+ // ---------------------------------------------------------------------------
20
22
  export const COMMODITY_ALIASES = {
21
23
  // Crude Oil
22
24
  brent: "BRENT_CRUDE_USD",
@@ -68,7 +70,7 @@ export const COMMODITY_ALIASES = {
68
70
  "aviation fuel": "JET_FUEL_USD",
69
71
  kerosene: "JET_FUEL_USD",
70
72
  "heating oil": "HEATING_OIL_USD",
71
- // Other
73
+ // Precious Metals
72
74
  gold: "GOLD_USD",
73
75
  "gold am fix": "GOLD_AM_USD",
74
76
  "lbma gold am": "GOLD_AM_USD",
@@ -91,7 +93,9 @@ export const COMMODITY_ALIASES = {
91
93
  "gbp usd": "GBP_USD",
92
94
  sterling: "GBP_USD",
93
95
  };
96
+ // ---------------------------------------------------------------------------
94
97
  // Commodity metadata for formatting
98
+ // ---------------------------------------------------------------------------
95
99
  export const COMMODITY_INFO = {
96
100
  BRENT_CRUDE_USD: { name: "Brent Crude Oil", unit: "barrel" },
97
101
  WTI_USD: { name: "WTI Crude Oil", unit: "barrel" },
@@ -121,7 +125,7 @@ export const COMMODITY_INFO = {
121
125
  EUR_USD: { name: "Euro to USD", unit: "rate" },
122
126
  GBP_USD: { name: "British Pound to USD", unit: "rate" },
123
127
  };
124
- // Available commodity codes
128
+ // Available commodity codes (used for input validation)
125
129
  export const COMMODITY_CODES = [
126
130
  "BRENT_CRUDE_USD",
127
131
  "WTI_USD",
@@ -151,13 +155,73 @@ export const COMMODITY_CODES = [
151
155
  "EUR_USD",
152
156
  "GBP_USD",
153
157
  ];
158
+ // US state abbreviation lookup for diesel tool
159
+ const US_STATES = {
160
+ alabama: "AL",
161
+ alaska: "AK",
162
+ arizona: "AZ",
163
+ arkansas: "AR",
164
+ california: "CA",
165
+ colorado: "CO",
166
+ connecticut: "CT",
167
+ delaware: "DE",
168
+ florida: "FL",
169
+ georgia: "GA",
170
+ hawaii: "HI",
171
+ idaho: "ID",
172
+ illinois: "IL",
173
+ indiana: "IN",
174
+ iowa: "IA",
175
+ kansas: "KS",
176
+ kentucky: "KY",
177
+ louisiana: "LA",
178
+ maine: "ME",
179
+ maryland: "MD",
180
+ massachusetts: "MA",
181
+ michigan: "MI",
182
+ minnesota: "MN",
183
+ mississippi: "MS",
184
+ missouri: "MO",
185
+ montana: "MT",
186
+ nebraska: "NE",
187
+ nevada: "NV",
188
+ "new hampshire": "NH",
189
+ "new jersey": "NJ",
190
+ "new mexico": "NM",
191
+ "new york": "NY",
192
+ "north carolina": "NC",
193
+ "north dakota": "ND",
194
+ ohio: "OH",
195
+ oklahoma: "OK",
196
+ oregon: "OR",
197
+ pennsylvania: "PA",
198
+ "rhode island": "RI",
199
+ "south carolina": "SC",
200
+ "south dakota": "SD",
201
+ tennessee: "TN",
202
+ texas: "TX",
203
+ utah: "UT",
204
+ vermont: "VT",
205
+ virginia: "VA",
206
+ washington: "WA",
207
+ "west virginia": "WV",
208
+ wisconsin: "WI",
209
+ wyoming: "WY",
210
+ "district of columbia": "DC",
211
+ };
212
+ // ---------------------------------------------------------------------------
154
213
  // Create server instance
214
+ // ---------------------------------------------------------------------------
155
215
  const server = new McpServer({
156
216
  name: "oilpriceapi",
157
- version: "1.2.0",
217
+ version: "2.0.0",
158
218
  });
219
+ // ---------------------------------------------------------------------------
220
+ // Helpers
221
+ // ---------------------------------------------------------------------------
159
222
  /**
160
- * Resolve a natural language commodity name to its API code
223
+ * Resolve a natural language commodity name to its API code.
224
+ * Returns null if no match found — callers should return an actionable error.
161
225
  */
162
226
  export function resolveCommodityCode(input) {
163
227
  const normalized = input.toLowerCase().trim();
@@ -165,19 +229,83 @@ export function resolveCommodityCode(input) {
165
229
  if (COMMODITY_CODES.includes(normalized.toUpperCase())) {
166
230
  return normalized.toUpperCase();
167
231
  }
168
- // Try alias mapping
232
+ // Try exact alias mapping
169
233
  const mapped = COMMODITY_ALIASES[normalized];
170
234
  if (mapped) {
171
235
  return mapped;
172
236
  }
173
- // Fuzzy match - check if input contains key words
237
+ // Fuzzy match check if input contains key words
174
238
  for (const [alias, code] of Object.entries(COMMODITY_ALIASES)) {
175
239
  if (normalized.includes(alias) || alias.includes(normalized)) {
176
240
  return code;
177
241
  }
178
242
  }
179
- // Default to Brent if no match
180
- return "BRENT_CRUDE_USD";
243
+ // No match found
244
+ return null;
245
+ }
246
+ /**
247
+ * Find the closest matching alias for an unrecognized input.
248
+ */
249
+ function suggestCommodities(input) {
250
+ const normalized = input.toLowerCase().trim();
251
+ const suggestions = [];
252
+ for (const [alias, code] of Object.entries(COMMODITY_ALIASES)) {
253
+ let score = 0;
254
+ const words = normalized.split(/\s+/);
255
+ for (const word of words) {
256
+ if (alias.includes(word) || word.includes(alias)) {
257
+ score += word.length;
258
+ }
259
+ }
260
+ if (score > 0) {
261
+ suggestions.push({ alias, code, score });
262
+ }
263
+ }
264
+ // Deduplicate by code and return top 3
265
+ const seen = new Set();
266
+ return suggestions
267
+ .sort((a, b) => b.score - a.score)
268
+ .filter((s) => {
269
+ if (seen.has(s.code))
270
+ return false;
271
+ seen.add(s.code);
272
+ return true;
273
+ })
274
+ .slice(0, 3)
275
+ .map((s) => `'${s.alias}' (${s.code})`);
276
+ }
277
+ /**
278
+ * Build an error tool result with isError flag so LLMs can distinguish
279
+ * errors from data.
280
+ */
281
+ function errorResult(message) {
282
+ return {
283
+ content: [{ type: "text", text: message }],
284
+ isError: true,
285
+ };
286
+ }
287
+ /**
288
+ * Build a success tool result.
289
+ */
290
+ function textResult(text) {
291
+ return {
292
+ content: [{ type: "text", text }],
293
+ };
294
+ }
295
+ /**
296
+ * Handle commodity resolution with helpful error on no match.
297
+ */
298
+ function resolveOrError(input) {
299
+ const code = resolveCommodityCode(input);
300
+ if (code)
301
+ return { code };
302
+ const suggestions = suggestCommodities(input);
303
+ let msg = `Commodity '${input}' not recognized.`;
304
+ if (suggestions.length > 0) {
305
+ msg += ` Did you mean: ${suggestions.join(", ")}?`;
306
+ }
307
+ msg += " Use opa_list_commodities to see all available codes.";
308
+ return { error: errorResult(msg) };
181
309
  }
182
310
  /**
183
311
  * Format a price for display
@@ -226,7 +354,7 @@ export async function makeApiRequest(endpoint, fetchFn = fetch) {
226
354
  return (await response.json());
227
355
  }
228
356
  if (response.status === 401) {
229
- console.error("Authentication failed. Check OILPRICEAPI_KEY environment variable.");
357
+ console.error("Authentication failed. Set OILPRICEAPI_KEY environment variable. Get a free key at https://oilpriceapi.com/signup");
230
358
  return null;
231
359
  }
232
360
  // Retry on 429 and 5xx
@@ -239,7 +367,6 @@ export async function makeApiRequest(endpoint, fetchFn = fetch) {
239
367
  await new Promise((resolve) => setTimeout(resolve, delay));
240
368
  continue;
241
369
  }
242
- // Non-retryable HTTP error (403, 404, etc.) — return null immediately
243
370
  console.error(`HTTP ${response.status}: ${response.statusText} for ${endpoint}`);
244
371
  return null;
245
372
  }
@@ -248,66 +375,57 @@ export async function makeApiRequest(endpoint, fetchFn = fetch) {
248
375
  console.error(`API request failed after ${maxRetries + 1} attempts: ${endpoint}`, error);
249
376
  return null;
250
377
  }
251
- // Network error - retry with backoff
252
378
  const delay = Math.pow(2, attempt) * 1000;
253
379
  await new Promise((resolve) => setTimeout(resolve, delay));
254
380
  }
255
381
  }
256
382
  return null;
257
383
  }
258
- // Register Tools
259
384
  /**
260
- * Get current price of a specific commodity
385
+ * Resolve a US state name or abbreviation to a 2-letter code.
261
386
  */
262
- server.tool("get_commodity_price", "Get the current real-time price of an oil, gas, or energy commodity. Use natural language like 'brent oil', 'natural gas', 'wti', or 'diesel'.", {
387
+ export function resolveStateCode(input) {
388
+ const normalized = input.toLowerCase().trim();
389
+ // Already a 2-letter code
390
+ if (/^[a-z]{2}$/i.test(normalized)) {
391
+ const upper = normalized.toUpperCase();
392
+ // Verify it's a real state abbreviation
393
+ if (Object.values(US_STATES).includes(upper) || upper === "DC") {
394
+ return upper;
395
+ }
396
+ return null;
397
+ }
398
+ // Try full name lookup
399
+ return US_STATES[normalized] ?? null;
400
+ }
401
+ // =========================================================================
402
+ // TOOLS (14 total — opa_ prefixed to avoid collisions)
403
+ // =========================================================================
404
+ server.tool("opa_get_price", "Get the current real-time spot price of an energy commodity. Use when the user asks about a single commodity's current price. Accepts natural language ('brent oil', 'diesel') or API codes ('WTI_USD'). Returns price, currency, 24h change, and timestamp. For multiple commodities at once, use opa_market_overview. For price trends, use opa_get_history.", {
263
405
  commodity: z
264
406
  .string()
265
407
  .describe("Commodity name or code (e.g., 'brent oil', 'natural gas', 'WTI_USD', 'diesel')"),
266
408
  }, async ({ commodity }) => {
267
- const code = resolveCommodityCode(commodity);
268
- const response = await makeApiRequest(`/v1/prices/latest?by_code=${code}`);
409
+ const resolved = resolveOrError(commodity);
410
+ if ("error" in resolved)
411
+ return resolved.error;
412
+ const response = await makeApiRequest(`/v1/prices/latest?by_code=${resolved.code}`);
269
413
  if (!response || response.status !== "success") {
270
- return {
271
- content: [
272
- {
273
- type: "text",
274
- text: `Failed to retrieve price for ${commodity}. Please try again or check if the commodity is supported.`,
275
- },
276
- ],
277
- };
278
- }
279
- const formatted = formatPrice(response.data);
280
- return {
281
- content: [
282
- {
283
- type: "text",
284
- text: `${formatted}\n\n_Data from [OilPriceAPI](https://oilpriceapi.com)_`,
285
- },
286
- ],
287
- };
414
+ return errorResult(`Could not retrieve price for '${commodity}' (code: ${resolved.code}). The API may be temporarily unavailable — try again in a moment.`);
415
+ }
416
+ return textResult(`${formatPrice(response.data)}\n\n_Data from [OilPriceAPI](https://oilpriceapi.com)_`);
288
417
  });
289
- /**
290
- * Get all commodity prices (market overview)
291
- */
292
- server.tool("get_market_overview", "Get current prices for all tracked commodities. Returns a market overview with oil, gas, coal, and refined product prices.", {
418
+ server.tool("opa_market_overview", "Get current prices for all tracked energy commodities in one call. Use when the user wants a broad market snapshot or asks about overall energy prices. Returns prices grouped by category (oil, gas, coal, refined products, metals, forex) with 24h changes. Supports filtering by category. For a single commodity, use opa_get_price instead.", {
293
419
  category: z
294
- .enum(["all", "oil", "gas", "coal", "refined"])
420
+ .enum(["all", "oil", "gas", "coal", "refined", "metals", "forex"])
295
421
  .optional()
296
- .describe("Filter by commodity category (default: all)"),
422
+ .describe("Filter by commodity category (default: all). Options: oil, gas, coal, refined, metals, forex."),
297
423
  }, async ({ category = "all" }) => {
298
424
  const response = await makeApiRequest("/v1/prices/all");
299
425
  if (!response || response.status !== "success") {
300
- return {
301
- content: [
302
- {
303
- type: "text",
304
- text: "Failed to retrieve market data. Please try again.",
305
- },
306
- ],
307
- };
426
+ return errorResult("Could not retrieve market data. The API may be temporarily unavailable — try again in a moment.");
308
427
  }
309
428
  const prices = response.data.data.prices;
310
- // Category filters
311
429
  const categoryFilters = {
312
430
  oil: ["BRENT_CRUDE_USD", "WTI_USD", "URALS_CRUDE_USD", "DUBAI_CRUDE_USD"],
313
431
  gas: ["NATURAL_GAS_USD", "NATURAL_GAS_GBP", "DUTCH_TTF_EUR"],
@@ -319,6 +437,8 @@ server.tool("get_market_overview", "Get current prices for all tracked commoditi
319
437
  "JET_FUEL_USD",
320
438
  "HEATING_OIL_USD",
321
439
  ],
440
+ metals: ["GOLD_USD", "GOLD_AM_USD", "GOLD_PM_USD", "SILVER_FIX_USD"],
441
+ forex: ["EUR_USD", "GBP_USD"],
322
442
  };
323
443
  let filteredCodes;
324
444
  if (category === "all") {
@@ -328,12 +448,13 @@ server.tool("get_market_overview", "Get current prices for all tracked commoditi
328
448
  filteredCodes = categoryFilters[category] || [];
329
449
  }
330
450
  const sections = ["# Energy Market Overview\n"];
331
- // Group by category
332
451
  const groupedPrices = {
333
452
  "Crude Oil": [],
334
453
  "Natural Gas": [],
335
454
  Coal: [],
336
455
  "Refined Products": [],
456
+ "Precious Metals": [],
457
+ Forex: [],
337
458
  Other: [],
338
459
  };
339
460
  for (const code of filteredCodes) {
@@ -359,6 +480,12 @@ server.tool("get_market_overview", "Get current prices for all tracked commoditi
359
480
  ].includes(code)) {
360
481
  groupedPrices["Refined Products"].push(data);
361
482
  }
483
+ else if (code.includes("GOLD") || code.includes("SILVER")) {
484
+ groupedPrices["Precious Metals"].push(data);
485
+ }
486
+ else if (code === "EUR_USD" || code === "GBP_USD") {
487
+ groupedPrices["Forex"].push(data);
488
+ }
362
489
  else {
363
490
  groupedPrices["Other"].push(data);
364
491
  }
@@ -386,156 +513,143 @@ server.tool("get_market_overview", "Get current prices for all tracked commoditi
386
513
  }
387
514
  sections.push("");
388
515
  }
389
- sections.push(`_Updated: ${new Date(response.data.data.timestamp).toLocaleString("en-US", {
390
- dateStyle: "medium",
391
- timeStyle: "short",
392
- timeZone: "UTC",
393
- })} UTC | Data from [OilPriceAPI](https://oilpriceapi.com)_`);
394
- return {
395
- content: [
396
- {
397
- type: "text",
398
- text: sections.join("\n"),
399
- },
400
- ],
401
- };
516
+ sections.push(`_Updated: ${new Date(response.data.data.timestamp).toLocaleString("en-US", { dateStyle: "medium", timeStyle: "short", timeZone: "UTC" })} UTC | Data from [OilPriceAPI](https://oilpriceapi.com)_`);
517
+ return textResult(sections.join("\n"));
402
518
  });
403
- /**
404
- * Compare prices between commodities
405
- */
406
- server.tool("compare_prices", "Compare current prices between multiple commodities (e.g., Brent vs WTI, US gas vs European gas).", {
519
+ server.tool("opa_compare_prices", "Compare current prices between 2-5 commodities side by side. Use when the user asks to compare commodities (e.g., 'Brent vs WTI', 'US gas vs EU gas'). Returns each commodity's price with 24h changes, plus the spread if comparing two same-currency commodities. Accepts natural language or codes.", {
407
520
  commodities: z
408
521
  .array(z.string())
409
522
  .min(2)
410
523
  .max(5)
411
- .describe("List of commodities to compare (2-5 items)"),
524
+ .describe("List of 2-5 commodity names or codes to compare (e.g., ['brent', 'wti'] or ['NATURAL_GAS_USD', 'DUTCH_TTF_EUR'])"),
412
525
  }, async ({ commodities }) => {
413
- const codes = commodities.map(resolveCommodityCode);
414
526
  const results = [];
415
- for (const code of codes) {
416
- const response = await makeApiRequest(`/v1/prices/latest?by_code=${code}`);
527
+ const errors = [];
528
+ for (const commodity of commodities) {
529
+ const resolved = resolveOrError(commodity);
530
+ if ("error" in resolved) {
531
+ errors.push(commodity);
532
+ continue;
533
+ }
534
+ const response = await makeApiRequest(`/v1/prices/latest?by_code=${resolved.code}`);
417
535
  if (response?.status === "success") {
418
536
  results.push(response.data);
419
537
  }
420
538
  }
421
539
  if (results.length < 2) {
422
- return {
423
- content: [
424
- {
425
- type: "text",
426
- text: "Could not retrieve enough price data for comparison. Please check commodity names.",
427
- },
428
- ],
429
- };
540
+ let msg = "Could not retrieve enough price data for comparison (need at least 2).";
541
+ if (errors.length > 0) {
542
+ msg += ` Unrecognized commodities: ${errors.join(", ")}. Use opa_list_commodities to see valid codes.`;
543
+ }
544
+ return errorResult(msg);
430
545
  }
431
546
  const sections = ["# Price Comparison\n"];
432
547
  for (const data of results) {
433
548
  sections.push(formatPrice(data));
434
549
  sections.push("");
435
550
  }
436
- // Calculate spread if comparing similar commodities
437
551
  if (results.length === 2 && results[0].currency === results[1].currency) {
438
552
  const spread = Math.abs(results[0].price - results[1].price);
439
553
  const info0 = COMMODITY_INFO[results[0].code]?.name || results[0].code;
440
554
  const info1 = COMMODITY_INFO[results[1].code]?.name || results[1].code;
441
- sections.push(`**Spread**: $${spread.toFixed(2)} (${info0} vs ${info1})`);
555
+ const currencySymbol = results[0].currency === "EUR"
556
+ ? "€"
557
+ : results[0].currency === "GBP"
558
+ ? "£"
559
+ : "$";
560
+ sections.push(`**Spread**: ${currencySymbol}${spread.toFixed(2)} (${info0} vs ${info1})`);
442
561
  }
443
562
  sections.push(`\n_Data from [OilPriceAPI](https://oilpriceapi.com)_`);
444
- return {
445
- content: [
446
- {
447
- type: "text",
448
- text: sections.join("\n"),
449
- },
450
- ],
451
- };
563
+ return textResult(sections.join("\n"));
452
564
  });
453
- /**
454
- * List available commodities
455
- */
456
- server.tool("list_commodities", "List all available commodities that can be queried for prices.", {}, async () => {
565
+ server.tool("opa_list_commodities", "List all available commodities that can be queried for prices. Use when the user asks what commodities are available, what codes to use, or when another tool returns a 'commodity not recognized' error. Returns the full catalog fetched live from the API, grouped by category. No parameters needed.", {}, async () => {
566
+ // Try to fetch the live commodity catalog from the API
567
+ const response = await makeApiRequest("/v1/commodities");
568
+ if (response?.status === "success" && response.data.commodities?.length) {
569
+ const grouped = {};
570
+ for (const c of response.data.commodities) {
571
+ const cat = c.category || "Other";
572
+ if (!grouped[cat])
573
+ grouped[cat] = [];
574
+ grouped[cat].push({ code: c.code, name: c.name });
575
+ }
576
+ const sections = [
577
+ `# Available Commodities (${response.data.commodities.length} total)\n`,
578
+ ];
579
+ for (const [category, items] of Object.entries(grouped)) {
580
+ sections.push(`## ${category}`);
581
+ for (const item of items) {
582
+ sections.push(`- \`${item.code}\` — ${item.name}`);
583
+ }
584
+ sections.push("");
585
+ }
586
+ sections.push("_You can use natural language like 'brent oil' or 'natural gas' — the server translates it to the right code._");
587
+ return textResult(sections.join("\n"));
588
+ }
589
+ // Fallback to static list if API call fails
457
590
  const sections = ["# Available Commodities\n"];
458
591
  sections.push("## Crude Oil");
459
- sections.push("- `BRENT_CRUDE_USD` - Brent Crude (global benchmark)");
460
- sections.push("- `WTI_USD` - West Texas Intermediate (US benchmark)");
461
- sections.push("- `URALS_CRUDE_USD` - Urals Crude (Russian)");
462
- sections.push("- `DUBAI_CRUDE_USD` - Dubai Crude (Middle East)");
592
+ sections.push("- `BRENT_CRUDE_USD` Brent Crude (global benchmark)");
593
+ sections.push("- `WTI_USD` West Texas Intermediate (US benchmark)");
594
+ sections.push("- `URALS_CRUDE_USD` Urals Crude (Russian)");
595
+ sections.push("- `DUBAI_CRUDE_USD` Dubai Crude (Middle East)");
463
596
  sections.push("");
464
597
  sections.push("## Natural Gas");
465
- sections.push("- `NATURAL_GAS_USD` - US Henry Hub ($/MMBtu)");
466
- sections.push("- `NATURAL_GAS_GBP` - UK NBP (pence/therm)");
467
- sections.push("- `DUTCH_TTF_EUR` - European TTF (€/MWh)");
598
+ sections.push("- `NATURAL_GAS_USD` US Henry Hub ($/MMBtu)");
599
+ sections.push("- `NATURAL_GAS_GBP` UK NBP (pence/therm)");
600
+ sections.push("- `DUTCH_TTF_EUR` European TTF (€/MWh)");
468
601
  sections.push("");
469
602
  sections.push("## Coal");
470
- sections.push("- `COAL_USD` - Thermal Coal");
471
- sections.push("- `NEWCASTLE_COAL_USD` - Newcastle (Asia-Pacific)");
603
+ sections.push("- `COAL_USD` Thermal Coal");
604
+ sections.push("- `NEWCASTLE_COAL_USD` Newcastle (Asia-Pacific)");
472
605
  sections.push("");
473
606
  sections.push("## Refined Products");
474
- sections.push("- `DIESEL_USD` - Diesel");
475
- sections.push("- `GASOLINE_USD` - Gasoline");
476
- sections.push("- `GASOLINE_RBOB_USD` - RBOB Gasoline");
477
- sections.push("- `JET_FUEL_USD` - Jet Fuel");
478
- sections.push("- `HEATING_OIL_USD` - Heating Oil");
607
+ sections.push("- `DIESEL_USD` Diesel");
608
+ sections.push("- `GASOLINE_USD` Gasoline");
609
+ sections.push("- `GASOLINE_RBOB_USD` RBOB Gasoline");
610
+ sections.push("- `JET_FUEL_USD` Jet Fuel");
611
+ sections.push("- `HEATING_OIL_USD` Heating Oil");
479
612
  sections.push("");
480
613
  sections.push("## Precious Metals");
481
- sections.push("- `GOLD_USD` - Gold");
482
- sections.push("- `GOLD_AM_USD` - LBMA Gold AM Fix (USD)");
483
- sections.push("- `GOLD_AM_GBP` - LBMA Gold AM Fix (GBP)");
484
- sections.push("- `GOLD_AM_EUR` - LBMA Gold AM Fix (EUR)");
485
- sections.push("- `GOLD_PM_USD` - LBMA Gold PM Fix (USD)");
486
- sections.push("- `GOLD_PM_GBP` - LBMA Gold PM Fix (GBP)");
487
- sections.push("- `GOLD_PM_EUR` - LBMA Gold PM Fix (EUR)");
488
- sections.push("- `SILVER_FIX_USD` - LBMA Silver Fix (USD)");
489
- sections.push("- `SILVER_FIX_GBP` - LBMA Silver Fix (GBP)");
490
- sections.push("- `SILVER_FIX_EUR` - LBMA Silver Fix (EUR)");
614
+ sections.push("- `GOLD_USD` Gold");
615
+ sections.push("- `SILVER_FIX_USD` LBMA Silver Fix");
491
616
  sections.push("");
492
617
  sections.push("## Other");
493
- sections.push("- `EU_CARBON_EUR` - EU Carbon Allowances");
494
- sections.push("- `EUR_USD` - Euro to USD");
495
- sections.push("- `GBP_USD` - British Pound to USD");
618
+ sections.push("- `EU_CARBON_EUR` EU Carbon Allowances");
619
+ sections.push("- `EUR_USD` Euro to USD");
620
+ sections.push("- `GBP_USD` British Pound to USD");
496
621
  sections.push("");
497
- sections.push("_You can use natural language like 'brent oil' or 'natural gas' - I'll translate it to the right code._");
498
- return {
499
- content: [
500
- {
501
- type: "text",
502
- text: sections.join("\n"),
503
- },
504
- ],
505
- };
622
+ sections.push("_Note: This is a partial list (API was unreachable). The full catalog has 70+ commodities. Try again later for the complete list._");
623
+ return textResult(sections.join("\n"));
506
624
  });
507
- /**
508
- * Get historical price data for a commodity
509
- */
510
- server.tool("get_historical_prices", "Get historical price data for a commodity over a time period (past day, week, month, or year).", {
511
- commodity: z.string().describe("Commodity name or code"),
625
+ server.tool("opa_get_history", "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).", {
626
+ commodity: z
627
+ .string()
628
+ .describe("Commodity name or code (e.g., 'brent', 'WTI_USD')"),
512
629
  period: z
513
630
  .enum(["day", "week", "month", "year"])
514
631
  .default("month")
515
- .describe("Time period"),
632
+ .describe("Time period: day, week, month, or year (default: month)"),
516
633
  }, async ({ commodity, period }) => {
517
- const code = resolveCommodityCode(commodity);
518
- const response = await makeApiRequest(`/v1/prices/past_${period}?by_code=${code}`);
634
+ const resolved = resolveOrError(commodity);
635
+ if ("error" in resolved)
636
+ return resolved.error;
637
+ const response = await makeApiRequest(`/v1/prices/past_${period}?by_code=${resolved.code}`);
519
638
  if (!response ||
520
639
  response.status !== "success" ||
521
640
  !response.data.prices?.length) {
522
- return {
523
- content: [
524
- {
525
- type: "text",
526
- text: `No historical data found for ${commodity} over the past ${period}.`,
527
- },
528
- ],
529
- };
530
- }
531
- const info = COMMODITY_INFO[code] || { name: code, unit: "unit" };
532
- // Derive currency symbol from commodity code (same logic as formatPrice)
533
- const currencyFromCode = code.endsWith("_EUR")
641
+ return errorResult(`No historical data found for '${commodity}' (code: ${resolved.code}) over the past ${period}. This commodity may not have enough data for this period, or may require a paid plan.`);
642
+ }
643
+ const info = COMMODITY_INFO[resolved.code] || {
644
+ name: resolved.code,
645
+ unit: "unit",
646
+ };
647
+ const currencyFromCode = resolved.code.endsWith("_EUR")
534
648
  ? "EUR"
535
- : code.endsWith("_GBP") || code.endsWith("_GBp")
649
+ : resolved.code.endsWith("_GBP") || resolved.code.endsWith("_GBp")
536
650
  ? "GBP"
537
651
  : "USD";
538
- const historicalCurrencySymbol = currencyFromCode === "EUR" ? "€" : currencyFromCode === "GBP" ? "£" : "$";
652
+ const sym = currencyFromCode === "EUR" ? "€" : currencyFromCode === "GBP" ? "£" : "$";
539
653
  const prices = response.data.prices;
540
654
  const latest = prices[0];
541
655
  const oldest = prices[prices.length - 1];
@@ -545,38 +659,28 @@ server.tool("get_historical_prices", "Get historical price data for a commodity
545
659
  const change = latest.price - oldest.price;
546
660
  const changePct = (change / oldest.price) * 100;
547
661
  const sections = [
548
- `# ${info.name} - Past ${period.charAt(0).toUpperCase() + period.slice(1)} Historical Data\n`,
549
- `- **Latest**: ${historicalCurrencySymbol}${latest.price.toFixed(2)}/${info.unit}`,
550
- `- **High**: ${historicalCurrencySymbol}${high.toFixed(2)}`,
551
- `- **Low**: ${historicalCurrencySymbol}${low.toFixed(2)}`,
552
- `- **Average**: ${historicalCurrencySymbol}${avg.toFixed(2)}`,
553
- `- **Change**: ${change >= 0 ? "+" : ""}${historicalCurrencySymbol}${change.toFixed(2)} (${change >= 0 ? "+" : ""}${changePct.toFixed(1)}%)`,
662
+ `# ${info.name} Past ${period.charAt(0).toUpperCase() + period.slice(1)}\n`,
663
+ `- **Latest**: ${sym}${latest.price.toFixed(2)}/${info.unit}`,
664
+ `- **High**: ${sym}${high.toFixed(2)}`,
665
+ `- **Low**: ${sym}${low.toFixed(2)}`,
666
+ `- **Average**: ${sym}${avg.toFixed(2)}`,
667
+ `- **Change**: ${change >= 0 ? "+" : ""}${sym}${change.toFixed(2)} (${change >= 0 ? "+" : ""}${changePct.toFixed(1)}%)`,
554
668
  `- **Data Points**: ${prices.length}`,
555
669
  `\n_Data from [OilPriceAPI](https://oilpriceapi.com)_`,
556
670
  ];
557
- return { content: [{ type: "text", text: sections.join("\n") }] };
671
+ return textResult(sections.join("\n"));
558
672
  });
559
- /**
560
- * Get the latest futures contract price
561
- */
562
- server.tool("get_futures_price", "Get the latest futures contract price for a commodity (Brent BZ or WTI CL).", {
673
+ server.tool("opa_get_futures", "Get the latest front-month futures contract price for crude oil. Use when the user asks about futures, forward prices, or contract prices. Supports Brent (BZ) and WTI (CL) futures. For the full forward curve across all contract months, use opa_get_futures_curve instead.", {
563
674
  contract: z
564
675
  .enum(["BZ", "CL"])
565
676
  .default("BZ")
566
- .describe("Futures contract (BZ=Brent, CL=WTI)"),
677
+ .describe("Futures contract: BZ = Brent crude, CL = WTI crude (default: BZ)"),
567
678
  }, async ({ contract }) => {
568
679
  const response = await makeApiRequest(`/v1/futures/latest?contract=${contract}`);
569
680
  if (!response ||
570
681
  response.status !== "success" ||
571
682
  !response.data.contracts?.length) {
572
- return {
573
- content: [
574
- {
575
- type: "text",
576
- text: `No futures data available for contract ${contract}.`,
577
- },
578
- ],
579
- };
683
+ return errorResult(`No futures data available for ${contract === "BZ" ? "Brent" : "WTI"} (${contract}). Futures data requires a paid plan.`);
580
684
  }
581
685
  const contractName = contract === "BZ" ? "Brent Crude" : "WTI Crude";
582
686
  const front = response.data.contracts[0];
@@ -586,29 +690,19 @@ server.tool("get_futures_price", "Get the latest futures contract price for a co
586
690
  text += ` (${front.change >= 0 ? "+" : ""}$${front.change.toFixed(2)})`;
587
691
  }
588
692
  text += `\n\n_Data from [OilPriceAPI](https://oilpriceapi.com)_`;
589
- return { content: [{ type: "text", text }] };
693
+ return textResult(text);
590
694
  });
591
- /**
592
- * Get the full futures forward curve
593
- */
594
- server.tool("get_futures_curve", "Get the full futures forward curve showing prices across contract months.", {
695
+ server.tool("opa_get_futures_curve", "Get the full futures forward curve showing prices across all contract months. Use when the user asks about the forward curve, contango/backwardation, or term structure. Returns a table of contract months with prices and changes, plus market structure analysis.", {
595
696
  contract: z
596
697
  .enum(["BZ", "CL"])
597
698
  .default("BZ")
598
- .describe("Futures contract (BZ=Brent, CL=WTI)"),
699
+ .describe("Futures contract: BZ = Brent crude, CL = WTI crude (default: BZ)"),
599
700
  }, async ({ contract }) => {
600
701
  const response = await makeApiRequest(`/v1/futures/curve?contract=${contract}`);
601
702
  if (!response ||
602
703
  response.status !== "success" ||
603
704
  !response.data.contracts?.length) {
604
- return {
605
- content: [
606
- {
607
- type: "text",
608
- text: `No futures curve data available for contract ${contract}.`,
609
- },
610
- ],
611
- };
705
+ return errorResult(`No futures curve data available for ${contract === "BZ" ? "Brent" : "WTI"} (${contract}). Futures data requires a paid plan.`);
612
706
  }
613
707
  const contractName = contract === "BZ" ? "Brent Crude" : "WTI Crude";
614
708
  const contracts = response.data.contracts;
@@ -625,20 +719,17 @@ server.tool("get_futures_curve", "Get the full futures forward curve showing pri
625
719
  const structure = front > back ? "backwardation" : "contango";
626
720
  text += `\n**Market Structure**: ${structure} (front $${front.toFixed(2)} vs back $${back.toFixed(2)})`;
627
721
  text += `\n\n_Data from [OilPriceAPI](https://oilpriceapi.com)_`;
628
- return { content: [{ type: "text", text }] };
722
+ return textResult(text);
629
723
  });
630
- /**
631
- * Get marine fuel (bunker) prices
632
- */
633
- server.tool("get_marine_fuel_prices", "Get latest marine fuel (bunker) prices across major ports. Includes VLSFO, MGO, and IFO380.", {
724
+ server.tool("opa_get_marine_fuels", "Get latest marine fuel (bunker) prices across major shipping ports. 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 a table of port prices.", {
634
725
  port: z
635
726
  .string()
636
727
  .optional()
637
- .describe("Filter by port name (e.g., 'SINGAPORE', 'ROTTERDAM')"),
728
+ .describe("Filter by port name (e.g., 'SINGAPORE', 'ROTTERDAM', 'HOUSTON')"),
638
729
  fuel_type: z
639
730
  .string()
640
731
  .optional()
641
- .describe("Filter by fuel type (VLSFO, MGO, IFO380)"),
732
+ .describe("Filter by fuel type: VLSFO, MGO, or IFO380"),
642
733
  }, async ({ port, fuel_type }) => {
643
734
  let endpoint = "/v1/marine-fuels/latest";
644
735
  const params = [];
@@ -652,11 +743,7 @@ server.tool("get_marine_fuel_prices", "Get latest marine fuel (bunker) prices ac
652
743
  if (!response ||
653
744
  response.status !== "success" ||
654
745
  !response.data.prices?.length) {
655
- return {
656
- content: [
657
- { type: "text", text: "No marine fuel price data available." },
658
- ],
659
- };
746
+ return errorResult("No marine fuel price data available. Marine fuel data requires a paid plan with bunker fuel coverage.");
660
747
  }
661
748
  const prices = response.data.prices;
662
749
  let text = "# Marine Fuel Prices\n\n";
@@ -666,20 +753,15 @@ server.tool("get_marine_fuel_prices", "Get latest marine fuel (bunker) prices ac
666
753
  text += `| ${p.port} | ${p.fuel_type} | ${p.price.toFixed(2)} | ${p.currency} | ${p.unit} |\n`;
667
754
  }
668
755
  text += `\n_${prices.length} prices | Data from [OilPriceAPI](https://oilpriceapi.com)_`;
669
- return { content: [{ type: "text", text }] };
756
+ return textResult(text);
670
757
  });
671
- /**
672
- * Get the latest oil and gas rig count data
673
- */
674
- server.tool("get_rig_counts", "Get the latest oil and gas rig count data (Baker Hughes).", {}, async () => {
758
+ server.tool("opa_get_rig_counts", "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.", {}, async () => {
675
759
  const response = await makeApiRequest("/v1/rig-counts/latest");
676
760
  if (!response || response.status !== "success") {
677
- return {
678
- content: [{ type: "text", text: "Rig count data not available." }],
679
- };
761
+ return errorResult("Rig count data not available. This may require a paid plan with energy intelligence access.");
680
762
  }
681
763
  const data = response.data;
682
- let text = `# Rig Count Data\n\n`;
764
+ let text = `# US Rig Count (Baker Hughes)\n\n`;
683
765
  text += `- **Oil Rigs**: ${data.oil}\n`;
684
766
  text += `- **Gas Rigs**: ${data.gas}\n`;
685
767
  text += `- **Total**: ${data.total}\n`;
@@ -689,22 +771,12 @@ server.tool("get_rig_counts", "Get the latest oil and gas rig count data (Baker
689
771
  }
690
772
  text += `- **Date**: ${data.date}\n`;
691
773
  text += `\n_Data from [OilPriceAPI](https://oilpriceapi.com)_`;
692
- return { content: [{ type: "text", text }] };
774
+ return textResult(text);
693
775
  });
694
- /**
695
- * Get drilling intelligence data
696
- */
697
- server.tool("get_drilling_intelligence", "Get drilling intelligence data including active wells, permits, and completions.", {}, async () => {
776
+ server.tool("opa_get_drilling", "Get drilling intelligence data including active wells, permits issued, and completions by region. Use when the user asks about drilling activity, well permits, or upstream operations. Returns totals and regional breakdown.", {}, async () => {
698
777
  const response = await makeApiRequest("/v1/drilling/latest");
699
778
  if (!response || response.status !== "success") {
700
- return {
701
- content: [
702
- {
703
- type: "text",
704
- text: "Drilling intelligence data not available.",
705
- },
706
- ],
707
- };
779
+ return errorResult("Drilling intelligence data not available. This requires a paid plan with energy intelligence access.");
708
780
  }
709
781
  const data = response.data;
710
782
  let text = `# Drilling Intelligence\n\n`;
@@ -722,9 +794,94 @@ server.tool("get_drilling_intelligence", "Get drilling intelligence data includi
722
794
  }
723
795
  }
724
796
  text += `\n_Data from [OilPriceAPI](https://oilpriceapi.com)_`;
725
- return { content: [{ type: "text", text }] };
797
+ return textResult(text);
798
+ });
799
+ // ---------------------------------------------------------------------------
800
+ // NEW TOOLS — Sprint 3
801
+ // ---------------------------------------------------------------------------
802
+ server.tool("opa_get_diesel_by_state", "Get the current average retail diesel price for a US state. Use when the user asks about diesel prices in a specific state, diesel fuel costs by state, or state-level fuel prices. Accepts state names ('California') or 2-letter codes ('CA'). Returns the AAA-sourced state average diesel price. Covers all 50 states plus DC.", {
803
+ state: z
804
+ .string()
805
+ .describe("US state name or 2-letter code (e.g., 'California', 'CA', 'Texas', 'TX')"),
806
+ }, async ({ state }) => {
807
+ const stateCode = resolveStateCode(state);
808
+ if (!stateCode) {
809
+ return errorResult(`'${state}' is not a recognized US state. Use a full state name (e.g., 'California') or 2-letter code (e.g., 'CA').`);
810
+ }
811
+ const code = `DIESEL_RETAIL_STATE_${stateCode}_USD`;
812
+ const response = await makeApiRequest(`/v1/prices/latest?by_code=${code}`);
813
+ if (!response || response.status !== "success") {
814
+ return errorResult(`No diesel price data available for ${state} (${stateCode}). State diesel data requires a plan with AAA diesel coverage.`);
815
+ }
816
+ const data = response.data;
817
+ let text = `# Diesel Price — ${stateCode}\n\n`;
818
+ text += `- **Price**: $${data.price.toFixed(3)}/gallon\n`;
819
+ if (data.change_24h !== undefined) {
820
+ const sign = data.change_24h >= 0 ? "+" : "";
821
+ text += `- **24h Change**: ${sign}$${data.change_24h.toFixed(3)}\n`;
822
+ }
823
+ const timestamp = data.updated_at || data.created_at;
824
+ if (timestamp) {
825
+ text += `- **Updated**: ${new Date(timestamp).toLocaleString("en-US", { dateStyle: "medium", timeStyle: "short", timeZone: "UTC" })} UTC\n`;
826
+ }
827
+ text += `- **Source**: AAA\n`;
828
+ text += `\n_Data from [OilPriceAPI](https://oilpriceapi.com)_`;
829
+ return textResult(text);
830
+ });
831
+ server.tool("opa_get_storage", "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.", {
832
+ facility: z
833
+ .enum(["cushing", "spr", "all"])
834
+ .default("all")
835
+ .describe("Storage facility: cushing (WTI delivery hub), spr (Strategic Petroleum Reserve), or all (default: all)"),
836
+ }, async ({ facility }) => {
837
+ const sections = ["# Oil Storage Levels\n"];
838
+ let hasData = false;
839
+ if (facility === "cushing" || facility === "all") {
840
+ const response = await makeApiRequest("/v1/storage/cushing");
841
+ if (response?.status === "success") {
842
+ hasData = true;
843
+ sections.push("## Cushing, Oklahoma (WTI Hub)\n");
844
+ sections.push("```json\n" + JSON.stringify(response.data, null, 2) + "\n```\n");
845
+ }
846
+ }
847
+ if (facility === "spr" || facility === "all") {
848
+ const response = await makeApiRequest("/v1/storage/spr");
849
+ if (response?.status === "success") {
850
+ hasData = true;
851
+ sections.push("## Strategic Petroleum Reserve (SPR)\n");
852
+ sections.push("```json\n" + JSON.stringify(response.data, null, 2) + "\n```\n");
853
+ }
854
+ }
855
+ if (!hasData) {
856
+ return errorResult("Storage data not available. This requires a paid plan with energy intelligence access.");
857
+ }
858
+ sections.push("_Data from [OilPriceAPI](https://oilpriceapi.com)_");
859
+ return textResult(sections.join("\n"));
726
860
  });
727
- // Register Resources subscribable price snapshots
861
+ server.tool("opa_get_opec_production", "Get the latest OPEC oil production data. Use when the user asks about OPEC output, production quotas, supply cuts, or OPEC+ compliance. Returns country-level production figures. Requires a paid plan with energy intelligence access.", {}, async () => {
862
+ const response = await makeApiRequest("/v1/ei/opec_productions/latest");
863
+ if (!response || response.status !== "success") {
864
+ return errorResult("OPEC production data not available. This requires a paid plan with energy intelligence access.");
865
+ }
866
+ let text = "# OPEC Production Data\n\n";
867
+ text += "```json\n" + JSON.stringify(response.data, null, 2) + "\n```\n";
868
+ text += "\n_Data from [OilPriceAPI](https://oilpriceapi.com)_";
869
+ return textResult(text);
870
+ });
871
+ server.tool("opa_get_forecasts", "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 for key commodities. Requires a paid plan with energy intelligence access.", {}, async () => {
872
+ const response = await makeApiRequest("/v1/ei/forecasts/latest");
873
+ if (!response || response.status !== "success") {
874
+ return errorResult("Forecast data not available. This requires a paid plan with energy intelligence access.");
875
+ }
876
+ let text = "# Energy Price Forecasts\n\n";
877
+ text += "```json\n" + JSON.stringify(response.data, null, 2) + "\n```\n";
878
+ text +=
879
+ "\n_Source: EIA STEO | Data from [OilPriceAPI](https://oilpriceapi.com)_";
880
+ return textResult(text);
881
+ });
882
+ // =========================================================================
883
+ // RESOURCES — subscribable price snapshots + dynamic template
884
+ // =========================================================================
728
885
  server.resource("price-brent", "price://brent", {
729
886
  description: "Current Brent Crude oil price (global benchmark)",
730
887
  mimeType: "application/json",
@@ -785,14 +942,31 @@ server.resource("market-overview", "price://all", {
785
942
  ],
786
943
  };
787
944
  });
788
- // Register Prompts — pre-built analyst templates
945
+ server.resource("price-diesel", "price://diesel", {
946
+ description: "Current US national average diesel price",
947
+ mimeType: "application/json",
948
+ }, async () => {
949
+ const response = await makeApiRequest("/v1/prices/latest?by_code=DIESEL_USD");
950
+ return {
951
+ contents: [
952
+ {
953
+ uri: "price://diesel",
954
+ mimeType: "application/json",
955
+ text: JSON.stringify(response?.data ?? { error: "unavailable" }, null, 2),
956
+ },
957
+ ],
958
+ };
959
+ });
960
+ // =========================================================================
961
+ // PROMPTS — pre-built analyst templates
962
+ // =========================================================================
789
963
  server.prompt("daily-briefing", "Energy market daily briefing with key prices, changes, and notable movements", {}, () => ({
790
964
  messages: [
791
965
  {
792
966
  role: "user",
793
967
  content: {
794
968
  type: "text",
795
- text: "Give me today's energy market briefing. Get all commodity prices and provide:\n1. Key price levels for Brent, WTI, and Natural Gas\n2. Biggest movers (largest 24h % changes)\n3. Notable spreads (Brent-WTI, US gas vs EU gas)\n4. Brief market context\nFormat as a concise analyst briefing.",
969
+ text: "Give me today's energy market briefing. Use opa_market_overview to get all commodity prices, then provide:\n1. Key price levels for Brent, WTI, and Natural Gas\n2. Biggest movers (largest 24h % changes)\n3. Notable spreads (Brent-WTI, US gas vs EU gas)\n4. Brief market context\nFormat as a concise analyst briefing.",
796
970
  },
797
971
  },
798
972
  ],
@@ -803,7 +977,7 @@ server.prompt("brent-wti-spread", "Analyze the Brent-WTI crude oil spread", {},
803
977
  role: "user",
804
978
  content: {
805
979
  type: "text",
806
- text: "Compare Brent and WTI crude oil prices. Calculate the spread and explain what it means for the market. Is the spread widening or narrowing based on the 24h changes?",
980
+ text: "Use opa_compare_prices with ['brent', 'wti'] to compare Brent and WTI crude oil prices. Calculate the spread and explain what it means for the market. Is the spread widening or narrowing based on the 24h changes?",
807
981
  },
808
982
  },
809
983
  ],
@@ -814,7 +988,7 @@ server.prompt("gas-market-analysis", "Compare US vs European natural gas markets
814
988
  role: "user",
815
989
  content: {
816
990
  type: "text",
817
- text: "Get prices for US Natural Gas (Henry Hub), UK Natural Gas (NBP), and European Gas (TTF). Compare the three markets:\n1. Current price levels in their native currencies\n2. 24h changes\n3. Which market is moving most?\n4. What does the transatlantic gas price gap suggest about supply/demand dynamics?",
991
+ text: "Use opa_compare_prices with ['natural gas', 'uk gas', 'european gas'] to compare the three gas markets:\n1. Current price levels in their native currencies\n2. 24h changes\n3. Which market is moving most?\n4. What does the transatlantic gas price gap suggest about supply/demand dynamics?",
818
992
  },
819
993
  },
820
994
  ],
@@ -829,7 +1003,29 @@ server.prompt("commodity-report", "Detailed report on a specific commodity", {
829
1003
  role: "user",
830
1004
  content: {
831
1005
  type: "text",
832
- text: `Get the current price for ${commodity} and provide a detailed report:\n1. Current price and currency\n2. 24h price change (absolute and percentage)\n3. Compare with related commodities in the same category\n4. Key factors that typically affect this commodity's price\n5. Who are the main consumers and producers?`,
1006
+ text: `Use opa_get_price for ${commodity} and opa_get_history for its past month, then provide a detailed report:\n1. Current price and currency\n2. 24h price change (absolute and percentage)\n3. Monthly trend (high, low, average)\n4. Key factors that typically affect this commodity's price\n5. Who are the main consumers and producers?`,
1007
+ },
1008
+ },
1009
+ ],
1010
+ }));
1011
+ server.prompt("diesel-cost-analysis", "Compare diesel prices across US states for fleet cost planning", {}, () => ({
1012
+ messages: [
1013
+ {
1014
+ role: "user",
1015
+ content: {
1016
+ type: "text",
1017
+ text: "Use opa_get_diesel_by_state to get diesel prices for the top 5 trucking corridor states (TX, CA, FL, PA, IL). Compare prices and identify the cheapest and most expensive states. Calculate the cost difference for a 200-gallon fill-up between the cheapest and most expensive state.",
1018
+ },
1019
+ },
1020
+ ],
1021
+ }));
1022
+ server.prompt("supply-analysis", "Analyze oil supply fundamentals using production, rig counts, and storage data", {}, () => ({
1023
+ messages: [
1024
+ {
1025
+ role: "user",
1026
+ content: {
1027
+ type: "text",
1028
+ text: "Use opa_get_opec_production, opa_get_rig_counts, and opa_get_storage to analyze current oil supply fundamentals:\n1. OPEC production levels and recent changes\n2. US rig count trends\n3. Cushing and SPR inventory levels\n4. Overall supply outlook — bullish or bearish for prices?",
833
1029
  },
834
1030
  },
835
1031
  ],
@@ -840,13 +1036,12 @@ export function createSandboxServer() {
840
1036
  }
841
1037
  // Main entry point
842
1038
  async function main() {
843
- // Check for API key
844
1039
  if (!API_KEY) {
845
- console.error("Warning: OILPRICEAPI_KEY not set. Some features may be limited.");
1040
+ console.error("Warning: OILPRICEAPI_KEY not set. Get a free key at https://oilpriceapi.com/signup");
846
1041
  }
847
1042
  const transport = new StdioServerTransport();
848
1043
  await server.connect(transport);
849
- console.error("OilPriceAPI MCP Server running on stdio");
1044
+ console.error("OilPriceAPI MCP Server v2.0.0 running on stdio");
850
1045
  }
851
1046
  main().catch((error) => {
852
1047
  console.error("Fatal error:", error);