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: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -291,6 +291,8 @@ All tools are prefixed with `opa_` to avoid name collisions when multiple MCP se
| `opa_get_futures_curve` | Full forward curve with contango/backwardation analysis |
| `opa_get_futures_history` | Brent, WTI, NYMEX natural gas OHLC history per contract month, up to 730 days |
| `opa_get_marine_fuels` | Bunker fuel prices by port and fuel type (VLSFO/MGO/IFO380) |
| `opa_get_marine_ports` | Bunkering port directory (UN/LOCODE, region, fuel services) or one port with recent prices |
| `opa_get_marine_fuel_history` | Daily history of one marine fuel at one port; paid subscription or active trial |
| `opa_get_rig_counts` | Baker Hughes US total rig count with region and observation date |
| `opa_get_drilling` | Drilling snapshot: rig counts, frac spreads, 30-day permits, DUCs |
| `opa_get_diesel_by_state` | AAA retail diesel price for any US state (50 states + DC) |
Expand Down
41 changes: 26 additions & 15 deletions capability-ledger.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"$comment": "API-to-MCP capability ledger (#77). One row per operation in the published OpenAPI contract. Checked by `npm run check:capability-ledger` against the live contract, build/capabilities.json and src/index.ts. Bump ledgerVersion and append to changes for every decision change, and link the change from the release notes.",
"schemaVersion": "1.0.0",
"ledgerVersion": 6,
"ledgerVersion": 7,
"source": {
"openapi": {
"url": "https://api.oilpriceapi.com/openapi.json",
Expand Down Expand Up @@ -200,7 +200,9 @@
"status": "partial",
"owner": "mcp-server",
"tools": [
"opa_get_marine_fuels"
"opa_get_marine_fuels",
"opa_get_marine_ports",
"opa_get_marine_fuel_history"
],
"reason": "Latest prices are exposed through the canonical GET /v1/prices/marine-fuels/latest; ports, all-port prices and history are selection 3."
},
Expand Down Expand Up @@ -911,20 +913,22 @@
{
"operation": "GET /v1/marine-ports",
"family": "marine",
"status": "not_exposed",
"disposition": "selected",
"status": "exposed",
"tools": [
"opa_get_marine_ports"
],
"selection": 3,
"owner": "mcp-server",
"reason": "Port directory, so an agent can resolve a port before pricing it. Selection 3."
"owner": "mcp-server"
},
{
"operation": "GET /v1/marine-ports/{port_code}",
"family": "marine",
"status": "not_exposed",
"disposition": "selected",
"status": "exposed",
"tools": [
"opa_get_marine_ports"
],
"selection": 3,
"owner": "mcp-server",
"reason": "Single-port detail with prices. Selection 3."
"owner": "mcp-server"
},
{
"operation": "GET /v1/market-brief",
Expand Down Expand Up @@ -1012,16 +1016,17 @@
"disposition": "selected",
"selection": 3,
"owner": "mcp-server",
"reason": "All-port marine fuel prices; opa_get_marine_fuels returns latest only. Selection 3."
"reason": "Paginated feed of recent marine collections (live 2026-10-15: 62 rows across 8 ports, several per fuel), overlapping opa_get_marine_fuels (latest per series) and opa_get_marine_fuel_history (per-port series). Selection 3; not shipped in the first #125 slice."
},
{
"operation": "GET /v1/prices/marine-fuels/historical",
"family": "marine",
"status": "not_exposed",
"disposition": "selected",
"status": "exposed",
"tools": [
"opa_get_marine_fuel_history"
],
"selection": 3,
"owner": "mcp-server",
"reason": "Marine fuel history; opa_get_marine_fuels returns latest only. Selection 3."
"owner": "mcp-server"
},
{
"operation": "GET /v1/prices/marine-fuels/latest",
Expand Down Expand Up @@ -1494,6 +1499,12 @@
"date": "2026-10-17",
"issue": "https://github.com/OilpriceAPI/mcp-server/issues/124",
"summary": "Selection 2 shipped: GET /v1/futures/brent/historical, GET /v1/futures/wti/historical and GET /v1/futures/natural-gas/historical are exposed by the new opa_get_futures_history tool (from/to up to 730 days, optional contract_month, daily/weekly/monthly, explicit row cap notice). The three canonical spreads rows stay deferred with an updated reason; preview OHLC, intraday and gasoil/carbon history stay deferred under futures-expanded (route policy: product_preview)."
},
{
"ledgerVersion": 7,
"date": "2026-10-17",
"issue": "https://github.com/OilpriceAPI/mcp-server/issues/125",
"summary": "Selection 3, first slice: expose GET /v1/marine-ports and GET /v1/marine-ports/{port_code} through opa_get_marine_ports and GET /v1/prices/marine-fuels/historical through opa_get_marine_fuel_history; move opa_get_marine_fuels from the deprecated /v1/marine-fuels/latest alias to canonical GET /v1/prices/marine-fuels/latest. GET /v1/prices/marine-fuels stays selected and not exposed. History requires a paid subscription or an active trial (API 403 otherwise); the tool reports the API's refusal text."
}
]
}
4 changes: 2 additions & 2 deletions scripts/docker-smoke.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ try {
);
if (
tools.scope !== "read" ||
tools.tools.length !== 34 ||
tools.tools.length !== 36 ||
tools.tools.some((tool) => tool.access === "write")
) {
throw new Error("Container default tool inventory was not exact/read-only.");
Expand Down Expand Up @@ -117,7 +117,7 @@ try {
const capabilities = JSON.parse(readFileSync(join(build, "capabilities.json"), "utf8"));
if (capabilities.package.sourceCommit !== process.env.EXPECTED_SOURCE_COMMIT) throw new Error("capability provenance drift");
if (capabilities.generatedAt !== process.env.EXPECTED_GENERATED_AT) throw new Error("capability timestamp drift");
if (capabilities.tools.length !== 38) throw new Error("capability inventory drift");
if (capabilities.tools.length !== 40) throw new Error("capability inventory drift");
if (existsSync(join(build, "__tests__"))) throw new Error("test artifacts leaked into runtime image");
for (const entry of readdirSync(build)) {
if (/\.(?:test|spec)\./.test(entry)) throw new Error("test artifact leaked into runtime image");
Expand Down
14 changes: 13 additions & 1 deletion scripts/live-contract-catalog.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -91,11 +91,23 @@ export const LIVE_CONTRACT_CATALOG = {
{ args: {} },
),
opa_get_marine_fuels: read(
"/v1/prices/marine-fuels/latest",
"/v1/prices/marine-fuels/latest?limit=100",
"success-envelope",
"conditional",
{ args: {} },
),
opa_get_marine_ports: read(
"/v1/marine-ports/SGSIN",
"success-envelope",
"conditional",
{ args: { port: "SGSIN" } },
),
opa_get_marine_fuel_history: read(
"/v1/prices/marine-fuels/historical?port_code=SGSIN&fuel_type=VLSFO_SGSIN_USD&days=7",
"success-envelope",
"conditional",
{ args: { port_code: "SGSIN", fuel_type: "VLSFO", days: 7 } },
),
opa_get_rig_counts: read(
"/v1/rig-counts/latest",
"success-envelope",
Expand Down
2 changes: 1 addition & 1 deletion scripts/live-contract-matrix-smoke.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { LIVE_CONTRACT_CATALOG } from "./live-contract-catalog.mjs";

const capabilities = JSON.parse(readFileSync(DEFAULT_CAPABILITIES_PATH, "utf8"));
const matrix = buildLiveContractMatrix(capabilities, LIVE_CONTRACT_CATALOG);
if (matrix.totals.tools !== 38 || matrix.tools.length !== capabilities.tools.length) {
if (matrix.totals.tools !== 40 || matrix.tools.length !== capabilities.tools.length) {
throw new Error("matrix did not cover the complete registered tool inventory");
}
for (const mode of ["network-read", "network-write", "non-network"]) {
Expand Down
6 changes: 3 additions & 3 deletions scripts/package-smoke.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -201,7 +201,7 @@ try {
);
if (
readTools.scope !== "read" ||
readTools.tools.length !== 34 ||
readTools.tools.length !== 36 ||
readTools.tools.some((tool) => tool.access === "write")
) {
throw new Error(
Expand Down Expand Up @@ -314,14 +314,14 @@ try {
entryPoint,
baseUrl,
"read",
34,
36,
capabilities.tools.length,
);
await assertProtocolScope(
entryPoint,
baseUrl,
"write",
38,
40,
capabilities.tools.length,
);

Expand Down
6 changes: 3 additions & 3 deletions src/__tests__/capabilityLedger.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -144,19 +144,19 @@ describe("capability ledger", () => {

it("fails when an exposed row is not reached by any MCP source call", () => {
const broken = clone(ledger);
// #123 exposed /v1/prices/historical for real, so the fixture uses a row
// Marine history is exposed, so the fixture uses a row
// no tool reaches yet.
const row = broken.operations.find(
(entry: { operation: string }) =>
entry.operation === "GET /v1/prices/marine-fuels/historical",
entry.operation === "GET /v1/electricity",
);
Object.assign(row, { status: "exposed", tools: ["opa_get_marine_fuels"] });
delete row.disposition;
delete row.reason;
delete row.selection;
const result = evaluate({ ledger: broken });
expect(result.errors.join("\n")).toContain(
"GET /v1/prices/marine-fuels/historical is marked exposed but no MCP source call reaches it",
"GET /v1/electricity is marked exposed but no MCP source call reaches it",
);
});

Expand Down
6 changes: 6 additions & 0 deletions src/__tests__/entitlementMisattribution.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,12 @@ const SITES: Array<{
args: {},
},
{ label: "opa_get_marine_fuels", tool: "opa_get_marine_fuels", args: {} },
{ label: "opa_get_marine_ports (list)", tool: "opa_get_marine_ports", args: {} },
{
label: "opa_get_marine_fuel_history",
tool: "opa_get_marine_fuel_history",
args: { port_code: "SGSIN", fuel_type: "VLSFO", days: 30 },
},
{ label: "opa_get_rig_counts", tool: "opa_get_rig_counts", args: {} },
{ label: "opa_get_drilling", tool: "opa_get_drilling", args: {} },
{
Expand Down
Loading
Loading