oilpriceapi-mcp 1.1.1 → 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,12 +12,14 @@ 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
- const USER_AGENT = "oilpriceapi-mcp/1.0.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
20
- const COMMODITY_ALIASES = {
21
+ // ---------------------------------------------------------------------------
22
+ export const COMMODITY_ALIASES = {
21
23
  // Crude Oil
22
24
  brent: "BRENT_CRUDE_USD",
23
25
  "brent oil": "BRENT_CRUDE_USD",
@@ -68,8 +70,20 @@ 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",
75
+ "gold am fix": "GOLD_AM_USD",
76
+ "lbma gold am": "GOLD_AM_USD",
77
+ "gold am fix gbp": "GOLD_AM_GBP",
78
+ "gold am fix eur": "GOLD_AM_EUR",
79
+ "gold pm fix": "GOLD_PM_USD",
80
+ "lbma gold pm": "GOLD_PM_USD",
81
+ "gold pm fix gbp": "GOLD_PM_GBP",
82
+ "gold pm fix eur": "GOLD_PM_EUR",
83
+ "silver fix": "SILVER_FIX_USD",
84
+ "lbma silver": "SILVER_FIX_USD",
85
+ "silver fix gbp": "SILVER_FIX_GBP",
86
+ "silver fix eur": "SILVER_FIX_EUR",
73
87
  carbon: "EU_CARBON_EUR",
74
88
  "eu carbon": "EU_CARBON_EUR",
75
89
  "carbon credits": "EU_CARBON_EUR",
@@ -79,8 +93,10 @@ const COMMODITY_ALIASES = {
79
93
  "gbp usd": "GBP_USD",
80
94
  sterling: "GBP_USD",
81
95
  };
96
+ // ---------------------------------------------------------------------------
82
97
  // Commodity metadata for formatting
83
- const COMMODITY_INFO = {
98
+ // ---------------------------------------------------------------------------
99
+ export const COMMODITY_INFO = {
84
100
  BRENT_CRUDE_USD: { name: "Brent Crude Oil", unit: "barrel" },
85
101
  WTI_USD: { name: "WTI Crude Oil", unit: "barrel" },
86
102
  URALS_CRUDE_USD: { name: "Urals Crude Oil", unit: "barrel" },
@@ -96,12 +112,21 @@ const COMMODITY_INFO = {
96
112
  JET_FUEL_USD: { name: "Jet Fuel", unit: "gallon" },
97
113
  HEATING_OIL_USD: { name: "Heating Oil", unit: "gallon" },
98
114
  GOLD_USD: { name: "Gold", unit: "troy oz" },
115
+ GOLD_AM_USD: { name: "LBMA Gold AM Fix", unit: "troy oz" },
116
+ GOLD_AM_GBP: { name: "LBMA Gold AM Fix (GBP)", unit: "troy oz" },
117
+ GOLD_AM_EUR: { name: "LBMA Gold AM Fix (EUR)", unit: "troy oz" },
118
+ GOLD_PM_USD: { name: "LBMA Gold PM Fix", unit: "troy oz" },
119
+ GOLD_PM_GBP: { name: "LBMA Gold PM Fix (GBP)", unit: "troy oz" },
120
+ GOLD_PM_EUR: { name: "LBMA Gold PM Fix (EUR)", unit: "troy oz" },
121
+ SILVER_FIX_USD: { name: "LBMA Silver Fix", unit: "troy oz" },
122
+ SILVER_FIX_GBP: { name: "LBMA Silver Fix (GBP)", unit: "troy oz" },
123
+ SILVER_FIX_EUR: { name: "LBMA Silver Fix (EUR)", unit: "troy oz" },
99
124
  EU_CARBON_EUR: { name: "EU Carbon Allowances", unit: "metric ton CO2" },
100
125
  EUR_USD: { name: "Euro to USD", unit: "rate" },
101
126
  GBP_USD: { name: "British Pound to USD", unit: "rate" },
102
127
  };
103
- // Available commodity codes
104
- const COMMODITY_CODES = [
128
+ // Available commodity codes (used for input validation)
129
+ export const COMMODITY_CODES = [
105
130
  "BRENT_CRUDE_USD",
106
131
  "WTI_USD",
107
132
  "URALS_CRUDE_USD",
@@ -117,42 +142,175 @@ const COMMODITY_CODES = [
117
142
  "JET_FUEL_USD",
118
143
  "HEATING_OIL_USD",
119
144
  "GOLD_USD",
145
+ "GOLD_AM_USD",
146
+ "GOLD_AM_GBP",
147
+ "GOLD_AM_EUR",
148
+ "GOLD_PM_USD",
149
+ "GOLD_PM_GBP",
150
+ "GOLD_PM_EUR",
151
+ "SILVER_FIX_USD",
152
+ "SILVER_FIX_GBP",
153
+ "SILVER_FIX_EUR",
120
154
  "EU_CARBON_EUR",
121
155
  "EUR_USD",
122
156
  "GBP_USD",
123
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
+ // ---------------------------------------------------------------------------
124
213
  // Create server instance
214
+ // ---------------------------------------------------------------------------
125
215
  const server = new McpServer({
126
216
  name: "oilpriceapi",
127
- version: "1.0.0",
217
+ version: "2.0.0",
128
218
  });
219
+ // ---------------------------------------------------------------------------
220
+ // Helpers
221
+ // ---------------------------------------------------------------------------
129
222
  /**
130
- * 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.
131
225
  */
132
- function resolveCommodityCode(input) {
226
+ export function resolveCommodityCode(input) {
133
227
  const normalized = input.toLowerCase().trim();
134
228
  // Check if it's already a valid code
135
229
  if (COMMODITY_CODES.includes(normalized.toUpperCase())) {
136
230
  return normalized.toUpperCase();
137
231
  }
138
- // Try alias mapping
232
+ // Try exact alias mapping
139
233
  const mapped = COMMODITY_ALIASES[normalized];
140
234
  if (mapped) {
141
235
  return mapped;
142
236
  }
143
- // Fuzzy match - check if input contains key words
237
+ // Fuzzy match check if input contains key words
144
238
  for (const [alias, code] of Object.entries(COMMODITY_ALIASES)) {
145
239
  if (normalized.includes(alias) || alias.includes(normalized)) {
146
240
  return code;
147
241
  }
148
242
  }
149
- // Default to Brent if no match
150
- 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) };
151
309
  }
152
310
  /**
153
311
  * Format a price for display
154
312
  */
155
- function formatPrice(data) {
313
+ export function formatPrice(data) {
156
314
  const info = COMMODITY_INFO[data.code] || { name: data.code, unit: "unit" };
157
315
  const currencySymbol = data.currency === "EUR"
158
316
  ? "€"
@@ -161,8 +319,10 @@ function formatPrice(data) {
161
319
  : "$";
162
320
  let result = `**${info.name}**: ${currencySymbol}${data.price.toFixed(2)}/${info.unit}`;
163
321
  if (data.change_24h !== undefined && data.change_24h_percent !== undefined) {
164
- const sign = data.change_24h >= 0 ? "+" : "";
165
- result += `\n- 24h Change: ${sign}${currencySymbol}${data.change_24h.toFixed(2)} (${sign}${data.change_24h_percent.toFixed(2)}%)`;
322
+ const sign = data.change_24h >= 0 ? "+" : "-";
323
+ const absChange = Math.abs(data.change_24h).toFixed(2);
324
+ const absPct = Math.abs(data.change_24h_percent).toFixed(2);
325
+ result += `\n- 24h Change: ${sign}${currencySymbol}${absChange} (${sign}${absPct}%)`;
166
326
  }
167
327
  const timestamp = data.updated_at || data.created_at;
168
328
  if (timestamp) {
@@ -176,9 +336,9 @@ function formatPrice(data) {
176
336
  return result;
177
337
  }
178
338
  /**
179
- * Make API request to OilPriceAPI
339
+ * Make API request to OilPriceAPI with retry and exponential backoff
180
340
  */
181
- async function makeApiRequest(endpoint) {
341
+ export async function makeApiRequest(endpoint, fetchFn = fetch) {
182
342
  const headers = {
183
343
  "User-Agent": USER_AGENT,
184
344
  Accept: "application/json",
@@ -186,74 +346,86 @@ async function makeApiRequest(endpoint) {
186
346
  if (API_KEY) {
187
347
  headers["Authorization"] = `Bearer ${API_KEY}`;
188
348
  }
189
- try {
190
- const response = await fetch(`${API_BASE}${endpoint}`, { headers });
191
- if (!response.ok) {
349
+ const maxRetries = 3;
350
+ for (let attempt = 0; attempt <= maxRetries; attempt++) {
351
+ try {
352
+ const response = await fetchFn(`${API_BASE}${endpoint}`, { headers });
353
+ if (response.ok) {
354
+ return (await response.json());
355
+ }
192
356
  if (response.status === 401) {
193
- 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");
358
+ return null;
359
+ }
360
+ // Retry on 429 and 5xx
361
+ if ((response.status === 429 || response.status >= 500) &&
362
+ attempt < maxRetries) {
363
+ const retryAfter = response.headers.get("Retry-After");
364
+ const delay = retryAfter
365
+ ? Math.min(parseInt(retryAfter, 10), 60) * 1000
366
+ : Math.pow(2, attempt) * 1000;
367
+ await new Promise((resolve) => setTimeout(resolve, delay));
368
+ continue;
194
369
  }
195
- throw new Error(`HTTP ${response.status}: ${response.statusText}`);
370
+ console.error(`HTTP ${response.status}: ${response.statusText} for ${endpoint}`);
371
+ return null;
372
+ }
373
+ catch (error) {
374
+ if (attempt === maxRetries) {
375
+ console.error(`API request failed after ${maxRetries + 1} attempts: ${endpoint}`, error);
376
+ return null;
377
+ }
378
+ const delay = Math.pow(2, attempt) * 1000;
379
+ await new Promise((resolve) => setTimeout(resolve, delay));
196
380
  }
197
- return (await response.json());
198
- }
199
- catch (error) {
200
- console.error(`API request failed: ${endpoint}`, error);
201
- return null;
202
381
  }
382
+ return null;
203
383
  }
204
- // Register Tools
205
384
  /**
206
- * Get current price of a specific commodity
385
+ * Resolve a US state name or abbreviation to a 2-letter code.
207
386
  */
208
- 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.", {
209
405
  commodity: z
210
406
  .string()
211
407
  .describe("Commodity name or code (e.g., 'brent oil', 'natural gas', 'WTI_USD', 'diesel')"),
212
408
  }, async ({ commodity }) => {
213
- const code = resolveCommodityCode(commodity);
214
- 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}`);
215
413
  if (!response || response.status !== "success") {
216
- return {
217
- content: [
218
- {
219
- type: "text",
220
- text: `Failed to retrieve price for ${commodity}. Please try again or check if the commodity is supported.`,
221
- },
222
- ],
223
- };
224
- }
225
- const formatted = formatPrice(response.data);
226
- return {
227
- content: [
228
- {
229
- type: "text",
230
- text: `${formatted}\n\n_Data from [OilPriceAPI](https://oilpriceapi.com)_`,
231
- },
232
- ],
233
- };
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)_`);
234
417
  });
235
- /**
236
- * Get all commodity prices (market overview)
237
- */
238
- 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.", {
239
419
  category: z
240
- .enum(["all", "oil", "gas", "coal", "refined"])
420
+ .enum(["all", "oil", "gas", "coal", "refined", "metals", "forex"])
241
421
  .optional()
242
- .describe("Filter by commodity category (default: all)"),
422
+ .describe("Filter by commodity category (default: all). Options: oil, gas, coal, refined, metals, forex."),
243
423
  }, async ({ category = "all" }) => {
244
424
  const response = await makeApiRequest("/v1/prices/all");
245
425
  if (!response || response.status !== "success") {
246
- return {
247
- content: [
248
- {
249
- type: "text",
250
- text: "Failed to retrieve market data. Please try again.",
251
- },
252
- ],
253
- };
426
+ return errorResult("Could not retrieve market data. The API may be temporarily unavailable — try again in a moment.");
254
427
  }
255
428
  const prices = response.data.data.prices;
256
- // Category filters
257
429
  const categoryFilters = {
258
430
  oil: ["BRENT_CRUDE_USD", "WTI_USD", "URALS_CRUDE_USD", "DUBAI_CRUDE_USD"],
259
431
  gas: ["NATURAL_GAS_USD", "NATURAL_GAS_GBP", "DUTCH_TTF_EUR"],
@@ -265,6 +437,8 @@ server.tool("get_market_overview", "Get current prices for all tracked commoditi
265
437
  "JET_FUEL_USD",
266
438
  "HEATING_OIL_USD",
267
439
  ],
440
+ metals: ["GOLD_USD", "GOLD_AM_USD", "GOLD_PM_USD", "SILVER_FIX_USD"],
441
+ forex: ["EUR_USD", "GBP_USD"],
268
442
  };
269
443
  let filteredCodes;
270
444
  if (category === "all") {
@@ -274,12 +448,13 @@ server.tool("get_market_overview", "Get current prices for all tracked commoditi
274
448
  filteredCodes = categoryFilters[category] || [];
275
449
  }
276
450
  const sections = ["# Energy Market Overview\n"];
277
- // Group by category
278
451
  const groupedPrices = {
279
452
  "Crude Oil": [],
280
453
  "Natural Gas": [],
281
454
  Coal: [],
282
455
  "Refined Products": [],
456
+ "Precious Metals": [],
457
+ Forex: [],
283
458
  Other: [],
284
459
  };
285
460
  for (const code of filteredCodes) {
@@ -305,6 +480,12 @@ server.tool("get_market_overview", "Get current prices for all tracked commoditi
305
480
  ].includes(code)) {
306
481
  groupedPrices["Refined Products"].push(data);
307
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
+ }
308
489
  else {
309
490
  groupedPrices["Other"].push(data);
310
491
  }
@@ -332,114 +513,375 @@ server.tool("get_market_overview", "Get current prices for all tracked commoditi
332
513
  }
333
514
  sections.push("");
334
515
  }
335
- sections.push(`_Updated: ${new Date(response.data.data.timestamp).toLocaleString("en-US", {
336
- dateStyle: "medium",
337
- timeStyle: "short",
338
- timeZone: "UTC",
339
- })} UTC | Data from [OilPriceAPI](https://oilpriceapi.com)_`);
340
- return {
341
- content: [
342
- {
343
- type: "text",
344
- text: sections.join("\n"),
345
- },
346
- ],
347
- };
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"));
348
518
  });
349
- /**
350
- * Compare prices between commodities
351
- */
352
- 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.", {
353
520
  commodities: z
354
521
  .array(z.string())
355
522
  .min(2)
356
523
  .max(5)
357
- .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'])"),
358
525
  }, async ({ commodities }) => {
359
- const codes = commodities.map(resolveCommodityCode);
360
526
  const results = [];
361
- for (const code of codes) {
362
- 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}`);
363
535
  if (response?.status === "success") {
364
536
  results.push(response.data);
365
537
  }
366
538
  }
367
539
  if (results.length < 2) {
368
- return {
369
- content: [
370
- {
371
- type: "text",
372
- text: "Could not retrieve enough price data for comparison. Please check commodity names.",
373
- },
374
- ],
375
- };
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);
376
545
  }
377
546
  const sections = ["# Price Comparison\n"];
378
547
  for (const data of results) {
379
548
  sections.push(formatPrice(data));
380
549
  sections.push("");
381
550
  }
382
- // Calculate spread if comparing similar commodities
383
551
  if (results.length === 2 && results[0].currency === results[1].currency) {
384
552
  const spread = Math.abs(results[0].price - results[1].price);
385
553
  const info0 = COMMODITY_INFO[results[0].code]?.name || results[0].code;
386
554
  const info1 = COMMODITY_INFO[results[1].code]?.name || results[1].code;
387
- 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})`);
388
561
  }
389
562
  sections.push(`\n_Data from [OilPriceAPI](https://oilpriceapi.com)_`);
390
- return {
391
- content: [
392
- {
393
- type: "text",
394
- text: sections.join("\n"),
395
- },
396
- ],
397
- };
563
+ return textResult(sections.join("\n"));
398
564
  });
399
- /**
400
- * List available commodities
401
- */
402
- 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
403
590
  const sections = ["# Available Commodities\n"];
404
591
  sections.push("## Crude Oil");
405
- sections.push("- `BRENT_CRUDE_USD` - Brent Crude (global benchmark)");
406
- sections.push("- `WTI_USD` - West Texas Intermediate (US benchmark)");
407
- sections.push("- `URALS_CRUDE_USD` - Urals Crude (Russian)");
408
- 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)");
409
596
  sections.push("");
410
597
  sections.push("## Natural Gas");
411
- sections.push("- `NATURAL_GAS_USD` - US Henry Hub ($/MMBtu)");
412
- sections.push("- `NATURAL_GAS_GBP` - UK NBP (pence/therm)");
413
- 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)");
414
601
  sections.push("");
415
602
  sections.push("## Coal");
416
- sections.push("- `COAL_USD` - Thermal Coal");
417
- sections.push("- `NEWCASTLE_COAL_USD` - Newcastle (Asia-Pacific)");
603
+ sections.push("- `COAL_USD` Thermal Coal");
604
+ sections.push("- `NEWCASTLE_COAL_USD` Newcastle (Asia-Pacific)");
418
605
  sections.push("");
419
606
  sections.push("## Refined Products");
420
- sections.push("- `DIESEL_USD` - Diesel");
421
- sections.push("- `GASOLINE_USD` - Gasoline");
422
- sections.push("- `GASOLINE_RBOB_USD` - RBOB Gasoline");
423
- sections.push("- `JET_FUEL_USD` - Jet Fuel");
424
- 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");
612
+ sections.push("");
613
+ sections.push("## Precious Metals");
614
+ sections.push("- `GOLD_USD` — Gold");
615
+ sections.push("- `SILVER_FIX_USD` — LBMA Silver Fix");
425
616
  sections.push("");
426
617
  sections.push("## Other");
427
- sections.push("- `GOLD_USD` - Gold");
428
- sections.push("- `EU_CARBON_EUR` - EU Carbon Allowances");
429
- sections.push("- `EUR_USD` - Euro to USD");
430
- 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");
431
621
  sections.push("");
432
- sections.push("_You can use natural language like 'brent oil' or 'natural gas' - I'll translate it to the right code._");
433
- return {
434
- content: [
435
- {
436
- type: "text",
437
- text: sections.join("\n"),
438
- },
439
- ],
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"));
624
+ });
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')"),
629
+ period: z
630
+ .enum(["day", "week", "month", "year"])
631
+ .default("month")
632
+ .describe("Time period: day, week, month, or year (default: month)"),
633
+ }, async ({ commodity, period }) => {
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}`);
638
+ if (!response ||
639
+ response.status !== "success" ||
640
+ !response.data.prices?.length) {
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",
440
646
  };
647
+ const currencyFromCode = resolved.code.endsWith("_EUR")
648
+ ? "EUR"
649
+ : resolved.code.endsWith("_GBP") || resolved.code.endsWith("_GBp")
650
+ ? "GBP"
651
+ : "USD";
652
+ const sym = currencyFromCode === "EUR" ? "€" : currencyFromCode === "GBP" ? "£" : "$";
653
+ const prices = response.data.prices;
654
+ const latest = prices[0];
655
+ const oldest = prices[prices.length - 1];
656
+ const high = Math.max(...prices.map((p) => p.price));
657
+ const low = Math.min(...prices.map((p) => p.price));
658
+ const avg = prices.reduce((sum, p) => sum + p.price, 0) / prices.length;
659
+ const change = latest.price - oldest.price;
660
+ const changePct = (change / oldest.price) * 100;
661
+ const sections = [
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)}%)`,
668
+ `- **Data Points**: ${prices.length}`,
669
+ `\n_Data from [OilPriceAPI](https://oilpriceapi.com)_`,
670
+ ];
671
+ return textResult(sections.join("\n"));
672
+ });
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.", {
674
+ contract: z
675
+ .enum(["BZ", "CL"])
676
+ .default("BZ")
677
+ .describe("Futures contract: BZ = Brent crude, CL = WTI crude (default: BZ)"),
678
+ }, async ({ contract }) => {
679
+ const response = await makeApiRequest(`/v1/futures/latest?contract=${contract}`);
680
+ if (!response ||
681
+ response.status !== "success" ||
682
+ !response.data.contracts?.length) {
683
+ return errorResult(`No futures data available for ${contract === "BZ" ? "Brent" : "WTI"} (${contract}). Futures data requires a paid plan.`);
684
+ }
685
+ const contractName = contract === "BZ" ? "Brent Crude" : "WTI Crude";
686
+ const front = response.data.contracts[0];
687
+ let text = `# ${contractName} Futures (${contract})\n\n`;
688
+ text += `**Front Month (${front.month})**: $${front.price.toFixed(2)}`;
689
+ if (front.change !== undefined) {
690
+ text += ` (${front.change >= 0 ? "+" : ""}$${front.change.toFixed(2)})`;
691
+ }
692
+ text += `\n\n_Data from [OilPriceAPI](https://oilpriceapi.com)_`;
693
+ return textResult(text);
694
+ });
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.", {
696
+ contract: z
697
+ .enum(["BZ", "CL"])
698
+ .default("BZ")
699
+ .describe("Futures contract: BZ = Brent crude, CL = WTI crude (default: BZ)"),
700
+ }, async ({ contract }) => {
701
+ const response = await makeApiRequest(`/v1/futures/curve?contract=${contract}`);
702
+ if (!response ||
703
+ response.status !== "success" ||
704
+ !response.data.contracts?.length) {
705
+ return errorResult(`No futures curve data available for ${contract === "BZ" ? "Brent" : "WTI"} (${contract}). Futures data requires a paid plan.`);
706
+ }
707
+ const contractName = contract === "BZ" ? "Brent Crude" : "WTI Crude";
708
+ const contracts = response.data.contracts;
709
+ let text = `# ${contractName} Futures Curve (${contract})\n\n`;
710
+ text += `| Month | Price | Change |\n|-------|-------|--------|\n`;
711
+ for (const c of contracts) {
712
+ const changeStr = c.change !== undefined
713
+ ? `${c.change >= 0 ? "+" : ""}$${c.change.toFixed(2)}`
714
+ : "N/A";
715
+ text += `| ${c.month} | $${c.price.toFixed(2)} | ${changeStr} |\n`;
716
+ }
717
+ const front = contracts[0].price;
718
+ const back = contracts[contracts.length - 1].price;
719
+ const structure = front > back ? "backwardation" : "contango";
720
+ text += `\n**Market Structure**: ${structure} (front $${front.toFixed(2)} vs back $${back.toFixed(2)})`;
721
+ text += `\n\n_Data from [OilPriceAPI](https://oilpriceapi.com)_`;
722
+ return textResult(text);
723
+ });
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.", {
725
+ port: z
726
+ .string()
727
+ .optional()
728
+ .describe("Filter by port name (e.g., 'SINGAPORE', 'ROTTERDAM', 'HOUSTON')"),
729
+ fuel_type: z
730
+ .string()
731
+ .optional()
732
+ .describe("Filter by fuel type: VLSFO, MGO, or IFO380"),
733
+ }, async ({ port, fuel_type }) => {
734
+ let endpoint = "/v1/marine-fuels/latest";
735
+ const params = [];
736
+ if (port)
737
+ params.push(`port=${encodeURIComponent(port)}`);
738
+ if (fuel_type)
739
+ params.push(`fuel_type=${encodeURIComponent(fuel_type)}`);
740
+ if (params.length)
741
+ endpoint += `?${params.join("&")}`;
742
+ const response = await makeApiRequest(endpoint);
743
+ if (!response ||
744
+ response.status !== "success" ||
745
+ !response.data.prices?.length) {
746
+ return errorResult("No marine fuel price data available. Marine fuel data requires a paid plan with bunker fuel coverage.");
747
+ }
748
+ const prices = response.data.prices;
749
+ let text = "# Marine Fuel Prices\n\n";
750
+ text += `| Port | Fuel Type | Price | Currency | Unit |\n`;
751
+ text += `|------|-----------|-------|----------|------|\n`;
752
+ for (const p of prices) {
753
+ text += `| ${p.port} | ${p.fuel_type} | ${p.price.toFixed(2)} | ${p.currency} | ${p.unit} |\n`;
754
+ }
755
+ text += `\n_${prices.length} prices | Data from [OilPriceAPI](https://oilpriceapi.com)_`;
756
+ return textResult(text);
757
+ });
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 () => {
759
+ const response = await makeApiRequest("/v1/rig-counts/latest");
760
+ if (!response || response.status !== "success") {
761
+ return errorResult("Rig count data not available. This may require a paid plan with energy intelligence access.");
762
+ }
763
+ const data = response.data;
764
+ let text = `# US Rig Count (Baker Hughes)\n\n`;
765
+ text += `- **Oil Rigs**: ${data.oil}\n`;
766
+ text += `- **Gas Rigs**: ${data.gas}\n`;
767
+ text += `- **Total**: ${data.total}\n`;
768
+ if (data.change_from_prior_week !== undefined) {
769
+ const sign = data.change_from_prior_week >= 0 ? "+" : "";
770
+ text += `- **Change from Prior Week**: ${sign}${data.change_from_prior_week}\n`;
771
+ }
772
+ text += `- **Date**: ${data.date}\n`;
773
+ text += `\n_Data from [OilPriceAPI](https://oilpriceapi.com)_`;
774
+ return textResult(text);
775
+ });
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 () => {
777
+ const response = await makeApiRequest("/v1/drilling/latest");
778
+ if (!response || response.status !== "success") {
779
+ return errorResult("Drilling intelligence data not available. This requires a paid plan with energy intelligence access.");
780
+ }
781
+ const data = response.data;
782
+ let text = `# Drilling Intelligence\n\n`;
783
+ text += `- **Total Wells**: ${data.total_wells.toLocaleString()}\n`;
784
+ text += `- **Active Rigs**: ${data.active_rigs.toLocaleString()}\n`;
785
+ if (data.permits_issued !== undefined)
786
+ text += `- **Permits Issued**: ${data.permits_issued.toLocaleString()}\n`;
787
+ if (data.completions !== undefined)
788
+ text += `- **Completions**: ${data.completions.toLocaleString()}\n`;
789
+ text += `- **Date**: ${data.date}\n`;
790
+ if (data.region_breakdown?.length) {
791
+ text += `\n## By Region\n`;
792
+ for (const r of data.region_breakdown) {
793
+ text += `- **${r.region}**: ${r.count}\n`;
794
+ }
795
+ }
796
+ text += `\n_Data from [OilPriceAPI](https://oilpriceapi.com)_`;
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);
441
830
  });
442
- // Register Resources subscribable price snapshots
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"));
860
+ });
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
+ // =========================================================================
443
885
  server.resource("price-brent", "price://brent", {
444
886
  description: "Current Brent Crude oil price (global benchmark)",
445
887
  mimeType: "application/json",
@@ -500,14 +942,31 @@ server.resource("market-overview", "price://all", {
500
942
  ],
501
943
  };
502
944
  });
503
- // 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
+ // =========================================================================
504
963
  server.prompt("daily-briefing", "Energy market daily briefing with key prices, changes, and notable movements", {}, () => ({
505
964
  messages: [
506
965
  {
507
966
  role: "user",
508
967
  content: {
509
968
  type: "text",
510
- 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.",
511
970
  },
512
971
  },
513
972
  ],
@@ -518,7 +977,7 @@ server.prompt("brent-wti-spread", "Analyze the Brent-WTI crude oil spread", {},
518
977
  role: "user",
519
978
  content: {
520
979
  type: "text",
521
- 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?",
522
981
  },
523
982
  },
524
983
  ],
@@ -529,7 +988,7 @@ server.prompt("gas-market-analysis", "Compare US vs European natural gas markets
529
988
  role: "user",
530
989
  content: {
531
990
  type: "text",
532
- 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?",
533
992
  },
534
993
  },
535
994
  ],
@@ -544,20 +1003,45 @@ server.prompt("commodity-report", "Detailed report on a specific commodity", {
544
1003
  role: "user",
545
1004
  content: {
546
1005
  type: "text",
547
- 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?`,
548
1007
  },
549
1008
  },
550
1009
  ],
551
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?",
1029
+ },
1030
+ },
1031
+ ],
1032
+ }));
1033
+ // Smithery sandbox export for server scanning
1034
+ export function createSandboxServer() {
1035
+ return server;
1036
+ }
552
1037
  // Main entry point
553
1038
  async function main() {
554
- // Check for API key
555
1039
  if (!API_KEY) {
556
- 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");
557
1041
  }
558
1042
  const transport = new StdioServerTransport();
559
1043
  await server.connect(transport);
560
- console.error("OilPriceAPI MCP Server running on stdio");
1044
+ console.error("OilPriceAPI MCP Server v2.0.0 running on stdio");
561
1045
  }
562
1046
  main().catch((error) => {
563
1047
  console.error("Fatal error:", error);