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/README.md +74 -77
- package/build/__tests__/index.test.d.ts +2 -0
- package/build/__tests__/index.test.d.ts.map +1 -0
- package/build/__tests__/index.test.js +362 -0
- package/build/__tests__/index.test.js.map +1 -0
- package/build/index.d.ts +42 -3
- package/build/index.d.ts.map +1 -1
- package/build/index.js +633 -149
- 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,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/
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
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: "
|
|
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
|
|
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
|
-
//
|
|
150
|
-
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) };
|
|
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
|
-
|
|
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
|
-
|
|
190
|
-
|
|
191
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
*
|
|
385
|
+
* Resolve a US state name or abbreviation to a 2-letter code.
|
|
207
386
|
*/
|
|
208
|
-
|
|
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
|
|
214
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
362
|
-
|
|
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
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
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
|
-
|
|
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
|
-
|
|
401
|
-
|
|
402
|
-
|
|
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`
|
|
406
|
-
sections.push("- `WTI_USD`
|
|
407
|
-
sections.push("- `URALS_CRUDE_USD`
|
|
408
|
-
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)");
|
|
409
596
|
sections.push("");
|
|
410
597
|
sections.push("## Natural Gas");
|
|
411
|
-
sections.push("- `NATURAL_GAS_USD`
|
|
412
|
-
sections.push("- `NATURAL_GAS_GBP`
|
|
413
|
-
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)");
|
|
414
601
|
sections.push("");
|
|
415
602
|
sections.push("## Coal");
|
|
416
|
-
sections.push("- `COAL_USD`
|
|
417
|
-
sections.push("- `NEWCASTLE_COAL_USD`
|
|
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`
|
|
421
|
-
sections.push("- `GASOLINE_USD`
|
|
422
|
-
sections.push("- `GASOLINE_RBOB_USD`
|
|
423
|
-
sections.push("- `JET_FUEL_USD`
|
|
424
|
-
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");
|
|
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("- `
|
|
428
|
-
sections.push("- `
|
|
429
|
-
sections.push("- `
|
|
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("
|
|
433
|
-
return
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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: "
|
|
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: "
|
|
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: `
|
|
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.
|
|
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);
|