oilpriceapi-mcp 1.2.0 → 2.2.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 +83 -97
- package/build/__tests__/index.test.js +34 -19
- package/build/__tests__/index.test.js.map +1 -1
- package/build/index.d.ts +37 -6
- package/build/index.d.ts.map +1 -1
- package/build/index.js +799 -242
- package/build/index.js.map +1 -1
- package/package.json +4 -2
package/build/index.js
CHANGED
|
@@ -1,9 +1,18 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
* OilPriceAPI MCP Server
|
|
3
|
+
* OilPriceAPI MCP Server v2.2.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
|
+
*
|
|
8
|
+
* 17 read-only tools (opa_ prefixed): prices, history, futures, marine fuels,
|
|
9
|
+
* rig counts, drilling, diesel-by-state, storage, OPEC production, forecasts,
|
|
10
|
+
* EIA oil inventories, well permits, refining spreads.
|
|
11
|
+
*
|
|
12
|
+
* 4 authenticated price-alert tools (opa_*_price_alert / opa_get_alert_triggers):
|
|
13
|
+
* create/list/delete persistent price alerts tied to the user's account and read
|
|
14
|
+
* recent trigger activity. These wrap the existing /v1/alerts engine and REQUIRE
|
|
15
|
+
* an API key (OILPRICEAPI_KEY).
|
|
7
16
|
*
|
|
8
17
|
* @see https://oilpriceapi.com
|
|
9
18
|
* @see https://modelcontextprotocol.io
|
|
@@ -12,11 +21,13 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
|
12
21
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
13
22
|
import { z } from "zod";
|
|
14
23
|
// API Configuration
|
|
15
|
-
const API_BASE = "https://api.oilpriceapi.com";
|
|
16
|
-
export const USER_AGENT = "oilpriceapi-mcp/
|
|
24
|
+
const API_BASE = process.env.OILPRICEAPI_BASE_URL || "https://api.oilpriceapi.com";
|
|
25
|
+
export const USER_AGENT = "oilpriceapi-mcp/2.2.0";
|
|
17
26
|
// Get API key from environment
|
|
18
27
|
const API_KEY = process.env.OILPRICEAPI_KEY || process.env.OIL_PRICE_API_KEY;
|
|
28
|
+
// ---------------------------------------------------------------------------
|
|
19
29
|
// Natural language to commodity code mapping
|
|
30
|
+
// ---------------------------------------------------------------------------
|
|
20
31
|
export const COMMODITY_ALIASES = {
|
|
21
32
|
// Crude Oil
|
|
22
33
|
brent: "BRENT_CRUDE_USD",
|
|
@@ -68,7 +79,7 @@ export const COMMODITY_ALIASES = {
|
|
|
68
79
|
"aviation fuel": "JET_FUEL_USD",
|
|
69
80
|
kerosene: "JET_FUEL_USD",
|
|
70
81
|
"heating oil": "HEATING_OIL_USD",
|
|
71
|
-
//
|
|
82
|
+
// Precious Metals
|
|
72
83
|
gold: "GOLD_USD",
|
|
73
84
|
"gold am fix": "GOLD_AM_USD",
|
|
74
85
|
"lbma gold am": "GOLD_AM_USD",
|
|
@@ -91,7 +102,9 @@ export const COMMODITY_ALIASES = {
|
|
|
91
102
|
"gbp usd": "GBP_USD",
|
|
92
103
|
sterling: "GBP_USD",
|
|
93
104
|
};
|
|
105
|
+
// ---------------------------------------------------------------------------
|
|
94
106
|
// Commodity metadata for formatting
|
|
107
|
+
// ---------------------------------------------------------------------------
|
|
95
108
|
export const COMMODITY_INFO = {
|
|
96
109
|
BRENT_CRUDE_USD: { name: "Brent Crude Oil", unit: "barrel" },
|
|
97
110
|
WTI_USD: { name: "WTI Crude Oil", unit: "barrel" },
|
|
@@ -121,7 +134,7 @@ export const COMMODITY_INFO = {
|
|
|
121
134
|
EUR_USD: { name: "Euro to USD", unit: "rate" },
|
|
122
135
|
GBP_USD: { name: "British Pound to USD", unit: "rate" },
|
|
123
136
|
};
|
|
124
|
-
// Available commodity codes
|
|
137
|
+
// Available commodity codes (used for input validation)
|
|
125
138
|
export const COMMODITY_CODES = [
|
|
126
139
|
"BRENT_CRUDE_USD",
|
|
127
140
|
"WTI_USD",
|
|
@@ -151,13 +164,90 @@ export const COMMODITY_CODES = [
|
|
|
151
164
|
"EUR_USD",
|
|
152
165
|
"GBP_USD",
|
|
153
166
|
];
|
|
167
|
+
// US state abbreviation lookup for diesel tool
|
|
168
|
+
const US_STATES = {
|
|
169
|
+
alabama: "AL",
|
|
170
|
+
alaska: "AK",
|
|
171
|
+
arizona: "AZ",
|
|
172
|
+
arkansas: "AR",
|
|
173
|
+
california: "CA",
|
|
174
|
+
colorado: "CO",
|
|
175
|
+
connecticut: "CT",
|
|
176
|
+
delaware: "DE",
|
|
177
|
+
florida: "FL",
|
|
178
|
+
georgia: "GA",
|
|
179
|
+
hawaii: "HI",
|
|
180
|
+
idaho: "ID",
|
|
181
|
+
illinois: "IL",
|
|
182
|
+
indiana: "IN",
|
|
183
|
+
iowa: "IA",
|
|
184
|
+
kansas: "KS",
|
|
185
|
+
kentucky: "KY",
|
|
186
|
+
louisiana: "LA",
|
|
187
|
+
maine: "ME",
|
|
188
|
+
maryland: "MD",
|
|
189
|
+
massachusetts: "MA",
|
|
190
|
+
michigan: "MI",
|
|
191
|
+
minnesota: "MN",
|
|
192
|
+
mississippi: "MS",
|
|
193
|
+
missouri: "MO",
|
|
194
|
+
montana: "MT",
|
|
195
|
+
nebraska: "NE",
|
|
196
|
+
nevada: "NV",
|
|
197
|
+
"new hampshire": "NH",
|
|
198
|
+
"new jersey": "NJ",
|
|
199
|
+
"new mexico": "NM",
|
|
200
|
+
"new york": "NY",
|
|
201
|
+
"north carolina": "NC",
|
|
202
|
+
"north dakota": "ND",
|
|
203
|
+
ohio: "OH",
|
|
204
|
+
oklahoma: "OK",
|
|
205
|
+
oregon: "OR",
|
|
206
|
+
pennsylvania: "PA",
|
|
207
|
+
"rhode island": "RI",
|
|
208
|
+
"south carolina": "SC",
|
|
209
|
+
"south dakota": "SD",
|
|
210
|
+
tennessee: "TN",
|
|
211
|
+
texas: "TX",
|
|
212
|
+
utah: "UT",
|
|
213
|
+
vermont: "VT",
|
|
214
|
+
virginia: "VA",
|
|
215
|
+
washington: "WA",
|
|
216
|
+
"west virginia": "WV",
|
|
217
|
+
wisconsin: "WI",
|
|
218
|
+
wyoming: "WY",
|
|
219
|
+
"district of columbia": "DC",
|
|
220
|
+
};
|
|
221
|
+
// Supported futures contracts (used by opa_get_futures + opa_get_futures_curve)
|
|
222
|
+
export const FUTURES_CONTRACTS = [
|
|
223
|
+
"BZ",
|
|
224
|
+
"CL",
|
|
225
|
+
"ice-gasoil",
|
|
226
|
+
"ttf-gas",
|
|
227
|
+
"lng-jkm",
|
|
228
|
+
"eua-carbon",
|
|
229
|
+
];
|
|
230
|
+
export const FUTURES_CONTRACT_NAMES = {
|
|
231
|
+
BZ: "Brent Crude",
|
|
232
|
+
CL: "WTI Crude",
|
|
233
|
+
"ice-gasoil": "ICE Gasoil",
|
|
234
|
+
"ttf-gas": "European TTF Natural Gas",
|
|
235
|
+
"lng-jkm": "LNG JKM (Asia)",
|
|
236
|
+
"eua-carbon": "EU Carbon Allowance (EUA)",
|
|
237
|
+
};
|
|
238
|
+
// ---------------------------------------------------------------------------
|
|
154
239
|
// Create server instance
|
|
240
|
+
// ---------------------------------------------------------------------------
|
|
155
241
|
const server = new McpServer({
|
|
156
242
|
name: "oilpriceapi",
|
|
157
|
-
version: "
|
|
243
|
+
version: "2.2.0",
|
|
158
244
|
});
|
|
245
|
+
// ---------------------------------------------------------------------------
|
|
246
|
+
// Helpers
|
|
247
|
+
// ---------------------------------------------------------------------------
|
|
159
248
|
/**
|
|
160
|
-
* Resolve a natural language commodity name to its API code
|
|
249
|
+
* Resolve a natural language commodity name to its API code.
|
|
250
|
+
* Returns null if no match found — callers should return an actionable error.
|
|
161
251
|
*/
|
|
162
252
|
export function resolveCommodityCode(input) {
|
|
163
253
|
const normalized = input.toLowerCase().trim();
|
|
@@ -165,19 +255,83 @@ export function resolveCommodityCode(input) {
|
|
|
165
255
|
if (COMMODITY_CODES.includes(normalized.toUpperCase())) {
|
|
166
256
|
return normalized.toUpperCase();
|
|
167
257
|
}
|
|
168
|
-
// Try alias mapping
|
|
258
|
+
// Try exact alias mapping
|
|
169
259
|
const mapped = COMMODITY_ALIASES[normalized];
|
|
170
260
|
if (mapped) {
|
|
171
261
|
return mapped;
|
|
172
262
|
}
|
|
173
|
-
// Fuzzy match
|
|
263
|
+
// Fuzzy match — check if input contains key words
|
|
174
264
|
for (const [alias, code] of Object.entries(COMMODITY_ALIASES)) {
|
|
175
265
|
if (normalized.includes(alias) || alias.includes(normalized)) {
|
|
176
266
|
return code;
|
|
177
267
|
}
|
|
178
268
|
}
|
|
179
|
-
//
|
|
180
|
-
return
|
|
269
|
+
// No match found
|
|
270
|
+
return null;
|
|
271
|
+
}
|
|
272
|
+
/**
|
|
273
|
+
* Find the closest matching alias for an unrecognized input.
|
|
274
|
+
*/
|
|
275
|
+
function suggestCommodities(input) {
|
|
276
|
+
const normalized = input.toLowerCase().trim();
|
|
277
|
+
const suggestions = [];
|
|
278
|
+
for (const [alias, code] of Object.entries(COMMODITY_ALIASES)) {
|
|
279
|
+
let score = 0;
|
|
280
|
+
const words = normalized.split(/\s+/);
|
|
281
|
+
for (const word of words) {
|
|
282
|
+
if (alias.includes(word) || word.includes(alias)) {
|
|
283
|
+
score += word.length;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
if (score > 0) {
|
|
287
|
+
suggestions.push({ alias, code, score });
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
// Deduplicate by code and return top 3
|
|
291
|
+
const seen = new Set();
|
|
292
|
+
return suggestions
|
|
293
|
+
.sort((a, b) => b.score - a.score)
|
|
294
|
+
.filter((s) => {
|
|
295
|
+
if (seen.has(s.code))
|
|
296
|
+
return false;
|
|
297
|
+
seen.add(s.code);
|
|
298
|
+
return true;
|
|
299
|
+
})
|
|
300
|
+
.slice(0, 3)
|
|
301
|
+
.map((s) => `'${s.alias}' (${s.code})`);
|
|
302
|
+
}
|
|
303
|
+
/**
|
|
304
|
+
* Build an error tool result with isError flag so LLMs can distinguish
|
|
305
|
+
* errors from data.
|
|
306
|
+
*/
|
|
307
|
+
function errorResult(message) {
|
|
308
|
+
return {
|
|
309
|
+
content: [{ type: "text", text: message }],
|
|
310
|
+
isError: true,
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
/**
|
|
314
|
+
* Build a success tool result.
|
|
315
|
+
*/
|
|
316
|
+
function textResult(text) {
|
|
317
|
+
return {
|
|
318
|
+
content: [{ type: "text", text }],
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
/**
|
|
322
|
+
* Handle commodity resolution with helpful error on no match.
|
|
323
|
+
*/
|
|
324
|
+
function resolveOrError(input) {
|
|
325
|
+
const code = resolveCommodityCode(input);
|
|
326
|
+
if (code)
|
|
327
|
+
return { code };
|
|
328
|
+
const suggestions = suggestCommodities(input);
|
|
329
|
+
let msg = `Commodity '${input}' not recognized.`;
|
|
330
|
+
if (suggestions.length > 0) {
|
|
331
|
+
msg += ` Did you mean: ${suggestions.join(", ")}?`;
|
|
332
|
+
}
|
|
333
|
+
msg += " Use opa_list_commodities to see all available codes.";
|
|
334
|
+
return { error: errorResult(msg) };
|
|
181
335
|
}
|
|
182
336
|
/**
|
|
183
337
|
* Format a price for display
|
|
@@ -226,7 +380,7 @@ export async function makeApiRequest(endpoint, fetchFn = fetch) {
|
|
|
226
380
|
return (await response.json());
|
|
227
381
|
}
|
|
228
382
|
if (response.status === 401) {
|
|
229
|
-
console.error("Authentication failed.
|
|
383
|
+
console.error("Authentication failed. Set OILPRICEAPI_KEY environment variable. Get a free key at https://oilpriceapi.com/signup");
|
|
230
384
|
return null;
|
|
231
385
|
}
|
|
232
386
|
// Retry on 429 and 5xx
|
|
@@ -239,7 +393,6 @@ export async function makeApiRequest(endpoint, fetchFn = fetch) {
|
|
|
239
393
|
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
240
394
|
continue;
|
|
241
395
|
}
|
|
242
|
-
// Non-retryable HTTP error (403, 404, etc.) — return null immediately
|
|
243
396
|
console.error(`HTTP ${response.status}: ${response.statusText} for ${endpoint}`);
|
|
244
397
|
return null;
|
|
245
398
|
}
|
|
@@ -248,66 +401,142 @@ export async function makeApiRequest(endpoint, fetchFn = fetch) {
|
|
|
248
401
|
console.error(`API request failed after ${maxRetries + 1} attempts: ${endpoint}`, error);
|
|
249
402
|
return null;
|
|
250
403
|
}
|
|
251
|
-
// Network error - retry with backoff
|
|
252
404
|
const delay = Math.pow(2, attempt) * 1000;
|
|
253
405
|
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
254
406
|
}
|
|
255
407
|
}
|
|
256
408
|
return null;
|
|
257
409
|
}
|
|
258
|
-
// Register Tools
|
|
259
410
|
/**
|
|
260
|
-
*
|
|
411
|
+
* Make an authenticated request to a stateful endpoint. Requires API_KEY.
|
|
412
|
+
* Returns { ok, status, body }; callers decide how to render success/error.
|
|
413
|
+
* Returns status 0 on a network/transport failure.
|
|
414
|
+
*/
|
|
415
|
+
export async function makeAuthRequest(endpoint, options = {}, fetchFn = fetch) {
|
|
416
|
+
const { method = "GET", body } = options;
|
|
417
|
+
const headers = {
|
|
418
|
+
"User-Agent": USER_AGENT,
|
|
419
|
+
Accept: "application/json",
|
|
420
|
+
// The API accepts the customer API key as a bearer token.
|
|
421
|
+
Authorization: `Bearer ${API_KEY}`,
|
|
422
|
+
};
|
|
423
|
+
if (body !== undefined) {
|
|
424
|
+
headers["Content-Type"] = "application/json";
|
|
425
|
+
}
|
|
426
|
+
try {
|
|
427
|
+
const response = await fetchFn(`${API_BASE}${endpoint}`, {
|
|
428
|
+
method,
|
|
429
|
+
headers,
|
|
430
|
+
body: body !== undefined ? JSON.stringify(body) : undefined,
|
|
431
|
+
});
|
|
432
|
+
let parsed = null;
|
|
433
|
+
const text = await response.text();
|
|
434
|
+
if (text) {
|
|
435
|
+
try {
|
|
436
|
+
parsed = JSON.parse(text);
|
|
437
|
+
}
|
|
438
|
+
catch {
|
|
439
|
+
parsed = text;
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
return { ok: response.ok, status: response.status, body: parsed };
|
|
443
|
+
}
|
|
444
|
+
catch (error) {
|
|
445
|
+
console.error(`Authenticated request failed: ${method} ${endpoint}`, error);
|
|
446
|
+
return { ok: false, status: 0, body: null };
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
/**
|
|
450
|
+
* Standard guard for alert tools: returns an actionable error result when no
|
|
451
|
+
* API key is configured, otherwise null.
|
|
452
|
+
*/
|
|
453
|
+
function requireApiKey() {
|
|
454
|
+
if (!API_KEY) {
|
|
455
|
+
return errorResult("Price alerts require an OilPriceAPI key. These tools create and manage " +
|
|
456
|
+
"persistent alerts tied to your account, so the server must be configured " +
|
|
457
|
+
"with one. Set the OILPRICEAPI_KEY environment variable for this MCP " +
|
|
458
|
+
"server (get a free key at https://oilpriceapi.com/signup), then retry.");
|
|
459
|
+
}
|
|
460
|
+
return null;
|
|
461
|
+
}
|
|
462
|
+
/**
|
|
463
|
+
* Map an authenticated-request failure to a clear, agent-readable error.
|
|
464
|
+
*/
|
|
465
|
+
function alertHttpError(result, action) {
|
|
466
|
+
if (result.status === 0) {
|
|
467
|
+
return `Could not ${action} — the OilPriceAPI service was unreachable. Try again in a moment.`;
|
|
468
|
+
}
|
|
469
|
+
if (result.status === 401) {
|
|
470
|
+
return `Could not ${action}: authentication failed (401). The configured OILPRICEAPI_KEY is missing or invalid. Set a valid key (https://oilpriceapi.com/signup) and retry.`;
|
|
471
|
+
}
|
|
472
|
+
// Surface the API's own error/validation message when present.
|
|
473
|
+
const body = result.body;
|
|
474
|
+
let detail = "";
|
|
475
|
+
if (body && typeof body === "object") {
|
|
476
|
+
const obj = body;
|
|
477
|
+
if (typeof obj.message === "string")
|
|
478
|
+
detail = obj.message;
|
|
479
|
+
else if (typeof obj.error === "string")
|
|
480
|
+
detail = obj.error;
|
|
481
|
+
else if (obj.errors)
|
|
482
|
+
detail = JSON.stringify(obj.errors);
|
|
483
|
+
}
|
|
484
|
+
return `Could not ${action} (HTTP ${result.status})${detail ? `: ${detail}` : "."}`;
|
|
485
|
+
}
|
|
486
|
+
// Operators accepted by the /v1/alerts engine (PriceAlert::VALID_OPERATORS).
|
|
487
|
+
export const ALERT_OPERATORS = [
|
|
488
|
+
"greater_than",
|
|
489
|
+
"less_than",
|
|
490
|
+
"equals",
|
|
491
|
+
"greater_than_or_equal",
|
|
492
|
+
"less_than_or_equal",
|
|
493
|
+
];
|
|
494
|
+
/**
|
|
495
|
+
* Resolve a US state name or abbreviation to a 2-letter code.
|
|
261
496
|
*/
|
|
262
|
-
|
|
497
|
+
export function resolveStateCode(input) {
|
|
498
|
+
const normalized = input.toLowerCase().trim();
|
|
499
|
+
// Already a 2-letter code
|
|
500
|
+
if (/^[a-z]{2}$/i.test(normalized)) {
|
|
501
|
+
const upper = normalized.toUpperCase();
|
|
502
|
+
// Verify it's a real state abbreviation
|
|
503
|
+
if (Object.values(US_STATES).includes(upper) || upper === "DC") {
|
|
504
|
+
return upper;
|
|
505
|
+
}
|
|
506
|
+
return null;
|
|
507
|
+
}
|
|
508
|
+
// Try full name lookup
|
|
509
|
+
return US_STATES[normalized] ?? null;
|
|
510
|
+
}
|
|
511
|
+
// =========================================================================
|
|
512
|
+
// READ TOOLS (17 — opa_ prefixed to avoid collisions). Plus 4 authenticated
|
|
513
|
+
// price-alert tools further below, for 21 tools total.
|
|
514
|
+
// =========================================================================
|
|
515
|
+
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
516
|
commodity: z
|
|
264
517
|
.string()
|
|
265
518
|
.describe("Commodity name or code (e.g., 'brent oil', 'natural gas', 'WTI_USD', 'diesel')"),
|
|
266
519
|
}, async ({ commodity }) => {
|
|
267
|
-
const
|
|
268
|
-
|
|
520
|
+
const resolved = resolveOrError(commodity);
|
|
521
|
+
if ("error" in resolved)
|
|
522
|
+
return resolved.error;
|
|
523
|
+
const response = await makeApiRequest(`/v1/prices/latest?by_code=${resolved.code}`);
|
|
269
524
|
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
|
-
};
|
|
525
|
+
return errorResult(`Could not retrieve price for '${commodity}' (code: ${resolved.code}). The API may be temporarily unavailable — try again in a moment.`);
|
|
526
|
+
}
|
|
527
|
+
return textResult(`${formatPrice(response.data)}\n\n_Data from [OilPriceAPI](https://oilpriceapi.com)_`);
|
|
288
528
|
});
|
|
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.", {
|
|
529
|
+
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
530
|
category: z
|
|
294
|
-
.enum(["all", "oil", "gas", "coal", "refined"])
|
|
531
|
+
.enum(["all", "oil", "gas", "coal", "refined", "metals", "forex"])
|
|
295
532
|
.optional()
|
|
296
|
-
.describe("Filter by commodity category (default: all)"),
|
|
533
|
+
.describe("Filter by commodity category (default: all). Options: oil, gas, coal, refined, metals, forex."),
|
|
297
534
|
}, async ({ category = "all" }) => {
|
|
298
535
|
const response = await makeApiRequest("/v1/prices/all");
|
|
299
536
|
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
|
-
};
|
|
537
|
+
return errorResult("Could not retrieve market data. The API may be temporarily unavailable — try again in a moment.");
|
|
308
538
|
}
|
|
309
539
|
const prices = response.data.data.prices;
|
|
310
|
-
// Category filters
|
|
311
540
|
const categoryFilters = {
|
|
312
541
|
oil: ["BRENT_CRUDE_USD", "WTI_USD", "URALS_CRUDE_USD", "DUBAI_CRUDE_USD"],
|
|
313
542
|
gas: ["NATURAL_GAS_USD", "NATURAL_GAS_GBP", "DUTCH_TTF_EUR"],
|
|
@@ -319,6 +548,8 @@ server.tool("get_market_overview", "Get current prices for all tracked commoditi
|
|
|
319
548
|
"JET_FUEL_USD",
|
|
320
549
|
"HEATING_OIL_USD",
|
|
321
550
|
],
|
|
551
|
+
metals: ["GOLD_USD", "GOLD_AM_USD", "GOLD_PM_USD", "SILVER_FIX_USD"],
|
|
552
|
+
forex: ["EUR_USD", "GBP_USD"],
|
|
322
553
|
};
|
|
323
554
|
let filteredCodes;
|
|
324
555
|
if (category === "all") {
|
|
@@ -328,12 +559,13 @@ server.tool("get_market_overview", "Get current prices for all tracked commoditi
|
|
|
328
559
|
filteredCodes = categoryFilters[category] || [];
|
|
329
560
|
}
|
|
330
561
|
const sections = ["# Energy Market Overview\n"];
|
|
331
|
-
// Group by category
|
|
332
562
|
const groupedPrices = {
|
|
333
563
|
"Crude Oil": [],
|
|
334
564
|
"Natural Gas": [],
|
|
335
565
|
Coal: [],
|
|
336
566
|
"Refined Products": [],
|
|
567
|
+
"Precious Metals": [],
|
|
568
|
+
Forex: [],
|
|
337
569
|
Other: [],
|
|
338
570
|
};
|
|
339
571
|
for (const code of filteredCodes) {
|
|
@@ -359,6 +591,12 @@ server.tool("get_market_overview", "Get current prices for all tracked commoditi
|
|
|
359
591
|
].includes(code)) {
|
|
360
592
|
groupedPrices["Refined Products"].push(data);
|
|
361
593
|
}
|
|
594
|
+
else if (code.includes("GOLD") || code.includes("SILVER")) {
|
|
595
|
+
groupedPrices["Precious Metals"].push(data);
|
|
596
|
+
}
|
|
597
|
+
else if (code === "EUR_USD" || code === "GBP_USD") {
|
|
598
|
+
groupedPrices["Forex"].push(data);
|
|
599
|
+
}
|
|
362
600
|
else {
|
|
363
601
|
groupedPrices["Other"].push(data);
|
|
364
602
|
}
|
|
@@ -386,156 +624,143 @@ server.tool("get_market_overview", "Get current prices for all tracked commoditi
|
|
|
386
624
|
}
|
|
387
625
|
sections.push("");
|
|
388
626
|
}
|
|
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
|
-
};
|
|
627
|
+
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)_`);
|
|
628
|
+
return textResult(sections.join("\n"));
|
|
402
629
|
});
|
|
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).", {
|
|
630
|
+
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
631
|
commodities: z
|
|
408
632
|
.array(z.string())
|
|
409
633
|
.min(2)
|
|
410
634
|
.max(5)
|
|
411
|
-
.describe("List of
|
|
635
|
+
.describe("List of 2-5 commodity names or codes to compare (e.g., ['brent', 'wti'] or ['NATURAL_GAS_USD', 'DUTCH_TTF_EUR'])"),
|
|
412
636
|
}, async ({ commodities }) => {
|
|
413
|
-
const codes = commodities.map(resolveCommodityCode);
|
|
414
637
|
const results = [];
|
|
415
|
-
|
|
416
|
-
|
|
638
|
+
const errors = [];
|
|
639
|
+
for (const commodity of commodities) {
|
|
640
|
+
const resolved = resolveOrError(commodity);
|
|
641
|
+
if ("error" in resolved) {
|
|
642
|
+
errors.push(commodity);
|
|
643
|
+
continue;
|
|
644
|
+
}
|
|
645
|
+
const response = await makeApiRequest(`/v1/prices/latest?by_code=${resolved.code}`);
|
|
417
646
|
if (response?.status === "success") {
|
|
418
647
|
results.push(response.data);
|
|
419
648
|
}
|
|
420
649
|
}
|
|
421
650
|
if (results.length < 2) {
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
},
|
|
428
|
-
],
|
|
429
|
-
};
|
|
651
|
+
let msg = "Could not retrieve enough price data for comparison (need at least 2).";
|
|
652
|
+
if (errors.length > 0) {
|
|
653
|
+
msg += ` Unrecognized commodities: ${errors.join(", ")}. Use opa_list_commodities to see valid codes.`;
|
|
654
|
+
}
|
|
655
|
+
return errorResult(msg);
|
|
430
656
|
}
|
|
431
657
|
const sections = ["# Price Comparison\n"];
|
|
432
658
|
for (const data of results) {
|
|
433
659
|
sections.push(formatPrice(data));
|
|
434
660
|
sections.push("");
|
|
435
661
|
}
|
|
436
|
-
// Calculate spread if comparing similar commodities
|
|
437
662
|
if (results.length === 2 && results[0].currency === results[1].currency) {
|
|
438
663
|
const spread = Math.abs(results[0].price - results[1].price);
|
|
439
664
|
const info0 = COMMODITY_INFO[results[0].code]?.name || results[0].code;
|
|
440
665
|
const info1 = COMMODITY_INFO[results[1].code]?.name || results[1].code;
|
|
441
|
-
|
|
666
|
+
const currencySymbol = results[0].currency === "EUR"
|
|
667
|
+
? "€"
|
|
668
|
+
: results[0].currency === "GBP"
|
|
669
|
+
? "£"
|
|
670
|
+
: "$";
|
|
671
|
+
sections.push(`**Spread**: ${currencySymbol}${spread.toFixed(2)} (${info0} vs ${info1})`);
|
|
442
672
|
}
|
|
443
673
|
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
|
-
};
|
|
674
|
+
return textResult(sections.join("\n"));
|
|
452
675
|
});
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
676
|
+
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 () => {
|
|
677
|
+
// Try to fetch the live commodity catalog from the API
|
|
678
|
+
const response = await makeApiRequest("/v1/commodities");
|
|
679
|
+
if (response?.status === "success" && response.data.commodities?.length) {
|
|
680
|
+
const grouped = {};
|
|
681
|
+
for (const c of response.data.commodities) {
|
|
682
|
+
const cat = c.category || "Other";
|
|
683
|
+
if (!grouped[cat])
|
|
684
|
+
grouped[cat] = [];
|
|
685
|
+
grouped[cat].push({ code: c.code, name: c.name });
|
|
686
|
+
}
|
|
687
|
+
const sections = [
|
|
688
|
+
`# Available Commodities (${response.data.commodities.length} total)\n`,
|
|
689
|
+
];
|
|
690
|
+
for (const [category, items] of Object.entries(grouped)) {
|
|
691
|
+
sections.push(`## ${category}`);
|
|
692
|
+
for (const item of items) {
|
|
693
|
+
sections.push(`- \`${item.code}\` — ${item.name}`);
|
|
694
|
+
}
|
|
695
|
+
sections.push("");
|
|
696
|
+
}
|
|
697
|
+
sections.push("_You can use natural language like 'brent oil' or 'natural gas' — the server translates it to the right code._");
|
|
698
|
+
return textResult(sections.join("\n"));
|
|
699
|
+
}
|
|
700
|
+
// Fallback to static list if API call fails
|
|
457
701
|
const sections = ["# Available Commodities\n"];
|
|
458
702
|
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`
|
|
703
|
+
sections.push("- `BRENT_CRUDE_USD` — Brent Crude (global benchmark)");
|
|
704
|
+
sections.push("- `WTI_USD` — West Texas Intermediate (US benchmark)");
|
|
705
|
+
sections.push("- `URALS_CRUDE_USD` — Urals Crude (Russian)");
|
|
706
|
+
sections.push("- `DUBAI_CRUDE_USD` — Dubai Crude (Middle East)");
|
|
463
707
|
sections.push("");
|
|
464
708
|
sections.push("## Natural Gas");
|
|
465
|
-
sections.push("- `NATURAL_GAS_USD`
|
|
466
|
-
sections.push("- `NATURAL_GAS_GBP`
|
|
467
|
-
sections.push("- `DUTCH_TTF_EUR`
|
|
709
|
+
sections.push("- `NATURAL_GAS_USD` — US Henry Hub ($/MMBtu)");
|
|
710
|
+
sections.push("- `NATURAL_GAS_GBP` — UK NBP (pence/therm)");
|
|
711
|
+
sections.push("- `DUTCH_TTF_EUR` — European TTF (€/MWh)");
|
|
468
712
|
sections.push("");
|
|
469
713
|
sections.push("## Coal");
|
|
470
|
-
sections.push("- `COAL_USD`
|
|
471
|
-
sections.push("- `NEWCASTLE_COAL_USD`
|
|
714
|
+
sections.push("- `COAL_USD` — Thermal Coal");
|
|
715
|
+
sections.push("- `NEWCASTLE_COAL_USD` — Newcastle (Asia-Pacific)");
|
|
472
716
|
sections.push("");
|
|
473
717
|
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`
|
|
718
|
+
sections.push("- `DIESEL_USD` — Diesel");
|
|
719
|
+
sections.push("- `GASOLINE_USD` — Gasoline");
|
|
720
|
+
sections.push("- `GASOLINE_RBOB_USD` — RBOB Gasoline");
|
|
721
|
+
sections.push("- `JET_FUEL_USD` — Jet Fuel");
|
|
722
|
+
sections.push("- `HEATING_OIL_USD` — Heating Oil");
|
|
479
723
|
sections.push("");
|
|
480
724
|
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)");
|
|
725
|
+
sections.push("- `GOLD_USD` — Gold");
|
|
726
|
+
sections.push("- `SILVER_FIX_USD` — LBMA Silver Fix");
|
|
491
727
|
sections.push("");
|
|
492
728
|
sections.push("## Other");
|
|
493
|
-
sections.push("- `EU_CARBON_EUR`
|
|
494
|
-
sections.push("- `EUR_USD`
|
|
495
|
-
sections.push("- `GBP_USD`
|
|
729
|
+
sections.push("- `EU_CARBON_EUR` — EU Carbon Allowances");
|
|
730
|
+
sections.push("- `EUR_USD` — Euro to USD");
|
|
731
|
+
sections.push("- `GBP_USD` — British Pound to USD");
|
|
496
732
|
sections.push("");
|
|
497
|
-
sections.push("
|
|
498
|
-
return
|
|
499
|
-
content: [
|
|
500
|
-
{
|
|
501
|
-
type: "text",
|
|
502
|
-
text: sections.join("\n"),
|
|
503
|
-
},
|
|
504
|
-
],
|
|
505
|
-
};
|
|
733
|
+
sections.push("_Note: This is a partial list (API was unreachable). The full catalog has 70+ commodities. Try again later for the complete list._");
|
|
734
|
+
return textResult(sections.join("\n"));
|
|
506
735
|
});
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
commodity: z.string().describe("Commodity name or code"),
|
|
736
|
+
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).", {
|
|
737
|
+
commodity: z
|
|
738
|
+
.string()
|
|
739
|
+
.describe("Commodity name or code (e.g., 'brent', 'WTI_USD')"),
|
|
512
740
|
period: z
|
|
513
741
|
.enum(["day", "week", "month", "year"])
|
|
514
742
|
.default("month")
|
|
515
|
-
.describe("Time period"),
|
|
743
|
+
.describe("Time period: day, week, month, or year (default: month)"),
|
|
516
744
|
}, async ({ commodity, period }) => {
|
|
517
|
-
const
|
|
518
|
-
|
|
745
|
+
const resolved = resolveOrError(commodity);
|
|
746
|
+
if ("error" in resolved)
|
|
747
|
+
return resolved.error;
|
|
748
|
+
const response = await makeApiRequest(`/v1/prices/past_${period}?by_code=${resolved.code}`);
|
|
519
749
|
if (!response ||
|
|
520
750
|
response.status !== "success" ||
|
|
521
751
|
!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")
|
|
752
|
+
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.`);
|
|
753
|
+
}
|
|
754
|
+
const info = COMMODITY_INFO[resolved.code] || {
|
|
755
|
+
name: resolved.code,
|
|
756
|
+
unit: "unit",
|
|
757
|
+
};
|
|
758
|
+
const currencyFromCode = resolved.code.endsWith("_EUR")
|
|
534
759
|
? "EUR"
|
|
535
|
-
: code.endsWith("_GBP") || code.endsWith("_GBp")
|
|
760
|
+
: resolved.code.endsWith("_GBP") || resolved.code.endsWith("_GBp")
|
|
536
761
|
? "GBP"
|
|
537
762
|
: "USD";
|
|
538
|
-
const
|
|
763
|
+
const sym = currencyFromCode === "EUR" ? "€" : currencyFromCode === "GBP" ? "£" : "$";
|
|
539
764
|
const prices = response.data.prices;
|
|
540
765
|
const latest = prices[0];
|
|
541
766
|
const oldest = prices[prices.length - 1];
|
|
@@ -545,40 +770,30 @@ server.tool("get_historical_prices", "Get historical price data for a commodity
|
|
|
545
770
|
const change = latest.price - oldest.price;
|
|
546
771
|
const changePct = (change / oldest.price) * 100;
|
|
547
772
|
const sections = [
|
|
548
|
-
`# ${info.name}
|
|
549
|
-
`- **Latest**: ${
|
|
550
|
-
`- **High**: ${
|
|
551
|
-
`- **Low**: ${
|
|
552
|
-
`- **Average**: ${
|
|
553
|
-
`- **Change**: ${change >= 0 ? "+" : ""}${
|
|
773
|
+
`# ${info.name} — Past ${period.charAt(0).toUpperCase() + period.slice(1)}\n`,
|
|
774
|
+
`- **Latest**: ${sym}${latest.price.toFixed(2)}/${info.unit}`,
|
|
775
|
+
`- **High**: ${sym}${high.toFixed(2)}`,
|
|
776
|
+
`- **Low**: ${sym}${low.toFixed(2)}`,
|
|
777
|
+
`- **Average**: ${sym}${avg.toFixed(2)}`,
|
|
778
|
+
`- **Change**: ${change >= 0 ? "+" : ""}${sym}${change.toFixed(2)} (${change >= 0 ? "+" : ""}${changePct.toFixed(1)}%)`,
|
|
554
779
|
`- **Data Points**: ${prices.length}`,
|
|
555
780
|
`\n_Data from [OilPriceAPI](https://oilpriceapi.com)_`,
|
|
556
781
|
];
|
|
557
|
-
return
|
|
782
|
+
return textResult(sections.join("\n"));
|
|
558
783
|
});
|
|
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).", {
|
|
784
|
+
server.tool("opa_get_futures", "Get the latest front-month futures contract price for energy commodities. Use when the user asks about futures, forward prices, or contract prices. Supports crude oil (BZ = Brent, CL = WTI), ICE Gasoil (ice-gasoil), European TTF gas (ttf-gas), LNG JKM (lng-jkm), and EUA carbon (eua-carbon). For the full forward curve across all contract months, use opa_get_futures_curve instead.", {
|
|
563
785
|
contract: z
|
|
564
|
-
.enum(
|
|
786
|
+
.enum(FUTURES_CONTRACTS)
|
|
565
787
|
.default("BZ")
|
|
566
|
-
.describe("Futures contract
|
|
788
|
+
.describe("Futures contract: BZ = Brent crude, CL = WTI crude, ice-gasoil = ICE Gasoil, ttf-gas = European TTF natural gas, lng-jkm = LNG JKM (Asia), eua-carbon = EU carbon allowance (default: BZ)"),
|
|
567
789
|
}, async ({ contract }) => {
|
|
568
|
-
const response = await makeApiRequest(`/v1/futures/latest?contract=${contract}`);
|
|
790
|
+
const response = await makeApiRequest(`/v1/futures/latest?contract=${encodeURIComponent(contract)}`);
|
|
569
791
|
if (!response ||
|
|
570
792
|
response.status !== "success" ||
|
|
571
793
|
!response.data.contracts?.length) {
|
|
572
|
-
return {
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
type: "text",
|
|
576
|
-
text: `No futures data available for contract ${contract}.`,
|
|
577
|
-
},
|
|
578
|
-
],
|
|
579
|
-
};
|
|
580
|
-
}
|
|
581
|
-
const contractName = contract === "BZ" ? "Brent Crude" : "WTI Crude";
|
|
794
|
+
return errorResult(`No futures data available for ${FUTURES_CONTRACT_NAMES[contract]} (${contract}). Futures data requires a paid plan.`);
|
|
795
|
+
}
|
|
796
|
+
const contractName = FUTURES_CONTRACT_NAMES[contract];
|
|
582
797
|
const front = response.data.contracts[0];
|
|
583
798
|
let text = `# ${contractName} Futures (${contract})\n\n`;
|
|
584
799
|
text += `**Front Month (${front.month})**: $${front.price.toFixed(2)}`;
|
|
@@ -586,31 +801,21 @@ server.tool("get_futures_price", "Get the latest futures contract price for a co
|
|
|
586
801
|
text += ` (${front.change >= 0 ? "+" : ""}$${front.change.toFixed(2)})`;
|
|
587
802
|
}
|
|
588
803
|
text += `\n\n_Data from [OilPriceAPI](https://oilpriceapi.com)_`;
|
|
589
|
-
return
|
|
804
|
+
return textResult(text);
|
|
590
805
|
});
|
|
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.", {
|
|
806
|
+
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. Supports crude oil (BZ = Brent, CL = WTI), ICE Gasoil (ice-gasoil), European TTF gas (ttf-gas), LNG JKM (lng-jkm), and EUA carbon (eua-carbon). Returns a table of contract months with prices and changes, plus market structure analysis.", {
|
|
595
807
|
contract: z
|
|
596
|
-
.enum(
|
|
808
|
+
.enum(FUTURES_CONTRACTS)
|
|
597
809
|
.default("BZ")
|
|
598
|
-
.describe("Futures contract
|
|
810
|
+
.describe("Futures contract: BZ = Brent crude, CL = WTI crude, ice-gasoil = ICE Gasoil, ttf-gas = European TTF natural gas, lng-jkm = LNG JKM (Asia), eua-carbon = EU carbon allowance (default: BZ)"),
|
|
599
811
|
}, async ({ contract }) => {
|
|
600
|
-
const response = await makeApiRequest(`/v1/futures/curve?contract=${contract}`);
|
|
812
|
+
const response = await makeApiRequest(`/v1/futures/curve?contract=${encodeURIComponent(contract)}`);
|
|
601
813
|
if (!response ||
|
|
602
814
|
response.status !== "success" ||
|
|
603
815
|
!response.data.contracts?.length) {
|
|
604
|
-
return {
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
type: "text",
|
|
608
|
-
text: `No futures curve data available for contract ${contract}.`,
|
|
609
|
-
},
|
|
610
|
-
],
|
|
611
|
-
};
|
|
612
|
-
}
|
|
613
|
-
const contractName = contract === "BZ" ? "Brent Crude" : "WTI Crude";
|
|
816
|
+
return errorResult(`No futures curve data available for ${FUTURES_CONTRACT_NAMES[contract]} (${contract}). Futures data requires a paid plan.`);
|
|
817
|
+
}
|
|
818
|
+
const contractName = FUTURES_CONTRACT_NAMES[contract];
|
|
614
819
|
const contracts = response.data.contracts;
|
|
615
820
|
let text = `# ${contractName} Futures Curve (${contract})\n\n`;
|
|
616
821
|
text += `| Month | Price | Change |\n|-------|-------|--------|\n`;
|
|
@@ -625,20 +830,17 @@ server.tool("get_futures_curve", "Get the full futures forward curve showing pri
|
|
|
625
830
|
const structure = front > back ? "backwardation" : "contango";
|
|
626
831
|
text += `\n**Market Structure**: ${structure} (front $${front.toFixed(2)} vs back $${back.toFixed(2)})`;
|
|
627
832
|
text += `\n\n_Data from [OilPriceAPI](https://oilpriceapi.com)_`;
|
|
628
|
-
return
|
|
833
|
+
return textResult(text);
|
|
629
834
|
});
|
|
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.", {
|
|
835
|
+
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
836
|
port: z
|
|
635
837
|
.string()
|
|
636
838
|
.optional()
|
|
637
|
-
.describe("Filter by port name (e.g., 'SINGAPORE', 'ROTTERDAM')"),
|
|
839
|
+
.describe("Filter by port name (e.g., 'SINGAPORE', 'ROTTERDAM', 'HOUSTON')"),
|
|
638
840
|
fuel_type: z
|
|
639
841
|
.string()
|
|
640
842
|
.optional()
|
|
641
|
-
.describe("Filter by fuel type
|
|
843
|
+
.describe("Filter by fuel type: VLSFO, MGO, or IFO380"),
|
|
642
844
|
}, async ({ port, fuel_type }) => {
|
|
643
845
|
let endpoint = "/v1/marine-fuels/latest";
|
|
644
846
|
const params = [];
|
|
@@ -652,11 +854,7 @@ server.tool("get_marine_fuel_prices", "Get latest marine fuel (bunker) prices ac
|
|
|
652
854
|
if (!response ||
|
|
653
855
|
response.status !== "success" ||
|
|
654
856
|
!response.data.prices?.length) {
|
|
655
|
-
return
|
|
656
|
-
content: [
|
|
657
|
-
{ type: "text", text: "No marine fuel price data available." },
|
|
658
|
-
],
|
|
659
|
-
};
|
|
857
|
+
return errorResult("No marine fuel price data available. Marine fuel data requires a paid plan with bunker fuel coverage.");
|
|
660
858
|
}
|
|
661
859
|
const prices = response.data.prices;
|
|
662
860
|
let text = "# Marine Fuel Prices\n\n";
|
|
@@ -666,20 +864,15 @@ server.tool("get_marine_fuel_prices", "Get latest marine fuel (bunker) prices ac
|
|
|
666
864
|
text += `| ${p.port} | ${p.fuel_type} | ${p.price.toFixed(2)} | ${p.currency} | ${p.unit} |\n`;
|
|
667
865
|
}
|
|
668
866
|
text += `\n_${prices.length} prices | Data from [OilPriceAPI](https://oilpriceapi.com)_`;
|
|
669
|
-
return
|
|
867
|
+
return textResult(text);
|
|
670
868
|
});
|
|
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 () => {
|
|
869
|
+
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
870
|
const response = await makeApiRequest("/v1/rig-counts/latest");
|
|
676
871
|
if (!response || response.status !== "success") {
|
|
677
|
-
return
|
|
678
|
-
content: [{ type: "text", text: "Rig count data not available." }],
|
|
679
|
-
};
|
|
872
|
+
return errorResult("Rig count data not available. This may require a paid plan with energy intelligence access.");
|
|
680
873
|
}
|
|
681
874
|
const data = response.data;
|
|
682
|
-
let text = `# Rig Count
|
|
875
|
+
let text = `# US Rig Count (Baker Hughes)\n\n`;
|
|
683
876
|
text += `- **Oil Rigs**: ${data.oil}\n`;
|
|
684
877
|
text += `- **Gas Rigs**: ${data.gas}\n`;
|
|
685
878
|
text += `- **Total**: ${data.total}\n`;
|
|
@@ -689,22 +882,12 @@ server.tool("get_rig_counts", "Get the latest oil and gas rig count data (Baker
|
|
|
689
882
|
}
|
|
690
883
|
text += `- **Date**: ${data.date}\n`;
|
|
691
884
|
text += `\n_Data from [OilPriceAPI](https://oilpriceapi.com)_`;
|
|
692
|
-
return
|
|
885
|
+
return textResult(text);
|
|
693
886
|
});
|
|
694
|
-
|
|
695
|
-
* Get drilling intelligence data
|
|
696
|
-
*/
|
|
697
|
-
server.tool("get_drilling_intelligence", "Get drilling intelligence data including active wells, permits, and completions.", {}, async () => {
|
|
887
|
+
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
888
|
const response = await makeApiRequest("/v1/drilling/latest");
|
|
699
889
|
if (!response || response.status !== "success") {
|
|
700
|
-
return
|
|
701
|
-
content: [
|
|
702
|
-
{
|
|
703
|
-
type: "text",
|
|
704
|
-
text: "Drilling intelligence data not available.",
|
|
705
|
-
},
|
|
706
|
-
],
|
|
707
|
-
};
|
|
890
|
+
return errorResult("Drilling intelligence data not available. This requires a paid plan with energy intelligence access.");
|
|
708
891
|
}
|
|
709
892
|
const data = response.data;
|
|
710
893
|
let text = `# Drilling Intelligence\n\n`;
|
|
@@ -722,9 +905,345 @@ server.tool("get_drilling_intelligence", "Get drilling intelligence data includi
|
|
|
722
905
|
}
|
|
723
906
|
}
|
|
724
907
|
text += `\n_Data from [OilPriceAPI](https://oilpriceapi.com)_`;
|
|
725
|
-
return
|
|
908
|
+
return textResult(text);
|
|
909
|
+
});
|
|
910
|
+
// ---------------------------------------------------------------------------
|
|
911
|
+
// NEW TOOLS — Sprint 3
|
|
912
|
+
// ---------------------------------------------------------------------------
|
|
913
|
+
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.", {
|
|
914
|
+
state: z
|
|
915
|
+
.string()
|
|
916
|
+
.describe("US state name or 2-letter code (e.g., 'California', 'CA', 'Texas', 'TX')"),
|
|
917
|
+
}, async ({ state }) => {
|
|
918
|
+
const stateCode = resolveStateCode(state);
|
|
919
|
+
if (!stateCode) {
|
|
920
|
+
return errorResult(`'${state}' is not a recognized US state. Use a full state name (e.g., 'California') or 2-letter code (e.g., 'CA').`);
|
|
921
|
+
}
|
|
922
|
+
const code = `DIESEL_RETAIL_STATE_${stateCode}_USD`;
|
|
923
|
+
const response = await makeApiRequest(`/v1/prices/latest?by_code=${code}`);
|
|
924
|
+
if (!response || response.status !== "success") {
|
|
925
|
+
return errorResult(`No diesel price data available for ${state} (${stateCode}). State diesel data requires a plan with AAA diesel coverage.`);
|
|
926
|
+
}
|
|
927
|
+
const data = response.data;
|
|
928
|
+
let text = `# Diesel Price — ${stateCode}\n\n`;
|
|
929
|
+
text += `- **Price**: $${data.price.toFixed(3)}/gallon\n`;
|
|
930
|
+
if (data.change_24h !== undefined) {
|
|
931
|
+
const sign = data.change_24h >= 0 ? "+" : "";
|
|
932
|
+
text += `- **24h Change**: ${sign}$${data.change_24h.toFixed(3)}\n`;
|
|
933
|
+
}
|
|
934
|
+
const timestamp = data.updated_at || data.created_at;
|
|
935
|
+
if (timestamp) {
|
|
936
|
+
text += `- **Updated**: ${new Date(timestamp).toLocaleString("en-US", { dateStyle: "medium", timeStyle: "short", timeZone: "UTC" })} UTC\n`;
|
|
937
|
+
}
|
|
938
|
+
text += `- **Source**: AAA\n`;
|
|
939
|
+
text += `\n_Data from [OilPriceAPI](https://oilpriceapi.com)_`;
|
|
940
|
+
return textResult(text);
|
|
941
|
+
});
|
|
942
|
+
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.", {
|
|
943
|
+
facility: z
|
|
944
|
+
.enum(["cushing", "spr", "all"])
|
|
945
|
+
.default("all")
|
|
946
|
+
.describe("Storage facility: cushing (WTI delivery hub), spr (Strategic Petroleum Reserve), or all (default: all)"),
|
|
947
|
+
}, async ({ facility }) => {
|
|
948
|
+
const sections = ["# Oil Storage Levels\n"];
|
|
949
|
+
let hasData = false;
|
|
950
|
+
if (facility === "cushing" || facility === "all") {
|
|
951
|
+
const response = await makeApiRequest("/v1/storage/cushing");
|
|
952
|
+
if (response?.status === "success") {
|
|
953
|
+
hasData = true;
|
|
954
|
+
sections.push("## Cushing, Oklahoma (WTI Hub)\n");
|
|
955
|
+
sections.push("```json\n" + JSON.stringify(response.data, null, 2) + "\n```\n");
|
|
956
|
+
}
|
|
957
|
+
}
|
|
958
|
+
if (facility === "spr" || facility === "all") {
|
|
959
|
+
const response = await makeApiRequest("/v1/storage/spr");
|
|
960
|
+
if (response?.status === "success") {
|
|
961
|
+
hasData = true;
|
|
962
|
+
sections.push("## Strategic Petroleum Reserve (SPR)\n");
|
|
963
|
+
sections.push("```json\n" + JSON.stringify(response.data, null, 2) + "\n```\n");
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
if (!hasData) {
|
|
967
|
+
return errorResult("Storage data not available. This requires a paid plan with energy intelligence access.");
|
|
968
|
+
}
|
|
969
|
+
sections.push("_Data from [OilPriceAPI](https://oilpriceapi.com)_");
|
|
970
|
+
return textResult(sections.join("\n"));
|
|
726
971
|
});
|
|
727
|
-
|
|
972
|
+
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 () => {
|
|
973
|
+
const response = await makeApiRequest("/v1/ei/opec_productions/latest");
|
|
974
|
+
if (!response || response.status !== "success") {
|
|
975
|
+
return errorResult("OPEC production data not available. This requires a paid plan with energy intelligence access.");
|
|
976
|
+
}
|
|
977
|
+
let text = "# OPEC Production Data\n\n";
|
|
978
|
+
text += "```json\n" + JSON.stringify(response.data, null, 2) + "\n```\n";
|
|
979
|
+
text += "\n_Data from [OilPriceAPI](https://oilpriceapi.com)_";
|
|
980
|
+
return textResult(text);
|
|
981
|
+
});
|
|
982
|
+
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 () => {
|
|
983
|
+
const response = await makeApiRequest("/v1/ei/forecasts/latest");
|
|
984
|
+
if (!response || response.status !== "success") {
|
|
985
|
+
return errorResult("Forecast data not available. This requires a paid plan with energy intelligence access.");
|
|
986
|
+
}
|
|
987
|
+
let text = "# Energy Price Forecasts\n\n";
|
|
988
|
+
text += "```json\n" + JSON.stringify(response.data, null, 2) + "\n```\n";
|
|
989
|
+
text +=
|
|
990
|
+
"\n_Source: EIA STEO | Data from [OilPriceAPI](https://oilpriceapi.com)_";
|
|
991
|
+
return textResult(text);
|
|
992
|
+
});
|
|
993
|
+
// ---------------------------------------------------------------------------
|
|
994
|
+
// NEW TOOLS — Sprint 4 (EIA inventories, well permits, refining spreads)
|
|
995
|
+
// ---------------------------------------------------------------------------
|
|
996
|
+
server.tool("opa_get_oil_inventories", "Get the latest EIA weekly petroleum inventory (stocks) data. Use when the user asks about oil inventories, crude stocks, weekly EIA stocks, inventory builds/draws, or product-level inventory levels. Returns the latest weekly figures; optionally a summary view or a breakdown by petroleum product. Requires a paid plan with energy intelligence access.", {
|
|
997
|
+
view: z
|
|
998
|
+
.enum(["latest", "summary", "by_product"])
|
|
999
|
+
.default("latest")
|
|
1000
|
+
.describe("Which view to return: latest (most recent weekly snapshot), summary (headline totals + week-over-week change), or by_product (breakdown per petroleum product). Default: latest."),
|
|
1001
|
+
}, async ({ view }) => {
|
|
1002
|
+
const endpointByView = {
|
|
1003
|
+
latest: "/v1/ei/oil_inventories/latest",
|
|
1004
|
+
summary: "/v1/ei/oil_inventories/summary",
|
|
1005
|
+
by_product: "/v1/ei/oil_inventories/by_product",
|
|
1006
|
+
};
|
|
1007
|
+
const response = await makeApiRequest(endpointByView[view]);
|
|
1008
|
+
if (!response || response.status !== "success") {
|
|
1009
|
+
return errorResult("EIA oil inventory data not available. This requires a paid plan with energy intelligence access.");
|
|
1010
|
+
}
|
|
1011
|
+
let text = `# EIA Weekly Oil Inventories (${view})\n\n`;
|
|
1012
|
+
text += "```json\n" + JSON.stringify(response.data, null, 2) + "\n```\n";
|
|
1013
|
+
text +=
|
|
1014
|
+
"\n_Source: EIA Weekly Petroleum Status Report | Data from [OilPriceAPI](https://oilpriceapi.com)_";
|
|
1015
|
+
return textResult(text);
|
|
1016
|
+
});
|
|
1017
|
+
server.tool("opa_get_well_permits", "Get the latest US oil & gas well drilling permit data. Use when the user asks about well permits, new drilling permits, permitting activity, or upstream permit trends. Returns the latest permits; optionally filtered/aggregated by state or by operator. Requires a paid plan with energy intelligence access.", {
|
|
1018
|
+
view: z
|
|
1019
|
+
.enum(["latest", "by_state", "by_operator"])
|
|
1020
|
+
.default("latest")
|
|
1021
|
+
.describe("Which view to return: latest (most recent permits), by_state (counts aggregated per state), or by_operator (counts aggregated per operator). Default: latest."),
|
|
1022
|
+
state: z
|
|
1023
|
+
.string()
|
|
1024
|
+
.optional()
|
|
1025
|
+
.describe("Optional US state name or 2-letter code to filter permits (e.g., 'Texas', 'TX'). Applies to the latest and by_state views."),
|
|
1026
|
+
}, async ({ view, state }) => {
|
|
1027
|
+
const pathByView = {
|
|
1028
|
+
latest: "/v1/ei/well-permits/latest",
|
|
1029
|
+
by_state: "/v1/ei/well-permits/by-state",
|
|
1030
|
+
by_operator: "/v1/ei/well-permits/by-operator",
|
|
1031
|
+
};
|
|
1032
|
+
let endpoint = pathByView[view];
|
|
1033
|
+
if (state) {
|
|
1034
|
+
const stateCode = resolveStateCode(state);
|
|
1035
|
+
if (!stateCode) {
|
|
1036
|
+
return errorResult(`'${state}' is not a recognized US state. Use a full state name (e.g., 'Texas') or 2-letter code (e.g., 'TX').`);
|
|
1037
|
+
}
|
|
1038
|
+
endpoint += `?state=${stateCode}`;
|
|
1039
|
+
}
|
|
1040
|
+
const response = await makeApiRequest(endpoint);
|
|
1041
|
+
if (!response || response.status !== "success") {
|
|
1042
|
+
return errorResult("Well permit data not available. This requires a paid plan with energy intelligence access.");
|
|
1043
|
+
}
|
|
1044
|
+
let text = `# US Well Permits (${view})\n\n`;
|
|
1045
|
+
text += "```json\n" + JSON.stringify(response.data, null, 2) + "\n```\n";
|
|
1046
|
+
text += "\n_Data from [OilPriceAPI](https://oilpriceapi.com)_";
|
|
1047
|
+
return textResult(text);
|
|
1048
|
+
});
|
|
1049
|
+
server.tool("opa_get_spread", "Get refining and trading spreads: crack spreads (refining margin proxy), basis spreads (regional price differentials), and blending/transport margins. Use when the user asks about crack spreads, 3-2-1 crack, refining margins, basis differentials, or blend/transport margins. Requires a paid plan with energy intelligence access.", {
|
|
1050
|
+
type: z
|
|
1051
|
+
.enum(["crack", "basis", "margin"])
|
|
1052
|
+
.describe("Spread type: crack (refining crack spread, e.g. 3-2-1), basis (regional/grade price differential), or margin (blending/transport margin)."),
|
|
1053
|
+
}, async ({ type }) => {
|
|
1054
|
+
const response = await makeApiRequest(`/v1/spreads/${type}`);
|
|
1055
|
+
if (!response || response.status !== "success") {
|
|
1056
|
+
return errorResult(`${type.charAt(0).toUpperCase() + type.slice(1)} spread data not available. This requires a paid plan with energy intelligence access.`);
|
|
1057
|
+
}
|
|
1058
|
+
let text = `# ${type.charAt(0).toUpperCase() + type.slice(1)} Spread\n\n`;
|
|
1059
|
+
text += "```json\n" + JSON.stringify(response.data, null, 2) + "\n```\n";
|
|
1060
|
+
text += "\n_Data from [OilPriceAPI](https://oilpriceapi.com)_";
|
|
1061
|
+
return textResult(text);
|
|
1062
|
+
});
|
|
1063
|
+
function formatAlertLine(a) {
|
|
1064
|
+
const label = a.summary ||
|
|
1065
|
+
a.condition ||
|
|
1066
|
+
`${a.commodity_code} ${a.condition_operator} ${a.condition_value}`;
|
|
1067
|
+
const status = a.enabled === false ? "disabled" : "enabled";
|
|
1068
|
+
const triggers = typeof a.trigger_count === "number" ? `, ${a.trigger_count} triggers` : "";
|
|
1069
|
+
const last = a.last_triggered_at
|
|
1070
|
+
? `, last triggered ${a.last_triggered_at}`
|
|
1071
|
+
: "";
|
|
1072
|
+
return `- **${a.name || label}** (id: \`${a.id}\`) — ${label} [${status}${triggers}${last}]`;
|
|
1073
|
+
}
|
|
1074
|
+
server.tool("opa_create_price_alert", "Create a PERSISTENT price alert tied to the user's OilPriceAPI account. " +
|
|
1075
|
+
"The alert engine continuously watches live prices and notifies the user (by " +
|
|
1076
|
+
"email, plus webhook if a URL is given) when the commodity price crosses the " +
|
|
1077
|
+
"threshold. Use when the user asks to be alerted/notified when a price goes " +
|
|
1078
|
+
"above or below a level (e.g. 'tell me when Brent drops below $70'). " +
|
|
1079
|
+
"REQUIRES an API key (OILPRICEAPI_KEY) — this writes to the user's account. " +
|
|
1080
|
+
"Alerts persist until deleted; manage them with opa_list_price_alerts and " +
|
|
1081
|
+
"opa_delete_price_alert.", {
|
|
1082
|
+
commodity: z
|
|
1083
|
+
.string()
|
|
1084
|
+
.describe("Commodity name or code to watch (e.g., 'brent', 'natural gas', 'WTI_USD')."),
|
|
1085
|
+
operator: z
|
|
1086
|
+
.enum(ALERT_OPERATORS)
|
|
1087
|
+
.describe("Threshold comparison: greater_than, less_than, equals, greater_than_or_equal, or less_than_or_equal. The alert fires when (current price) <operator> (threshold)."),
|
|
1088
|
+
threshold: z
|
|
1089
|
+
.number()
|
|
1090
|
+
.positive()
|
|
1091
|
+
.describe("The price threshold to compare against, in the commodity's native currency (e.g., 70 for $70/barrel)."),
|
|
1092
|
+
name: z
|
|
1093
|
+
.string()
|
|
1094
|
+
.optional()
|
|
1095
|
+
.describe("Optional human-readable label for the alert. If omitted, a descriptive name is generated."),
|
|
1096
|
+
notify: z
|
|
1097
|
+
.string()
|
|
1098
|
+
.url()
|
|
1099
|
+
.optional()
|
|
1100
|
+
.describe("Optional HTTPS webhook URL to POST to when the alert triggers (in addition to email). Must start with https://."),
|
|
1101
|
+
}, async ({ commodity, operator, threshold, name, notify }) => {
|
|
1102
|
+
const keyErr = requireApiKey();
|
|
1103
|
+
if (keyErr)
|
|
1104
|
+
return keyErr;
|
|
1105
|
+
const resolved = resolveOrError(commodity);
|
|
1106
|
+
if ("error" in resolved)
|
|
1107
|
+
return resolved.error;
|
|
1108
|
+
const operatorText = {
|
|
1109
|
+
greater_than: ">",
|
|
1110
|
+
less_than: "<",
|
|
1111
|
+
equals: "=",
|
|
1112
|
+
greater_than_or_equal: ">=",
|
|
1113
|
+
less_than_or_equal: "<=",
|
|
1114
|
+
};
|
|
1115
|
+
const defaultName = `${resolved.code} ${operatorText[operator]} ${threshold}`;
|
|
1116
|
+
const alert = {
|
|
1117
|
+
name: name || defaultName,
|
|
1118
|
+
commodity_code: resolved.code,
|
|
1119
|
+
condition_operator: operator,
|
|
1120
|
+
condition_value: threshold,
|
|
1121
|
+
// Attribution: stamp the alert as MCP-created via the alert's metadata
|
|
1122
|
+
// (the API permits a free-form metadata object on price alerts).
|
|
1123
|
+
metadata: { source: "mcp" },
|
|
1124
|
+
};
|
|
1125
|
+
if (notify)
|
|
1126
|
+
alert.webhook_url = notify;
|
|
1127
|
+
const result = await makeAuthRequest("/v1/alerts", {
|
|
1128
|
+
method: "POST",
|
|
1129
|
+
body: { price_alert: alert },
|
|
1130
|
+
});
|
|
1131
|
+
if (!result.ok) {
|
|
1132
|
+
return errorResult(alertHttpError(result, "create the price alert"));
|
|
1133
|
+
}
|
|
1134
|
+
const created = result.body;
|
|
1135
|
+
let text = "# Price Alert Created\n\n";
|
|
1136
|
+
text += formatAlertLine(created);
|
|
1137
|
+
text +=
|
|
1138
|
+
"\n\nThis alert is now active on the user's account and will notify them when the condition is met. " +
|
|
1139
|
+
"Use `opa_list_price_alerts` to see all alerts or `opa_delete_price_alert` to remove it.";
|
|
1140
|
+
text += `\n\n_Data from [OilPriceAPI](https://oilpriceapi.com)_`;
|
|
1141
|
+
return textResult(text);
|
|
1142
|
+
});
|
|
1143
|
+
server.tool("opa_list_price_alerts", "List all PERSISTENT price alerts on the user's OilPriceAPI account. Use when " +
|
|
1144
|
+
"the user asks what alerts they have set up, or to find an alert's id before " +
|
|
1145
|
+
"deleting it. REQUIRES an API key (OILPRICEAPI_KEY) — alerts are account-scoped. " +
|
|
1146
|
+
"No parameters needed.", {}, async () => {
|
|
1147
|
+
const keyErr = requireApiKey();
|
|
1148
|
+
if (keyErr)
|
|
1149
|
+
return keyErr;
|
|
1150
|
+
const result = await makeAuthRequest("/v1/alerts");
|
|
1151
|
+
if (!result.ok) {
|
|
1152
|
+
return errorResult(alertHttpError(result, "list price alerts"));
|
|
1153
|
+
}
|
|
1154
|
+
const alerts = Array.isArray(result.body)
|
|
1155
|
+
? result.body
|
|
1156
|
+
: [];
|
|
1157
|
+
if (alerts.length === 0) {
|
|
1158
|
+
return textResult("No price alerts are set up on this account yet. Use `opa_create_price_alert` to create one.");
|
|
1159
|
+
}
|
|
1160
|
+
const sections = [`# Price Alerts (${alerts.length})\n`];
|
|
1161
|
+
for (const a of alerts)
|
|
1162
|
+
sections.push(formatAlertLine(a));
|
|
1163
|
+
sections.push(`\n_Data from [OilPriceAPI](https://oilpriceapi.com)_`);
|
|
1164
|
+
return textResult(sections.join("\n"));
|
|
1165
|
+
});
|
|
1166
|
+
server.tool("opa_delete_price_alert", "Permanently delete a price alert from the user's OilPriceAPI account by id. " +
|
|
1167
|
+
"Use when the user wants to remove/cancel/stop an existing alert. Get the id " +
|
|
1168
|
+
"from opa_list_price_alerts first. This permanently removes the alert from the " +
|
|
1169
|
+
"user's account. REQUIRES an API key (OILPRICEAPI_KEY).", {
|
|
1170
|
+
id: z
|
|
1171
|
+
.string()
|
|
1172
|
+
.describe("The id of the alert to delete (a UUID, as returned by opa_list_price_alerts or opa_create_price_alert)."),
|
|
1173
|
+
}, async ({ id }) => {
|
|
1174
|
+
const keyErr = requireApiKey();
|
|
1175
|
+
if (keyErr)
|
|
1176
|
+
return keyErr;
|
|
1177
|
+
const result = await makeAuthRequest(`/v1/alerts/${encodeURIComponent(id)}`, { method: "DELETE" });
|
|
1178
|
+
if (result.status === 404) {
|
|
1179
|
+
return errorResult(`No alert found with id \`${id}\` on this account. Use opa_list_price_alerts to see valid ids.`);
|
|
1180
|
+
}
|
|
1181
|
+
if (!result.ok) {
|
|
1182
|
+
return errorResult(alertHttpError(result, "delete the price alert"));
|
|
1183
|
+
}
|
|
1184
|
+
return textResult(`Price alert \`${id}\` was permanently deleted from the user's account.`);
|
|
1185
|
+
});
|
|
1186
|
+
server.tool("opa_get_alert_triggers", "Get recent trigger activity for the user's price alerts — which alerts have " +
|
|
1187
|
+
"fired, how many times, and when they last triggered. Use when the user asks " +
|
|
1188
|
+
"whether any alerts have gone off or about recent alert activity. REQUIRES an " +
|
|
1189
|
+
"API key (OILPRICEAPI_KEY). Note: the API tracks trigger history as per-alert " +
|
|
1190
|
+
"counters (trigger_count / last_triggered_at) rather than a separate event " +
|
|
1191
|
+
"feed, so this returns alerts that have triggered.", {
|
|
1192
|
+
since: z
|
|
1193
|
+
.string()
|
|
1194
|
+
.optional()
|
|
1195
|
+
.describe("Optional ISO 8601 date/time (e.g., '2026-06-01' or '2026-06-01T00:00:00Z'). Only alerts last triggered on or after this time are shown."),
|
|
1196
|
+
}, async ({ since }) => {
|
|
1197
|
+
const keyErr = requireApiKey();
|
|
1198
|
+
if (keyErr)
|
|
1199
|
+
return keyErr;
|
|
1200
|
+
let sinceTime = null;
|
|
1201
|
+
if (since) {
|
|
1202
|
+
const parsed = Date.parse(since);
|
|
1203
|
+
if (Number.isNaN(parsed)) {
|
|
1204
|
+
return errorResult(`'${since}' is not a valid date. Use an ISO 8601 date like '2026-06-01' or '2026-06-01T00:00:00Z'.`);
|
|
1205
|
+
}
|
|
1206
|
+
sinceTime = parsed;
|
|
1207
|
+
}
|
|
1208
|
+
const result = await makeAuthRequest("/v1/alerts");
|
|
1209
|
+
if (!result.ok) {
|
|
1210
|
+
return errorResult(alertHttpError(result, "fetch alert triggers"));
|
|
1211
|
+
}
|
|
1212
|
+
const alerts = Array.isArray(result.body)
|
|
1213
|
+
? result.body
|
|
1214
|
+
: [];
|
|
1215
|
+
let triggered = alerts.filter((a) => typeof a.trigger_count === "number" && a.trigger_count > 0);
|
|
1216
|
+
if (sinceTime !== null) {
|
|
1217
|
+
triggered = triggered.filter((a) => {
|
|
1218
|
+
if (!a.last_triggered_at)
|
|
1219
|
+
return false;
|
|
1220
|
+
const t = Date.parse(a.last_triggered_at);
|
|
1221
|
+
return !Number.isNaN(t) && t >= sinceTime;
|
|
1222
|
+
});
|
|
1223
|
+
}
|
|
1224
|
+
if (triggered.length === 0) {
|
|
1225
|
+
return textResult(since
|
|
1226
|
+
? `No price alerts have triggered since ${since}.`
|
|
1227
|
+
: "No price alerts have triggered yet.");
|
|
1228
|
+
}
|
|
1229
|
+
triggered.sort((a, b) => {
|
|
1230
|
+
const ta = a.last_triggered_at ? Date.parse(a.last_triggered_at) : 0;
|
|
1231
|
+
const tb = b.last_triggered_at ? Date.parse(b.last_triggered_at) : 0;
|
|
1232
|
+
return tb - ta;
|
|
1233
|
+
});
|
|
1234
|
+
const sections = [`# Recent Alert Triggers (${triggered.length})\n`];
|
|
1235
|
+
for (const a of triggered) {
|
|
1236
|
+
const label = a.summary ||
|
|
1237
|
+
a.condition ||
|
|
1238
|
+
`${a.commodity_code} ${a.condition_operator} ${a.condition_value}`;
|
|
1239
|
+
sections.push(`- **${a.name || label}** (id: \`${a.id}\`) — ${a.trigger_count} trigger(s), last at ${a.last_triggered_at}`);
|
|
1240
|
+
}
|
|
1241
|
+
sections.push(`\n_Data from [OilPriceAPI](https://oilpriceapi.com)_`);
|
|
1242
|
+
return textResult(sections.join("\n"));
|
|
1243
|
+
});
|
|
1244
|
+
// =========================================================================
|
|
1245
|
+
// RESOURCES — subscribable price snapshots + dynamic template
|
|
1246
|
+
// =========================================================================
|
|
728
1247
|
server.resource("price-brent", "price://brent", {
|
|
729
1248
|
description: "Current Brent Crude oil price (global benchmark)",
|
|
730
1249
|
mimeType: "application/json",
|
|
@@ -785,14 +1304,31 @@ server.resource("market-overview", "price://all", {
|
|
|
785
1304
|
],
|
|
786
1305
|
};
|
|
787
1306
|
});
|
|
788
|
-
|
|
1307
|
+
server.resource("price-diesel", "price://diesel", {
|
|
1308
|
+
description: "Current US national average diesel price",
|
|
1309
|
+
mimeType: "application/json",
|
|
1310
|
+
}, async () => {
|
|
1311
|
+
const response = await makeApiRequest("/v1/prices/latest?by_code=DIESEL_USD");
|
|
1312
|
+
return {
|
|
1313
|
+
contents: [
|
|
1314
|
+
{
|
|
1315
|
+
uri: "price://diesel",
|
|
1316
|
+
mimeType: "application/json",
|
|
1317
|
+
text: JSON.stringify(response?.data ?? { error: "unavailable" }, null, 2),
|
|
1318
|
+
},
|
|
1319
|
+
],
|
|
1320
|
+
};
|
|
1321
|
+
});
|
|
1322
|
+
// =========================================================================
|
|
1323
|
+
// PROMPTS — pre-built analyst templates
|
|
1324
|
+
// =========================================================================
|
|
789
1325
|
server.prompt("daily-briefing", "Energy market daily briefing with key prices, changes, and notable movements", {}, () => ({
|
|
790
1326
|
messages: [
|
|
791
1327
|
{
|
|
792
1328
|
role: "user",
|
|
793
1329
|
content: {
|
|
794
1330
|
type: "text",
|
|
795
|
-
text: "Give me today's energy market briefing.
|
|
1331
|
+
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
1332
|
},
|
|
797
1333
|
},
|
|
798
1334
|
],
|
|
@@ -803,7 +1339,7 @@ server.prompt("brent-wti-spread", "Analyze the Brent-WTI crude oil spread", {},
|
|
|
803
1339
|
role: "user",
|
|
804
1340
|
content: {
|
|
805
1341
|
type: "text",
|
|
806
|
-
text: "
|
|
1342
|
+
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
1343
|
},
|
|
808
1344
|
},
|
|
809
1345
|
],
|
|
@@ -814,7 +1350,7 @@ server.prompt("gas-market-analysis", "Compare US vs European natural gas markets
|
|
|
814
1350
|
role: "user",
|
|
815
1351
|
content: {
|
|
816
1352
|
type: "text",
|
|
817
|
-
text: "
|
|
1353
|
+
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
1354
|
},
|
|
819
1355
|
},
|
|
820
1356
|
],
|
|
@@ -829,7 +1365,29 @@ server.prompt("commodity-report", "Detailed report on a specific commodity", {
|
|
|
829
1365
|
role: "user",
|
|
830
1366
|
content: {
|
|
831
1367
|
type: "text",
|
|
832
|
-
text: `
|
|
1368
|
+
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?`,
|
|
1369
|
+
},
|
|
1370
|
+
},
|
|
1371
|
+
],
|
|
1372
|
+
}));
|
|
1373
|
+
server.prompt("diesel-cost-analysis", "Compare diesel prices across US states for fleet cost planning", {}, () => ({
|
|
1374
|
+
messages: [
|
|
1375
|
+
{
|
|
1376
|
+
role: "user",
|
|
1377
|
+
content: {
|
|
1378
|
+
type: "text",
|
|
1379
|
+
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.",
|
|
1380
|
+
},
|
|
1381
|
+
},
|
|
1382
|
+
],
|
|
1383
|
+
}));
|
|
1384
|
+
server.prompt("supply-analysis", "Analyze oil supply fundamentals using production, rig counts, and storage data", {}, () => ({
|
|
1385
|
+
messages: [
|
|
1386
|
+
{
|
|
1387
|
+
role: "user",
|
|
1388
|
+
content: {
|
|
1389
|
+
type: "text",
|
|
1390
|
+
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
1391
|
},
|
|
834
1392
|
},
|
|
835
1393
|
],
|
|
@@ -840,13 +1398,12 @@ export function createSandboxServer() {
|
|
|
840
1398
|
}
|
|
841
1399
|
// Main entry point
|
|
842
1400
|
async function main() {
|
|
843
|
-
// Check for API key
|
|
844
1401
|
if (!API_KEY) {
|
|
845
|
-
console.error("Warning: OILPRICEAPI_KEY not set.
|
|
1402
|
+
console.error("Warning: OILPRICEAPI_KEY not set. Get a free key at https://oilpriceapi.com/signup");
|
|
846
1403
|
}
|
|
847
1404
|
const transport = new StdioServerTransport();
|
|
848
1405
|
await server.connect(transport);
|
|
849
|
-
console.error("OilPriceAPI MCP Server running on stdio");
|
|
1406
|
+
console.error("OilPriceAPI MCP Server v2.2.0 running on stdio");
|
|
850
1407
|
}
|
|
851
1408
|
main().catch((error) => {
|
|
852
1409
|
console.error("Fatal error:", error);
|