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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
7 changes: 6 additions & 1 deletion src/lib/x402/fetch-middleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import {
signPayment,
parsePaymentRequired,
selectAcceptEntry,
isSupportedNetwork,
DEFAULT_PAYMENT_EXPIRY_SECONDS,
type SignerWallet,
type PaymentRequiredAccept,
Expand Down Expand Up @@ -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.
*/
Expand All @@ -540,6 +542,9 @@ function selectAcceptFromToolMeta(
if (preference !== 'auto' && preference !== x402.scheme) {
return undefined;
}
if (!isSupportedNetwork(x402.network)) {
return undefined;
}

return {
scheme: x402.scheme,
Expand Down
26 changes: 22 additions & 4 deletions src/lib/x402/signer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,15 @@ const NETWORKS: Record<string, NetworkConfig> = {
},
};

/**
* 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
// ---------------------------------------------------------------------------
Expand Down Expand Up @@ -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)
);
}

Expand All @@ -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[],
Expand Down Expand Up @@ -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(', ')}`
);
}

Expand Down
12 changes: 12 additions & 0 deletions test/unit/lib/x402/fetch-middleware.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});
});

// ---------------------------------------------------------------------------
Expand Down
47 changes: 47 additions & 0 deletions test/unit/lib/x402/signer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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();
});
});

// ---------------------------------------------------------------------------
Expand Down Expand Up @@ -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');
});
});

// ---------------------------------------------------------------------------
Expand Down
Loading