diff --git a/examples/_shared/recovery-scenarios.mjs b/examples/_shared/recovery-scenarios.mjs index 723f660..afe7678 100644 --- a/examples/_shared/recovery-scenarios.mjs +++ b/examples/_shared/recovery-scenarios.mjs @@ -23,7 +23,7 @@ export async function createLedgerExample() { category: "paper/search MCP local adapter", gate, toolName: "paper_search", - price: 0.15, + price: usd("0.15"), handlerKind: "paper", paymentMetaFactory: async () => null, primeForPaid: async () => { @@ -71,7 +71,7 @@ export async function createMppExample() { category: "scraping/extraction MCP paid step", gate, toolName: "extract_document", - price: 0.2, + price: usd("0.20"), handlerKind: "extract", paymentMetaFactory: async () => { const challenge = await mppAdapter.createChallenge({ @@ -125,7 +125,7 @@ export async function createX402Example() { category: "paid API wrapper MCP", gate, toolName: "partner_api_lookup", - price: 0.3, + price: usd("0.30"), handlerKind: "api", paymentMetaFactory: async (mode = "success") => { const challenge = await x402Adapter.createChallenge({ diff --git a/examples/advanced-server/index.mjs b/examples/advanced-server/index.mjs index b7cb2dc..33e1b5b 100644 --- a/examples/advanced-server/index.mjs +++ b/examples/advanced-server/index.mjs @@ -290,23 +290,23 @@ server.tool( caller_id: z.string().optional().describe("Caller ID (default: demo-user)"), }, async ({ amount_usd, caller_id = "demo-user" }) => { - await gate.ledger.credit(caller_id, usd(amount_usd), { + await gate.ledger.credit(caller_id, usd(amount_usd), { source: "manual", reference: `topup-${Date.now()}`, }); const balance = await gate.ledger.getBalance(caller_id); - return { - content: [ - { - type: "text", - text: JSON.stringify( - { added_usd: amount_usd, new_balance_usd: toNumber(balance) }, - null, - 2, - ), - }, - ], - }; + return { + content: [ + { + type: "text", + text: JSON.stringify( + { added_usd: amount_usd, new_balance_usd: toNumber(balance) }, + null, + 2, + ), + }, + ], + }; }, ); diff --git a/examples/x402-testnet-recovery/_shared.mjs b/examples/x402-testnet-recovery/_shared.mjs index b2a11f9..f9b11f5 100644 --- a/examples/x402-testnet-recovery/_shared.mjs +++ b/examples/x402-testnet-recovery/_shared.mjs @@ -1,16 +1,65 @@ +import { existsSync, readFileSync } from "node:fs"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; import { ToolGate, X402RailAdapter, createMcpAdapter, + usd, } from "../../dist/index.js"; +const currentDir = path.dirname(fileURLToPath(import.meta.url)); + +loadEnvFile(path.resolve(currentDir, "../../.env")); +loadEnvFile(path.resolve(currentDir, ".env")); + export const callerId = "x402-testnet-caller"; export const publisherKey = "tg_x402_testnet"; export const toolName = "partner_api_lookup"; export const defaultAmount = 0.3; +function loadEnvFile(filePath) { + if (!existsSync(filePath)) { + return; + } + + const source = readFileSync(filePath, "utf8"); + for (const line of source.split(/\r?\n/)) { + const trimmed = line.trim(); + if (!trimmed || trimmed.startsWith("#")) { + continue; + } + + const separator = trimmed.indexOf("="); + if (separator === -1) { + continue; + } + + const key = trimmed.slice(0, separator).trim(); + let value = trimmed.slice(separator + 1).trim(); + if (!key || process.env[key]) { + continue; + } + + if ( + (value.startsWith('"') && value.endsWith('"')) || + (value.startsWith("'") && value.endsWith("'")) + ) { + value = value.slice(1, -1); + } + + process.env[key] = value; + } +} + export function printSummary(summary) { - process.stdout.write(`${JSON.stringify(summary, null, 2)}\n`); + process.stdout.write( + `${JSON.stringify( + summary, + (_key, value) => (typeof value === "bigint" ? value.toString() : value), + 2, + )}\n`, + ); } export function parseJsonEnv(name) { @@ -56,7 +105,7 @@ export function createRegistration(gate, duplicateKeys) { }, required: ["requestId", "query"], }, - price: defaultAmount, + price: usd("0.30"), onPaymentFailed: "fallback", idempotencyKey: (args, currentCallerId) => `${toolName}:${currentCallerId}:${String(args.requestId)}`, @@ -97,7 +146,7 @@ export function createBlockingRegistration(gate) { }, required: ["requestId", "query"], }, - price: defaultAmount, + price: usd("0.30"), onPaymentFailed: "block", idempotencyKey: (args, currentCallerId) => `${toolName}_blocking:${currentCallerId}:${String(args.requestId)}`, diff --git a/integrations/firecrawl-mcp-toolgate/mcp-e2e-server.mjs b/integrations/firecrawl-mcp-toolgate/mcp-e2e-server.mjs index bd82e36..be2d462 100644 --- a/integrations/firecrawl-mcp-toolgate/mcp-e2e-server.mjs +++ b/integrations/firecrawl-mcp-toolgate/mcp-e2e-server.mjs @@ -25,7 +25,7 @@ const mcp = createMcpAdapter(gate, { mcp.paidTool("firecrawl_scrape", { description: "Paid wrapper for the Firecrawl MCP scrape tool", inputSchema: firecrawlScrapeInputSchema, - price: 0.25, + price: usd("0.25"), onPaymentFailed: "fallback", idempotencyKey: createFirecrawlIdempotencyKey, onDuplicateDetected: async (_input, record) => { diff --git a/integrations/firecrawl-mcp-toolgate/scenario-fake.mjs b/integrations/firecrawl-mcp-toolgate/scenario-fake.mjs index 415f8df..0671bbc 100644 --- a/integrations/firecrawl-mcp-toolgate/scenario-fake.mjs +++ b/integrations/firecrawl-mcp-toolgate/scenario-fake.mjs @@ -24,7 +24,7 @@ function createRegisteredFirecrawlTool({ gate, transport, duplicateKeys }) { mcp.paidTool("firecrawl_scrape", { description: "Paid wrapper for the Firecrawl MCP scrape tool", inputSchema: firecrawlScrapeInputSchema, - price: 0.25, + price: usd("0.25"), onPaymentFailed: "fallback", idempotencyKey: createFirecrawlIdempotencyKey, onDuplicateDetected: async (_input, record) => { diff --git a/integrations/firecrawl-mcp-toolgate/scenario-live.mjs b/integrations/firecrawl-mcp-toolgate/scenario-live.mjs index 3f6fa50..b4dc779 100644 --- a/integrations/firecrawl-mcp-toolgate/scenario-live.mjs +++ b/integrations/firecrawl-mcp-toolgate/scenario-live.mjs @@ -31,7 +31,7 @@ function createRegisteredFirecrawlTool({ gate, transport, duplicateKeys }) { mcp.paidTool("firecrawl_scrape", { description: "Paid wrapper for the Firecrawl MCP scrape tool", inputSchema: firecrawlScrapeInputSchema, - price: 0.25, + price: usd("0.25"), onPaymentFailed: "fallback", idempotencyKey: createFirecrawlIdempotencyKey, onDuplicateDetected: async (_input, record) => { @@ -174,6 +174,27 @@ try { const summary = await runLiveScenario(); process.stdout.write(`${JSON.stringify(summary, null, 2)}\n`); } catch (error) { - process.stderr.write(`${(error && error.message) || String(error)}\n`); - process.exitCode = 1; + const message = (error && error.message) || String(error); + if (message.includes("Missing FIRECRAWL_API_KEY")) { + process.stdout.write( + `${JSON.stringify( + { + integration: "firecrawl-mcp-toolgate-live", + blocked: true, + blocker: { + reason: "missing_env", + required: ["FIRECRAWL_API_KEY"], + details: + "Export FIRECRAWL_API_KEY in the terminal before running the live Firecrawl scenario.", + }, + }, + null, + 2, + )}\n`, + ); + process.exitCode = 0; + } else { + process.stderr.write(`${message}\n`); + process.exitCode = 1; + } } diff --git a/integrations/stripe-test-mode/scenario.mjs b/integrations/stripe-test-mode/scenario.mjs index eabcc69..b3280b6 100644 --- a/integrations/stripe-test-mode/scenario.mjs +++ b/integrations/stripe-test-mode/scenario.mjs @@ -9,6 +9,8 @@ import { ToolGate, StripeAdapter, createWebhookHandler, + toNumber, + usd, } from "../../dist/index.js"; const publisherKey = "tg_stripe_test_mode"; @@ -53,7 +55,13 @@ function loadEnvFile(filePath) { } function printSummary(summary) { - process.stdout.write(`${JSON.stringify(summary, null, 2)}\n`); + process.stdout.write( + `${JSON.stringify( + summary, + (_key, value) => (typeof value === "bigint" ? value.toString() : value), + 2, + )}\n`, + ); } function runCommand(command, args, env = process.env) { @@ -162,7 +170,7 @@ async function runScenario() { const paidLookup = gate.paidAction({ name: "premium_lookup", description: "Stripe recovery acceptance scenario", - price: 0.25, + price: usd("0.25"), onPaymentFailed: "block", idempotencyKey: (input, currentCallerId) => `premium_lookup:${currentCallerId}:${String(input.requestId)}`, @@ -332,7 +340,7 @@ async function runScenario() { assert.equal(webhook.result.processed, true); assert.equal(webhook.result.duplicate, undefined); - assert.equal(await gate.ledger.getBalance(callerId), 1); + assert.equal(toNumber(await gate.ledger.getBalance(callerId)), 1); assert.equal(retrievedSession.id, checkoutSession.id); const paidInput = { @@ -348,7 +356,7 @@ async function runScenario() { assert.deepEqual(duplicateResult, paidResult); const balanceAfterExecution = await gate.ledger.getBalance(callerId); - assert.equal(balanceAfterExecution, 0.75); + assert.equal(toNumber(balanceAfterExecution), 0.75); if (paidTrace) { paidTrace.receiptId = checkoutSession.id; @@ -379,7 +387,10 @@ async function runScenario() { const finalTrace = await gate.traces.findByIdempotencyKey(paidTraceKey); assert.equal(duplicateWebhookResult.duplicate, true); - assert.equal(balanceAfterDuplicateWebhook, balanceBeforeDuplicateWebhook); + assert.equal( + toNumber(balanceAfterDuplicateWebhook), + toNumber(balanceBeforeDuplicateWebhook), + ); assert.equal(finalTrace?.provider?.correlationId, checkoutSession.id); assert.equal(finalTrace?.provider?.traceId ?? null, paymentIntentId); @@ -420,8 +431,10 @@ async function runScenario() { { name: "duplicate_webhook", result: duplicateWebhookResult, - balanceBeforeDuplicateWebhook, - balanceAfterDuplicateWebhook, + balanceBeforeDuplicateWebhook: toNumber( + balanceBeforeDuplicateWebhook, + ), + balanceAfterDuplicateWebhook: toNumber(balanceAfterDuplicateWebhook), }, ], notes: { diff --git a/package.json b/package.json index 1e852f3..1b1fd9e 100644 --- a/package.json +++ b/package.json @@ -37,7 +37,7 @@ "scenario:mpp": "npm run build && node examples/mcp-mpp-recovery/scenario.mjs", "scenario:x402": "npm run build && node examples/mcp-x402-experimental/scenario.mjs", "scenario:x402-testnet:challenge": "npm run build && node examples/x402-testnet-recovery/challenge.mjs", - "scenario:x402-testnet:sign": "npm run build && node examples/x402-testnet-recovery/sign-payload.mjs", + "scenario:x402-testnet:sign": "npm run build && node examples/x402-testnet-recovery/challenge.mjs | node examples/x402-testnet-recovery/sign-payload.mjs", "scenario:x402-testnet": "npm run build && node integrations/x402-testnet/scenario.mjs", "test": "npm run build && node --test --test-force-exit src/__tests__/paidTool.test.mjs src/__tests__/mcp-adapter.test.mjs src/__tests__/stripe.test.mjs src/__tests__/webhook-handler.test.mjs src/__tests__/db-ledger.test.mjs src/__tests__/rail-adapter.test.mjs src/__tests__/policy.test.mjs src/__tests__/protocol-compliance.test.mjs src/__tests__/idempotency.test.mjs src/__tests__/trace.test.mjs src/__tests__/firecrawl-integration.test.mjs src/__tests__/local-first.test.mjs", "prepublishOnly": "npm run typecheck && npm test" diff --git a/src/__tests__/firecrawl-integration.test.mjs b/src/__tests__/firecrawl-integration.test.mjs index 8111a23..7f857bb 100644 --- a/src/__tests__/firecrawl-integration.test.mjs +++ b/src/__tests__/firecrawl-integration.test.mjs @@ -25,7 +25,7 @@ function createRegisteredFirecrawlTool({ gate, transport, duplicateKeys }) { mcp.paidTool("firecrawl_scrape", { description: "Paid wrapper for the Firecrawl MCP scrape tool", inputSchema: firecrawlScrapeInputSchema, - price: 0.25, + price: usd("0.25"), onPaymentFailed: "fallback", idempotencyKey: createFirecrawlIdempotencyKey, onDuplicateDetected: async (_input, record) => {