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/README.md +65 -97
- package/build/__tests__/index.test.js +34 -19
- package/build/__tests__/index.test.js.map +1 -1
- package/build/index.d.ts +11 -6
- package/build/index.d.ts.map +1 -1
- package/build/index.js +429 -234
- package/build/index.js.map +1 -1
- package/package.json +4 -2
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
|
-
*
|
|
6
|
-
*
|
|
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/
|
|
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
|
-
//
|
|
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: "
|
|
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
|
|
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
|
-
//
|
|
180
|
-
return
|
|
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.
|
|
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
|
-
*
|
|
385
|
+
* Resolve a US state name or abbreviation to a 2-letter code.
|
|
261
386
|
*/
|
|
262
|
-
|
|
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
|
|
268
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
416
|
-
|
|
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
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
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
|
-
|
|
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
|
-
|
|
455
|
-
|
|
456
|
-
|
|
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`
|
|
460
|
-
sections.push("- `WTI_USD`
|
|
461
|
-
sections.push("- `URALS_CRUDE_USD`
|
|
462
|
-
sections.push("- `DUBAI_CRUDE_USD`
|
|
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`
|
|
466
|
-
sections.push("- `NATURAL_GAS_GBP`
|
|
467
|
-
sections.push("- `DUTCH_TTF_EUR`
|
|
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`
|
|
471
|
-
sections.push("- `NEWCASTLE_COAL_USD`
|
|
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`
|
|
475
|
-
sections.push("- `GASOLINE_USD`
|
|
476
|
-
sections.push("- `GASOLINE_RBOB_USD`
|
|
477
|
-
sections.push("- `JET_FUEL_USD`
|
|
478
|
-
sections.push("- `HEATING_OIL_USD`
|
|
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`
|
|
482
|
-
sections.push("- `
|
|
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`
|
|
494
|
-
sections.push("- `EUR_USD`
|
|
495
|
-
sections.push("- `GBP_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("
|
|
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
|
-
|
|
509
|
-
|
|
510
|
-
|
|
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
|
|
518
|
-
|
|
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
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
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
|
|
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}
|
|
549
|
-
`- **Latest**: ${
|
|
550
|
-
`- **High**: ${
|
|
551
|
-
`- **Low**: ${
|
|
552
|
-
`- **Average**: ${
|
|
553
|
-
`- **Change**: ${change >= 0 ? "+" : ""}${
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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: "
|
|
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: "
|
|
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: `
|
|
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.
|
|
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);
|