Skip to content
Merged
Show file tree
Hide file tree
Changes from 15 commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
9204a92
feat(x402): support upto scheme (Permit2 permitWitnessTransferFrom)
MQ37 May 17, 2026
0112153
feat(x402): scheme-aware debug logs and 6-decimal USD precision
MQ37 May 20, 2026
459bfba
feat(sessions): mark x402-authenticated sessions in listings
MQ37 May 20, 2026
0d65e96
feat(x402): pin scheme preference on a session via --x402-scheme
MQ37 May 20, 2026
e374380
fix(x402): honor schemePreference on proactive sign and tool-result r…
MQ37 May 21, 2026
b776b21
docs(x402): document upto scheme, session preferences and flags in RE…
MQ37 May 21, 2026
0cc5a6c
fix(x402): preserve schemePreference on automatic bridge failover
MQ37 May 21, 2026
3401fd7
fix(x402): type-declare schemePreference on bulk-connect options; dro…
MQ37 May 21, 2026
adb6a6b
refactor(x402)!: collapse --x402-scheme into --x402 [auto|upto|exact]
MQ37 May 21, 2026
52883eb
fix(cli): use ClientError for CLI validation so users see a clean mes…
MQ37 May 21, 2026
c626f14
Merge branch 'main' into feat/x402-upto
MQ37 May 25, 2026
0b960df
chore(x402): remove unreferenced docs/examples/sign-x402.ts
MQ37 May 25, 2026
203fa8c
Delete X402_UPTO_INVESTIGATION.md
MQ37 May 25, 2026
31891a3
Update regression target description in test file
MQ37 May 25, 2026
fc24cb7
refactor(x402): extract DEFAULT_PAYMENT_EXPIRY_SECONDS constant
MQ37 May 25, 2026
467d836
refactor(x402): trim dead migration code and tighten bridge parse
MQ37 May 25, 2026
8497cb3
feat(cli): allow --x402 flag in any position relative to positionals
MQ37 May 25, 2026
03278ae
style: apply prettier to bridge --x402 parse and normaliseLegacyX402
MQ37 May 25, 2026
082ecba
refactor(x402)!: drop normaliseLegacyX402 on-read migration
MQ37 May 25, 2026
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
6 changes: 5 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,17 +17,21 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

- `mcpc connect` (with no arguments) now auto-discovers standard MCP config files (`.mcp.json`, `mcp.json`, `.cursor/mcp.json`, `.vscode/mcp.json`, `~/.claude.json`, Claude Desktop, Windsurf, Kiro, etc.) in the current directory and home directory, and connects every server defined across them. Entries with duplicate session names are deduplicated (project-scoped files win over global ones). VS Code's `"servers"` key is also supported.
- `mcpc connect` auto-connects to `mcp.apify.com` as `@apify` when the `APIFY_API_TOKEN` environment variable is set, using it as a Bearer token. Existing live sessions are reused without restart.
- `mcpc x402 sign` supports the x402 `upto` scheme alongside `exact`. Use `--scheme <auto|upto|exact>` to pick a preference (default `auto` prefers `upto`, falls back to `exact`). The signer auto-grants a one-time `USDC.approve(PERMIT2, MAX_UINT256)` allowance on first upto sign; pass `--no-approve` to skip.
- `mcpc connect --x402 [auto|upto|exact]` enables x402 auto-payment with an optional scheme preference. Bare `--x402` defaults to `auto` (prefer upto, fall back to exact). The choice is persisted to `sessions.json` and reused on `mcpc restart`. Use `--x402=<scheme>` when the flag is followed by positional arguments to avoid Commander's greedy parsing of the next token.
- Sessions using x402 auto-payment now show a yellow `[x402]` marker in session listings, alongside the existing OAuth and proxy markers.

### Changed

- Stdio (command-based) config entries are now skipped by default when connecting from a config file (`mcpc connect <file>`). Pass `--stdio` to include them. Single-entry connects (`mcpc connect file:entry @session`) are not affected.
- x402 debug logs now announce the selected scheme (`scheme=upto` / `scheme=exact`) up front and print USD amounts with 6-decimal precision (USDC atomic unit). Enable with `--verbose` or `MCPC_VERBOSE=1`.
- **Breaking:** `mcpc connect --json` now always returns an array of `InitializeResult` objects (extended with `toolNames` and `_mcpc` metadata), regardless of whether you connect a single server, a config file, or auto-discover all configs. Skipped/failed entries carry `_mcpc.status` (`created` | `active` | `failed` | `skipped`) and `_mcpc.skipReason` / `_mcpc.error`. The previous wrapper-object shapes (`{configFile, results, skipped}` and `{discovered, results, skipped}`) have been removed.
- `tools-call --task` now prints the task ID and recovery commands when interrupted with Ctrl+C, so you can fetch or cancel the server-side task later
- Human-mode `tools-call` / `tasks-result` output no longer prints the **Structured content** section when **Content** already has at least one visible block. The JSON dump was redundant verbose output (especially for LLMs) whenever a server returned both. The section is still shown when `content` is empty or contains only a JSON-dump duplicate of `structuredContent`; use `--json` to always get the full payload

### Fixed

- `mcpc connect` and `mcpc restart` no longer fail with `ENOENT` when the macOS Keychain prompts for a password. Credentials are now read from the keychain *before* the bridge process is spawned, so the bridge's IPC startup timer no longer races a foreground password dialog (#55)
- `mcpc connect` and `mcpc restart` no longer fail with `ENOENT` when the macOS Keychain prompts for a password. Credentials are now read from the keychain _before_ the bridge process is spawned, so the bridge's IPC startup timer no longer races a foreground password dialog (#55)
- Background auto-reconnect now correctly marks sessions as `unauthorized` when the server returns 401/403, instead of leaving them stuck in `connecting` after the bridge crashed on an unhandled rejection
- Sessions using a static bearer token (via `--header "Authorization: ..."`) no longer flip between `unauthorized` and `connecting` on every `mcpc` invocation — they stay `unauthorized` until you `mcpc login` or reconnect. OAuth-profile sessions still auto-retry because tokens may have been refreshed by another session
- Stdio servers no longer fail silently: the bridge now captures the child's stderr, writes it to `~/.mcpc/logs/bridge-<session>.log`, and appends a tail of the most recent lines to `mcpc connect` errors. This makes it obvious when a stdio server crashes on startup due to e.g. missing TLS trust (`NODE_EXTRA_CA_CERTS`), missing proxy vars, or missing credentials (#195)
Expand Down
63 changes: 40 additions & 23 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -349,7 +349,7 @@ and auto-reconnects on network failures or its own crashes (10s cooldown on fail
**Session states:**

| State | Meaning |
|------------------| ----------------------------------------------------------------------------------------------- |
| ---------------- | ----------------------------------------------------------------------------------------------- |
| 🟢`live` | Bridge process running and server responding |
| 🟡`connecting` | Initial bridge startup in progress (`mcpc connect`) |
| 🟡`reconnecting` | Bridge crashed or lost auth; auto-reconnecting in the background |
Expand Down Expand Up @@ -484,13 +484,13 @@ always win over stored profiles, and credentials are never silently downgraded.
is missing, expired, or invalid, `mcpc` fails with an error that includes the right
`mcpc login` command to recover.

| Flag | Behavior |
| ------------------------------- | ------------------------------------------------------------------------------------------- |
| `--header "Authorization: ..."` | Use explicit header; skip OAuth auto-detection. Cannot combine with `--profile`. |
| `--profile <name>` | Require the named profile to exist. |
| `--no-profile` | Connect anonymously even if a `default` profile exists. |
| `--x402` | Skip OAuth auto-detection; use x402 payments instead. Combine with `--profile` to use both. |
| _(none)_ | Use `default` profile if it exists; otherwise connect anonymously. |
| Flag | Behavior |
| ------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- |
| `--header "Authorization: ..."` | Use explicit header; skip OAuth auto-detection. Cannot combine with `--profile`. |
| `--profile <name>` | Require the named profile to exist. |
| `--no-profile` | Connect anonymously even if a `default` profile exists. |
| `--x402 [scheme]` | Skip OAuth auto-detection; use x402 payments instead. Optional scheme: `auto` (default), `upto`, `exact`. Combine with `--profile` to use both. |
| _(none)_ | Use `default` profile if it exists; otherwise connect anonymously. |

Config file headers (from `--config`) apply to servers loaded from that file.

Expand Down Expand Up @@ -680,13 +680,20 @@ This is entirely **opt-in**: existing functionality is unaffected unless you exp

### How it works

1. **Server returns HTTP 402** with a `PAYMENT-REQUIRED` header describing the price and payment details.
2. `mcpc` parses the header, signs an [EIP-3009](https://eips.ethereum.org/EIPS/eip-3009) `TransferWithAuthorization` using your local wallet.
3. `mcpc` retries the request with a `PAYMENT-SIGNATURE` header containing the signed payment.
The x402 protocol defines different payment **schemes**:

- **`exact`** (Standard EIP-3009): The client signs an exact `TransferWithAuthorization` on USDC. Settles on-chain immediately at call-time.
- **`upto`** (Permit2): The client signs a maximum authorization cap using Uniswap's `Permit2` witness signatures. The facilitator verifies the signature off-chain immediately, and settles the actual accumulated usage later (asynchronously).

Regardless of the scheme, the general flow is:

1. **Server returns HTTP 402** with a `PAYMENT-REQUIRED` header advertising its supported schemes and details.
2. `mcpc` parses the header, picks the best scheme, and signs the payment payload using your local wallet.
- For `upto`, `mcpc` automatically checks and grants the one-time on-chain Permit2 allowance if needed (requires a small native ETH float for gas).
3. `mcpc` retries the request with a `PAYMENT-SIGNATURE` header containing the signed payload.
4. The server verifies the signature and fulfills the request.

For tools that advertise pricing in their `_meta.x402` metadata, `mcpc` can **proactively sign** payments
on the first request, avoiding the 402 round-trip entirely.
For tools that advertise pricing in their `_meta.x402` metadata, `mcpc` can **proactively sign** payments on the first request, avoiding the 402 round-trip entirely. This path is fully scheme-aware and respects your configured session preference.

### Wallet setup

Expand Down Expand Up @@ -730,31 +737,41 @@ mcpc x402 sign <base64-payment-required> --amount 1.00 --expiry 3600 --json

**Options:**

| Option | Description |
| -------------------- | ------------------------------------------------------------- |
| `--amount <usd>` | Override the payment amount in USD (e.g. `0.50` for $0.50) |
| `--expiry <seconds>` | Override the payment expiry in seconds from now (e.g. `3600`) |
| Option | Description |
| -------------------- | ----------------------------------------------------------------------- |
| `--amount <usd>` | Override the payment amount in USD (e.g. `0.50` for $0.50) |
| `--expiry <seconds>` | Override the payment expiry in seconds from now (e.g. `3600`) |
| `--scheme <val>` | Scheme preference: `auto` (default, upto > exact), `upto`, or `exact` |
| `--no-approve` | For `upto`, skip checking and auto-approving on-chain Permit2 allowance |

The command outputs the signed `PAYMENT-SIGNATURE` header value and an MCP config snippet
that can be used directly with other MCP clients.

### Using x402 with MCP servers

Pass the `--x402` flag when connecting to a session or running direct commands:
Pass the `--x402` flag when connecting to a session. It accepts an optional scheme preference
(`auto`, `upto`, or `exact`); bare `--x402` defaults to `auto`.

```bash
# Create a session with x402 payment support
# Create a session with x402 payment support (auto picks the best advertised scheme)
mcpc connect mcp.apify.com @apify --x402

# The session now automatically handles 402 responses
# Pin a specific scheme — value goes after the flag
mcpc connect mcp.apify.com @apify --x402 exact

# When --x402 precedes positional args, use the equals form to avoid Commander's
# greedy [optional] argument parser eating the URL/session as the value.
mcpc connect --x402=upto mcp.apify.com @apify

# The session now automatically handles 402 responses using your preference
mcpc @apify tools-call expensive-tool query:="hello"

# Restart a session with x402 enabled
mcpc @apify restart --x402
# Restart re-uses the saved scheme from sessions.json — no need to repeat the flag
mcpc @apify restart
```

When `--x402` is active, a fetch middleware wraps all HTTP requests to the MCP server.
If any request returns HTTP 402, the middleware transparently signs and retries.
If any request returns HTTP 402, the middleware transparently signs and retries. Your scheme preference is persisted in `sessions.json` and reused on every reconnect or restart.

### Supported networks

Expand Down
30 changes: 21 additions & 9 deletions src/bridge/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ import { createServer, type Server as NetServer, type Socket } from 'net';
import { unlink } from 'fs/promises';
import { createMcpClient, CreateMcpClientOptions } from '../core/index.js';
import type { McpClient } from '../core/index.js';
import type { ServerConfig, IpcMessage, LoggingLevel } from '../lib/index.js';
import { KEEPALIVE_INTERVAL_MS } from '../lib/types.js';
import type { ServerConfig, IpcMessage, LoggingLevel, X402SchemePreference } from '../lib/index.js';
import { KEEPALIVE_INTERVAL_MS, X402_SCHEME_PREFERENCES } from '../lib/types.js';
import { createLogger, setVerbose, initFileLogger, closeFileLogger } from '../lib/index.js';
import {
fileExists,
Expand Down Expand Up @@ -74,7 +74,8 @@ interface BridgeOptions {
profileName?: string; // Auth profile name for token refresh
proxyConfig?: ProxyConfig; // Proxy server configuration
mcpSessionId?: string; // MCP session ID for resumption (Streamable HTTP only)
x402?: boolean; // Enable x402 auto-payment
/** x402 scheme preference; presence enables x402 auto-payment, absence disables. */
x402?: X402SchemePreference;
insecure?: boolean; // Skip TLS certificate verification
}

Expand Down Expand Up @@ -615,6 +616,7 @@ class BridgeProcess {
wallet,
getToolByName,
paymentCache: this.x402PaymentCache,
...(this.options.x402 && { schemePreference: this.options.x402 }),
});
}

Expand Down Expand Up @@ -1111,7 +1113,7 @@ class BridgeProcess {
const paymentRequired = extractPaymentRequiredFromResult(toolResult);
if (!paymentRequired) return { handled: false };

const parsed = extractAcceptFromPaymentRequired(paymentRequired);
const parsed = extractAcceptFromPaymentRequired(paymentRequired, this.options.x402);
if (!parsed) {
logger.warn('Payment-required tool result but could not extract supported payment terms');
return { handled: false };
Expand All @@ -1129,7 +1131,7 @@ class BridgeProcess {
});
this.x402PaymentCache.signature = signed.paymentSignatureBase64;
logger.debug(
`Fresh payment signed for retry: $${signed.amountUsd.toFixed(4)} to ${signed.to} on ${signed.networkLabel}`
`Fresh payment signed for retry: $${signed.amountUsd.toFixed(6)} to ${signed.to} on ${signed.networkLabel}`
);
} catch (signError) {
logger.warn('Failed to sign fresh payment for 402 retry:', signError);
Expand Down Expand Up @@ -1613,7 +1615,7 @@ async function main(): Promise<void> {

if (args.length < 2) {
console.error(
'Usage: mcpc-bridge <sessionName> <transportConfigJson> [--verbose] [--profile <name>] [--proxy-host <host>] [--proxy-port <port>] [--mcp-session-id <id>] [--x402] [--insecure]'
'Usage: mcpc-bridge <sessionName> <transportConfigJson> [--verbose] [--profile <name>] [--proxy-host <host>] [--proxy-port <port>] [--mcp-session-id <id>] [--x402 <auto|upto|exact>] [--insecure]'
);
process.exit(1);
}
Expand Down Expand Up @@ -1648,8 +1650,18 @@ async function main(): Promise<void> {
mcpSessionId = args[mcpSessionIdIndex + 1];
}

// Parse --x402 flag (for x402 payment signing)
const x402 = args.includes('--x402');
// Parse `--x402 <scheme>`. The CLI always spawns the bridge with an explicit
// scheme value; if a bare `--x402` slips through (no value, or invalid value)
// we default to `auto` to keep the spawn surface tolerant.
let x402: X402SchemePreference | undefined;
const x402Index = args.indexOf('--x402');
if (x402Index !== -1) {
const value = args[x402Index + 1];
x402 =
value !== undefined && (X402_SCHEME_PREFERENCES as readonly string[]).includes(value)
? (value as X402SchemePreference)
: 'auto';
}

// Parse --insecure flag (skip TLS certificate verification)
const insecure = args.includes('--insecure');
Expand All @@ -1674,7 +1686,7 @@ async function main(): Promise<void> {
bridgeOptions.mcpSessionId = mcpSessionId;
}
if (x402) {
bridgeOptions.x402 = true;
bridgeOptions.x402 = x402;
}
if (insecure) {
bridgeOptions.insecure = true;
Expand Down
15 changes: 10 additions & 5 deletions src/cli/commands/sessions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,12 @@ import {
redactHeaders,
} from '../../lib/index.js';
import { DISCONNECTED_THRESHOLD_MS } from '../../lib/types.js';
import type { ServerConfig, ProxyConfig, ServerDetails } from '../../lib/types.js';
import type {
ServerConfig,
ProxyConfig,
ServerDetails,
X402SchemePreference,
} from '../../lib/types.js';
import {
formatOutput,
formatSuccess,
Expand Down Expand Up @@ -299,7 +304,7 @@ export async function connectSession(
noProfile?: boolean;
proxy?: string;
proxyBearerToken?: string;
x402?: boolean;
x402?: X402SchemePreference;
insecure?: boolean;
skipDetails?: boolean;
quiet?: boolean;
Expand Down Expand Up @@ -461,7 +466,7 @@ export async function connectSession(
server: sessionTransportConfig,
...(profileName && { profileName }),
...(proxyConfig && { proxy: proxyConfig }),
...(options.x402 && { x402: true }),
...(options.x402 && { x402: options.x402 }),
...(options.insecure && { insecure: true }),
// Clear any previous error status (unauthorized, expired) when reconnecting
...(isReconnect && { status: 'active' }),
Expand Down Expand Up @@ -498,7 +503,7 @@ export async function connectSession(
bridgeOptions.proxyConfig = proxyConfig;
}
if (options.x402) {
bridgeOptions.x402 = true;
bridgeOptions.x402 = options.x402;
}
if (options.insecure) {
bridgeOptions.insecure = true;
Expand Down Expand Up @@ -1019,7 +1024,7 @@ type BulkConnectOptions = {
proxy?: string;
proxyBearerToken?: string;
stdio?: boolean;
x402?: boolean;
x402?: X402SchemePreference;
insecure?: boolean;
};

Expand Down
Loading
Loading