Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/live-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ jobs:
node-version: "22"
- run: npm ci
- run: npm audit --audit-level=low
- name: Live smoke test (ice-brent latest + curve)
- name: Live smoke test (generic Brent latest + curve)
env:
OILPRICEAPI_TEST_KEY: ${{ secrets.OILPRICEAPI_TEST_KEY }}
OILPRICEAPI_LIVE_REQUIRED: "1"
Expand Down
4 changes: 2 additions & 2 deletions .mcp.json
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
{
"name": "oilpriceapi",
"description": "Source-timestamped energy data and reviewed OilPriceAPI product facts for MCP clients.",
"version": "3.2.2",
"version": "3.2.3",
"mcpServers": {
"oilpriceapi": {
"command": "npx",
"args": ["-y", "oilpriceapi-mcp@3.2.2"]
"args": ["-y", "oilpriceapi-mcp@3.2.3"]
}
}
}
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -257,7 +257,7 @@ All tools are prefixed with `opa_` to avoid name collisions when multiple MCP se
| `opa_compare_prices` | Side-by-side comparison of 2-5 commodities with spread |
| `opa_list_commodities` | Account-visible commodity catalog returned by the live API |
| `opa_get_history` | Historical prices with high/low/avg/change (day/week/month/year) |
| `opa_get_futures` | Front-month futures (Brent BZ, WTI CL, ICE Gasoil, TTF, JKM, EUA) |
| `opa_get_futures` | Front-month futures (Brent, WTI, gasoil, TTF, JKM, EU carbon) |
| `opa_get_futures_curve` | Full forward curve with contango/backwardation analysis |
| `opa_get_marine_fuels` | Bunker fuel prices by port and fuel type (VLSFO/MGO/IFO380) |
| `opa_get_rig_counts` | Baker Hughes US rig count with week-over-week change |
Expand Down Expand Up @@ -315,7 +315,7 @@ The market brief gives a multi-commodity snapshot in one call. Subscriptions ("w
"How many well permits were issued in Texas?"
"What's the current 3-2-1 crack spread?"
"What's the UPS ground fuel surcharge?"
"Show me the ICE Gasoil futures curve"
"Show me the gasoil futures curve"
```

## Resources
Expand Down
2 changes: 1 addition & 1 deletion manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"manifest_version": "0.2",
"name": "oilpriceapi",
"display_name": "OilPriceAPI \u2014 Oil, Gas & Commodity Prices",
"version": "3.2.2",
"version": "3.2.3",
"description": "Source-timestamped energy data and reviewed OilPriceAPI product facts for MCP clients.",
"long_description": "OilPriceAPI tools and resources for latest available energy values, history, futures, refining spreads, carrier fuel surcharges, selected energy-intelligence datasets, alerts, market briefs, and a versioned public product contract. Keyless demo mode supports a limited commodity set. Dataset access and limits vary by plan and account entitlement.",
"author": {
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "oilpriceapi-mcp",
"version": "3.2.2",
"version": "3.2.3",
"mcpName": "io.github.OilpriceAPI/mcp-server",
"description": "Source-timestamped oil, gas, and related energy data plus reviewed product facts for MCP clients.",
"type": "module",
Expand Down
6 changes: 3 additions & 3 deletions scripts/live-smoke.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@
*
* Verifies the futures path fix (v2.2.1) AND the #3245 subscription +
* market-brief tools (v2.3.0) end-to-end against PRODUCTION:
* - GET /v1/futures/ice-brent (latest) -> 200 + numeric front-month last_price
* - GET /v1/futures/ice-brent/curve (curve) -> 200 + curve data OR documented no-data state
* - GET /v1/futures/brent (latest) -> 200 + numeric front-month last_price
* - GET /v1/futures/brent/curve (curve) -> 200 + curve data OR documented no-data state
* - GET /v1/market-brief?codes=BRENT_CRUDE_USD -> 200 + a numeric price (opa_get_market_brief)
* - GET /v1/subscriptions (list) -> 200 + { data: { subscriptions: [...] } } (opa_list_subscriptions)
* - POST/GET/DELETE round-trip on /v1/subscriptions (opa_create_price_subscription / events / delete)
Expand Down Expand Up @@ -37,7 +37,7 @@ const API_BASE =
process.env.OILPRICEAPI_BASE_URL || "https://api.oilpriceapi.com";
const KEY = process.env.OILPRICEAPI_TEST_KEY;
const REQUIRE_KEY = process.env.OILPRICEAPI_LIVE_REQUIRED === "1";
const SLUG = "ice-brent";
const SLUG = "brent";
const RATE_LIMIT_MS = 1100; // > 1 req/sec
const RATE_LIMIT_RETRIES = 1;
// Opt-in: exercise the WRITE round-trip (create -> events -> delete) against
Expand Down
4 changes: 2 additions & 2 deletions server.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,12 @@
"url": "https://github.com/OilpriceAPI/mcp-server",
"source": "github"
},
"version": "3.2.2",
"version": "3.2.3",
"packages": [
{
"registryType": "npm",
"identifier": "oilpriceapi-mcp",
"version": "3.2.2",
"version": "3.2.3",
"transport": {
"type": "stdio"
},
Expand Down
148 changes: 140 additions & 8 deletions src/__tests__/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,135 @@ import {
SERVER_INSTRUCTIONS,
productFactsProvider,
formatStartupInventory,
FUTURES_CONTRACTS,
FUTURES_CONTRACT_SLUGS,
FUTURES_CONTRACT_NAMES,
} from "../index.js";

describe("instrument-generic futures routes (#4175)", () => {
const routeCases = [
["brent", "brent"],
["BZ", "brent"],
["ice-brent", "brent"],
["wti", "wti"],
["CL", "wti"],
["ice-wti", "wti"],
["gasoil", "gasoil"],
["G", "gasoil"],
["ice-gasoil", "gasoil"],
["eu-carbon", "eu-carbon"],
["EUA", "eu-carbon"],
["eua-carbon", "eu-carbon"],
] as const;
const server = createSandboxServer();
const tools = (
server as unknown as {
_registeredTools: Record<
string,
{
handler: (
args: Record<string, unknown>,
extra: Record<string, unknown>,
) => Promise<{
content: Array<{ type: string; text: string }>;
isError?: boolean;
}>;
}
>;
}
)._registeredTools;

afterEach(() => {
vi.unstubAllEnvs();
vi.unstubAllGlobals();
});

it.each(routeCases)(
"normalizes %s to /v1/futures/%s",
async (input, canonical) => {
vi.stubEnv("OILPRICEAPI_KEY", "test-key-123");
const fetchSpy = vi.fn().mockResolvedValue({
ok: true,
status: 200,
headers: { get: () => null },
json: async () => ({
front_month: { contract_month: "2026-09", last_price: 70 },
contracts: [{ contract_month: "2026-09", last_price: 70 }],
}),
});
vi.stubGlobal("fetch", fetchSpy);

const result = await tools.opa_get_futures.handler(
{ contract: input },
{},
);

expect(result.isError).toBeUndefined();
expect(new URL(String(fetchSpy.mock.calls[0][0])).pathname).toBe(
`/v1/futures/${canonical}`,
);
},
);

it.each(routeCases)(
"normalizes curve input %s to /v1/futures/%s/curve",
async (input, canonical) => {
vi.stubEnv("OILPRICEAPI_KEY", "test-key-123");
const fetchSpy = vi.fn().mockResolvedValue({
ok: true,
status: 200,
headers: { get: () => null },
json: async () => ({
contracts: [
{ contract_month: "2026-09", settlement_price: 70 },
{ contract_month: "2026-10", settlement_price: 71 },
],
}),
});
vi.stubGlobal("fetch", fetchSpy);

const result = await tools.opa_get_futures_curve.handler(
{ contract: input },
{},
);

expect(result.isError).toBeUndefined();
expect(new URL(String(fetchSpy.mock.calls[0][0])).pathname).toBe(
`/v1/futures/${canonical}/curve`,
);
},
);

it("publishes generic slugs while retaining legacy inputs", () => {
for (const slug of ["brent", "wti", "gasoil", "eu-carbon"] as const) {
expect(FUTURES_CONTRACTS).toContain(slug);
expect(FUTURES_CONTRACT_SLUGS[slug]).toBe(slug);
}
expect(FUTURES_CONTRACT_SLUGS["ice-brent"]).toBe("brent");
expect(FUTURES_CONTRACT_SLUGS["ice-wti"]).toBe("wti");
expect(FUTURES_CONTRACT_SLUGS["ice-gasoil"]).toBe("gasoil");
expect(FUTURES_CONTRACT_SLUGS["eua-carbon"]).toBe("eu-carbon");
expect(FUTURES_CONTRACT_NAMES["ice-brent"]).toBe("Brent Crude");
expect(FUTURES_CONTRACT_NAMES["ice-wti"]).toBe("WTI Crude");
expect(FUTURES_CONTRACT_NAMES["ice-gasoil"]).toBe("Gasoil");
});

it("recommends generic slugs and scopes older venue names to compatibility", () => {
const metadata = (
server as unknown as {
_registeredTools: Record<string, { description?: string }>;
}
)._registeredTools;

for (const name of ["opa_get_futures", "opa_get_futures_curve"]) {
const description = metadata[name].description ?? "";
expect(description).toContain("canonical instrument slugs");
expect(description).toContain("compatibility inputs");
expect(description).not.toMatch(/ICE Brent|ICE WTI|ICE Gasoil/);
}
});
});

describe("startup inventory", () => {
it("reports enabled and registered counts without a fixed denominator", () => {
expect(
Expand Down Expand Up @@ -109,7 +236,9 @@ describe("wellPermitSearchEndpoint (#57)", () => {
operator: "Example",
county: "Reeves",
}),
).toEqual(expect.objectContaining({ error: expect.stringMatching(/cannot/) }));
).toEqual(
expect.objectContaining({ error: expect.stringMatching(/cannot/) }),
);
expect(
wellPermitSearchEndpoint({
state: "TX",
Expand All @@ -119,9 +248,9 @@ describe("wellPermitSearchEndpoint (#57)", () => {
).toEqual(
expect.objectContaining({ error: expect.stringMatching(/on or before/) }),
);
expect(
wellPermitSearchEndpoint({ state: "Atlantis" }),
).toEqual(expect.objectContaining({ error: expect.stringMatching(/state/) }));
expect(wellPermitSearchEndpoint({ state: "Atlantis" })).toEqual(
expect.objectContaining({ error: expect.stringMatching(/state/) }),
);
});
});

Expand All @@ -143,7 +272,9 @@ describe("wellLookupEndpoint (#57)", () => {
normalizedApi: "423294471300",
});
expect(wellLookupEndpoint("4232")).toEqual(
expect.objectContaining({ error: expect.stringMatching(/10-, 12-, or 14/) }),
expect.objectContaining({
error: expect.stringMatching(/10-, 12-, or 14/),
}),
);
});
});
Expand Down Expand Up @@ -1141,7 +1272,7 @@ describe("tier-limit gate errors (#17) - makeApiRequest", () => {
});

const error = await makeApiRequest(
"/v1/futures/ice-brent",
"/v1/futures/brent",
mockFetch as typeof fetch,
).catch((e: unknown) => e);

Expand Down Expand Up @@ -1355,7 +1486,9 @@ describe("tool registration metadata", () => {
expect(description).toContain(
"Observation-dated bulk backfills may appear in an earlier as_of result",
);
expect(description).not.toMatch(/as it was knowable|later-collected rows absent/i);
expect(description).not.toMatch(
/as it was knowable|later-collected rows absent/i,
);
});

it("registers all four write tools (creates + deletes)", () => {
Expand Down Expand Up @@ -1566,7 +1699,6 @@ describe("reviewed plan allowance copy", () => {
expect(text).toContain("Dataset access and limits vary by plan");
expect(text).not.toContain("latest prices only");
expect(text).not.toMatch(/200 requests?\/(?:mo|month)/i);

});
});

Expand Down
Loading