diff --git a/CHANGELOG.md b/CHANGELOG.md index 4fd3225a..cb593fb5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -36,6 +36,7 @@ Argument completions for prompts and resource templates, tasks on MCP 2026-07-28 ### Fixed +- x402 payments no longer fail with "Unknown network" when a server lists a payment option on a chain mcpc cannot sign for (e.g. Solana) before the Base one: `x402 sign` and `--x402` sessions now pick the first option on a supported network. When there is none, the error lists what the server offered and which networks are supported. - `tools-call`, `prompts-get` and `completion-complete` no longer send arguments other than what you typed: a JSON array passed inline or on stdin is now rejected (MCP arguments must be an object), a `__proto__:=...` argument is no longer dropped, and a number too large to represent (e.g. `n:=1e400`) is kept as a string instead of being sent as `null`. (#448) - Bridge log rotation no longer races the file it rotates: under load the rename could run before the log file was opened or flushed, so the rotation was silently skipped and the first lines of the new file were lost. - Windows sessions no longer pile up duplicate bridge processes when `tasklist` is slow or unavailable. A liveness check that could not run used to report every bridge dead, so each command spawned a replacement without stopping the healthy original; the check now falls back to a signal probe, the old bridge is always asked to shut down, and a bridge that finds another one registered for its session exits on its own. (#427) diff --git a/src/lib/x402/fetch-middleware.ts b/src/lib/x402/fetch-middleware.ts index 052219fc..fe907327 100644 --- a/src/lib/x402/fetch-middleware.ts +++ b/src/lib/x402/fetch-middleware.ts @@ -30,6 +30,7 @@ import { signPayment, parsePaymentRequired, selectAcceptEntry, + isSupportedNetwork, DEFAULT_PAYMENT_EXPIRY_SECONDS, type SignerWallet, type PaymentRequiredAccept, @@ -520,7 +521,8 @@ function extractToolCallName(body: RequestInit['body'] | undefined): string | un * Pick an accept entry from a tool's `_meta.x402` block honoring `schemePreference`. * * Prefers the spec-shaped `accepts[]` array when present; falls back to the flat fields - * only when the preference matches the flat scheme (or the preference is `auto`). + * only when the preference matches the flat scheme (or the preference is `auto`) and the + * flat network is one mcpc can sign for. * Returning undefined defers signing to the 402 fallback path, which re-runs the * selector against the authoritative PAYMENT-REQUIRED header. */ @@ -540,6 +542,9 @@ function selectAcceptFromToolMeta( if (preference !== 'auto' && preference !== x402.scheme) { return undefined; } + if (!isSupportedNetwork(x402.network)) { + return undefined; + } return { scheme: x402.scheme, diff --git a/src/lib/x402/signer.ts b/src/lib/x402/signer.ts index 3254a4f3..76f22ab4 100644 --- a/src/lib/x402/signer.ts +++ b/src/lib/x402/signer.ts @@ -127,6 +127,15 @@ const NETWORKS: Record = { }, }; +/** + * Whether mcpc can sign payments on the given network (CAIP-2 id, e.g. `eip155:8453`). + * Servers may offer the same price on several chains at once (Base, Solana, ...); only + * the networks in `NETWORKS` are supported. + */ +export function isSupportedNetwork(network: string): boolean { + return Object.hasOwn(NETWORKS, network); +} + // --------------------------------------------------------------------------- // Types // --------------------------------------------------------------------------- @@ -221,14 +230,19 @@ function randomPermit2Nonce(): string { // --------------------------------------------------------------------------- function isValidExactAccept(a: PaymentRequiredAccept): boolean { - return a.scheme === 'exact' && Boolean(a.payTo && a.amount && a.network && a.asset); + return ( + a.scheme === 'exact' && + Boolean(a.payTo && a.amount && a.network && a.asset) && + isSupportedNetwork(a.network) + ); } function isValidUptoAccept(a: PaymentRequiredAccept): boolean { return ( a.scheme === 'upto' && Boolean(a.payTo && a.amount && a.network && a.asset) && - Boolean(a.extra?.facilitatorAddress) + Boolean(a.extra?.facilitatorAddress) && + isSupportedNetwork(a.network) ); } @@ -238,6 +252,9 @@ function isValidUptoAccept(a: PaymentRequiredAccept): boolean { * - `auto` → prefer valid `upto`, fallback to valid `exact` * - `upto` → require valid `upto`; undefined if none * - `exact` → require valid `exact`; undefined if none + * + * Entries on networks mcpc cannot sign for (see `isSupportedNetwork`) are skipped, so a + * server that lists a Solana option before the Base one still gets a Base payment. */ export function selectAcceptEntry( accepts: PaymentRequiredAccept[], @@ -291,9 +308,10 @@ export function parsePaymentRequired( const accept = selectAcceptEntry(header.accepts, schemePreference); if (!accept) { const requested = schemePreference === 'auto' ? 'exact or upto' : schemePreference; - const available = header.accepts.map((a) => a.scheme).join(', '); + const offered = header.accepts.map((a) => `${a.scheme} on ${a.network}`).join(', '); throw new ClientError( - `No valid "${requested}" scheme found in PAYMENT-REQUIRED accepts. Available: ${available}` + `No valid "${requested}" scheme on a supported network found in PAYMENT-REQUIRED accepts. ` + + `Offered: ${offered}. Supported networks: ${Object.keys(NETWORKS).join(', ')}` ); } diff --git a/test/unit/lib/x402/fetch-middleware.test.ts b/test/unit/lib/x402/fetch-middleware.test.ts index ed282161..ffdbc139 100644 --- a/test/unit/lib/x402/fetch-middleware.test.ts +++ b/test/unit/lib/x402/fetch-middleware.test.ts @@ -338,6 +338,18 @@ describe('extractAcceptFromPaymentRequired', () => { const result = extractAcceptFromPaymentRequired(uptoOnly, 'exact'); expect(result).toBeUndefined(); }); + + it('skips entries on networks mcpc cannot sign for', () => { + const solanaFirst = { + x402Version: 2, + accepts: [ + { ...EXACT_ACCEPT, network: 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp' }, + EXACT_ACCEPT, + ], + }; + const result = extractAcceptFromPaymentRequired(solanaFirst, 'exact'); + expect(result?.accept.network).toBe(EXACT_ACCEPT.network); + }); }); // --------------------------------------------------------------------------- diff --git a/test/unit/lib/x402/signer.test.ts b/test/unit/lib/x402/signer.test.ts index d15f9070..8b6661df 100644 --- a/test/unit/lib/x402/signer.test.ts +++ b/test/unit/lib/x402/signer.test.ts @@ -94,6 +94,16 @@ const VALID_UPTO_ACCEPT: PaymentRequiredAccept = { }, }; +// Apify's x402 endpoints offer the same price on Base and on Solana; mcpc signs EVM only. +const SOLANA_EXACT_ACCEPT: PaymentRequiredAccept = { + scheme: 'exact', + network: 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp', + amount: '1000000', + asset: 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v', + payTo: 'ECUPkq6DP8VUSEueL4qNznyNDJLwu6PQhnMQ5cuZsTLH', + maxTimeoutSeconds: 60, +}; + const MOCK_WALLET: SignerWallet = { privateKey: '0x1234567890abcdef', address: MOCK_ADDRESS, @@ -166,6 +176,28 @@ describe('selectAcceptEntry', () => { const result = selectAcceptEntry([VALID_EXACT_ACCEPT, secondExact], 'auto'); expect(result?.amount).toBe('1000000'); }); + + it('exact: skips entries on unsupported networks', () => { + const result = selectAcceptEntry([SOLANA_EXACT_ACCEPT, VALID_EXACT_ACCEPT], 'exact'); + expect(result?.network).toBe('eip155:84532'); + }); + + it('upto: skips entries on unsupported networks', () => { + const solanaUpto = { ...VALID_UPTO_ACCEPT, network: SOLANA_EXACT_ACCEPT.network }; + const result = selectAcceptEntry([solanaUpto, VALID_UPTO_ACCEPT], 'upto'); + expect(result?.network).toBe('eip155:84532'); + }); + + it('auto: prefers a supported exact over an upto on an unsupported network', () => { + const solanaUpto = { ...VALID_UPTO_ACCEPT, network: SOLANA_EXACT_ACCEPT.network }; + const result = selectAcceptEntry([solanaUpto, VALID_EXACT_ACCEPT], 'auto'); + expect(result?.scheme).toBe('exact'); + }); + + it('returns undefined when every entry is on an unsupported network', () => { + const result = selectAcceptEntry([SOLANA_EXACT_ACCEPT], 'auto'); + expect(result).toBeUndefined(); + }); }); // --------------------------------------------------------------------------- @@ -227,6 +259,21 @@ describe('parsePaymentRequired', () => { expect(() => parsePaymentRequired(b64, 'upto')).toThrow(ClientError); expect(() => parsePaymentRequired(b64, 'upto')).toThrow('upto'); }); + + it('picks the Base entry when a Solana entry is listed first', () => { + const b64 = buildPaymentRequired([SOLANA_EXACT_ACCEPT, VALID_EXACT_ACCEPT]); + const { accept } = parsePaymentRequired(b64, 'auto'); + expect(accept.network).toBe('eip155:84532'); + }); + + it('throws naming the offered and the supported networks when none is supported', () => { + const b64 = buildPaymentRequired([SOLANA_EXACT_ACCEPT]); + expect(() => parsePaymentRequired(b64, 'exact')).toThrow(ClientError); + expect(() => parsePaymentRequired(b64, 'exact')).toThrow( + 'exact on solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp' + ); + expect(() => parsePaymentRequired(b64, 'exact')).toThrow('Supported networks: eip155:8453'); + }); }); // ---------------------------------------------------------------------------