Skip to content
Open
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
22 changes: 21 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -754,6 +754,17 @@ Two schemes are supported, both signed by your local wallet:

Flow: server returns HTTP 402 with a `PAYMENT-REQUIRED` header → `mcpc` picks the best scheme per your preference, signs, and retries with `PAYMENT-SIGNATURE` → server verifies and fulfills. Tools that advertise pricing in `_meta.x402` are signed proactively, skipping the 402 round-trip.

For guarded autonomous spending, `--x402-policy agent-guild` waits for the authoritative 402,
buys one short-lived [Agent Guild](https://agent-guild-5d5r.onrender.com) AGPD-1 decision,
and verifies its Ed25519 signature, issuer, freshness, policy thresholds, and exact payment fields
locally. The protected payment is never signed unless that credential says `allow`. Any decision
or verification failure blocks the payment.

Guarded mode requires `--x402-max-amount <atomic>` as a local ceiling for the protected payment.
The decision is itself a paid x402 call on Base mainnet, but mcpc separately pins that purchase
to exact scheme, Base USDC, the Guild treasury and a $0.01 maximum; redirects fail closed. The
local wallet must hold enough Base USDC for both the decision and the protected tool call.

### Wallet setup

`mcpc` stores a single wallet in `~/.mcpc/wallets.json` (file permissions `0600`).
Expand Down Expand Up @@ -819,6 +830,10 @@ mcpc connect mcp.apify.com @apify --x402
mcpc connect --x402 upto mcp.apify.com @apify
mcpc connect mcp.apify.com @apify --x402 exact

# Require a signed, exact pre-payment decision and cap each tool payment at 1 USDC
mcpc connect mcp.apify.com @apify --x402 exact --x402-policy agent-guild \
--x402-max-amount 1000000

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

Expand All @@ -827,7 +842,12 @@ 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. Your scheme preference is persisted in `sessions.json` and reused on every reconnect or restart.
If any request returns HTTP 402, the middleware transparently signs and retries. When a payment
policy is enabled, proactive signing is disabled until the authoritative 402 supplies the exact
resource URL; a signature already approved for the immediate retry may still be reused. Your
guarded retry receives its signature through call-local async state, so concurrent calls cannot
consume each other's approvals. Your scheme preference, payment policy and ceiling are persisted
in `sessions.json` and reused on every reconnect or restart.

### Supported networks

Expand Down
5 changes: 5 additions & 0 deletions docs/REFERENCE.md
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,8 @@ Options:
--stdio Launch all local stdio servers from selected config files
--protocol-version <version> Pin the MCP protocol version (see below)
--x402 [scheme] Enable x402 auto-payment (see below)
--x402-policy <policy> Authorize each payment with a signed policy decision
--x402-max-amount <atomic> Maximum atomic token amount for a guarded payment
--json Output in JSON format

Server formats:
Expand Down Expand Up @@ -152,6 +154,9 @@ Protocol version:
x402 payments (experimental):
--x402 pays for paid tool calls from the wallet set up with mcpc x402.
Schemes: auto (default, prefers upto), upto, exact.
--x402-policy agent-guild buys and locally verifies a short-lived signed
Agent Guild decision bound to the exact payment before the wallet signs.
Guarded mode also requires --x402-max-amount <atomic> as a local spend ceiling.

Output:
For a single server, shows session, server info, capabilities, and tools.
Expand Down
6 changes: 5 additions & 1 deletion skills/mcpc/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -243,7 +243,11 @@ mcpc @apify skills-get <name> --raw # print the SKILL.md markdown (pipe to a

(`--no-profile`, `--stdio`, `--proxy`, and `-H` are options of `connect`, not global flags.)

`mcpc` also has experimental `--x402` auto-payment for paid MCP tools — see `mcpc help x402`.
`mcpc` also has experimental `--x402` auto-payment for paid MCP tools. For autonomous wallets,
prefer `--x402-policy agent-guild --x402-max-amount <atomic>`: it buys and locally verifies a
short-lived signed decision bound to the exact payment, enforces the local amount ceiling before
signing, and fails closed if the decision cannot be verified.
See `mcpc help x402`.

## Debugging

Expand Down
99 changes: 96 additions & 3 deletions src/bridge/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,15 @@ import type {
IpcMessage,
LoggingLevel,
X402SchemePreference,
X402PaymentPolicyPreset,
ServerDetails,
} from '../lib/index.js';
import {
KEEPALIVE_INTERVAL_MILLIS,
MAX_PERSISTED_INSTRUCTIONS_CHARS,
TRIMMED_INSTRUCTIONS_NOTICE,
X402_SCHEME_PREFERENCES,
X402_PAYMENT_POLICY_PRESETS,
} from '../lib/types.js';
import { createLogger, setVerbose, initFileLogger, closeFileLogger } from '../lib/index.js';
import {
Expand Down Expand Up @@ -74,6 +76,7 @@ import type { ProxyConfig } from '../lib/types.js';
// x402 modules pull in the bundled viem (~1 MB of crypto code) — import types
// only here and load the implementations lazily at the x402-gated call sites.
import type { X402PaymentCache } from '../lib/x402/fetch-middleware.js';
import type { X402PaymentPolicy, X402PaymentSignatureScope } from '../lib/x402/payment-policy.js';
import type { SignerWallet } from '../lib/x402/signer.js';
import type { FetchLike } from '@modelcontextprotocol/client';

Expand All @@ -96,6 +99,10 @@ interface BridgeOptions {
protocolVersion?: string; // Protocol version negotiated by the resumed session (only set with mcpSessionId)
/** x402 scheme preference; presence enables x402 auto-payment, absence disables. */
x402?: X402SchemePreference;
/** Optional fail-closed policy applied before every fresh x402 signature. */
x402Policy?: X402PaymentPolicyPreset;
/** Required local atomic-unit ceiling when an x402 payment policy is enabled. */
x402MaxAmountAtomic?: string;
insecure?: boolean; // Skip TLS certificate verification
}

Expand Down Expand Up @@ -142,6 +149,11 @@ class BridgeProcess {
// Shared payment signature cache — middleware reads/writes, bridge invalidates on payment-required results
private x402PaymentCache: X402PaymentCache = { signature: null };

// Built alongside the fetch middleware so HTTP and tool-result 402 paths
// authorize payments with the same fail-closed policy instance.
private x402PaymentPolicy: X402PaymentPolicy | null = null;
private x402PaymentSignatureScope: X402PaymentSignatureScope | null = null;

// Active async tasks (in-memory, also persisted to disk for crash recovery)
private activeTasks: Map<string, Task> = new Map();

Expand Down Expand Up @@ -678,11 +690,27 @@ class BridgeProcess {
return this.client?.getCachedTools()?.find((t: Tool) => t.name === name);
};
const { createX402FetchMiddleware } = await import('../lib/x402/fetch-middleware.js');
if (this.options.x402Policy === 'agent-guild') {
const { createAgentGuildPaymentPolicy } = await import('../lib/x402/agent-guild-policy.js');
const { X402PaymentSignatureScope } = await import('../lib/x402/payment-policy.js');
this.x402PaymentSignatureScope = new X402PaymentSignatureScope();
this.x402PaymentPolicy = createAgentGuildPaymentPolicy({
baseFetch: proxyFetch,
wallet,
...(this.options.x402MaxAmountAtomic && {
maxAmountAtomic: this.options.x402MaxAmountAtomic,
}),
});
}
customFetch = createX402FetchMiddleware(proxyFetch, {
wallet,
getToolByName,
paymentCache: this.x402PaymentCache,
...(this.options.x402 && { schemePreference: this.options.x402 }),
...(this.x402PaymentPolicy && { paymentPolicy: this.x402PaymentPolicy }),
...(this.x402PaymentSignatureScope && {
paymentSignatureScope: this.x402PaymentSignatureScope,
}),
});
}

Expand Down Expand Up @@ -1329,14 +1357,29 @@ class BridgeProcess {

// Invalidate cache and sign fresh
this.x402PaymentCache.signature = null;
if (this.x402PaymentPolicy) {
const decision = await this.x402PaymentPolicy({
paymentRequired: {
x402Version: Number(paymentRequired.x402Version),
accepts: [parsed.accept],
...(parsed.resource && { resource: parsed.resource }),
},
selectedRequirements: parsed.accept,
...(this.options.serverConfig.url && { requestUrl: this.options.serverConfig.url }),
});
if (decision?.abort) {
throw new ClientError(`x402 payment blocked by policy: ${decision.reason}`);
}
}
let paymentSignatureBase64: string;
try {
const { signPayment } = await import('../lib/x402/signer.js');
const signed = await signPayment({
wallet: this.x402Wallet,
accept: parsed.accept,
resource: parsed.resource,
});
this.x402PaymentCache.signature = signed.paymentSignatureBase64;
paymentSignatureBase64 = signed.paymentSignatureBase64;
logger.debug(
`Fresh payment signed for retry: $${signed.amountUsd.toFixed(6)} to ${signed.to} on ${signed.networkLabel}`
);
Expand All @@ -1346,7 +1389,12 @@ class BridgeProcess {
}

// Retry once with the new cached payment
const result = await retryFn();
const result = this.x402PaymentSignatureScope
? await this.x402PaymentSignatureScope.run(paymentSignatureBase64, retryFn)
: await (async () => {
this.x402PaymentCache.signature = paymentSignatureBase64;
return retryFn();
})();
return { handled: true, result };
}

Expand Down Expand Up @@ -1891,7 +1939,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>] [--protocol-version <version>] [--x402 <auto|upto|exact>] [--insecure]'
'Usage: mcpc-bridge <sessionName> <transportConfigJson> [--verbose] [--profile <name>] [--proxy-host <host>] [--proxy-port <port>] [--mcp-session-id <id>] [--protocol-version <version>] [--x402 <auto|upto|exact>] [--x402-policy <agent-guild>] [--x402-max-amount <atomic>] [--insecure]'
);
process.exit(1);
}
Expand Down Expand Up @@ -1947,6 +1995,45 @@ async function main(): Promise<void> {
x402 = value as X402SchemePreference;
}

let x402Policy: X402PaymentPolicyPreset | undefined;
const x402PolicyIndex = args.indexOf('--x402-policy');
if (x402PolicyIndex !== -1) {
const value = args[x402PolicyIndex + 1];
if (
value === undefined ||
!(X402_PAYMENT_POLICY_PRESETS as readonly string[]).includes(value)
) {
console.error(
`--x402-policy requires one of: ${X402_PAYMENT_POLICY_PRESETS.join('|')} (got ${value ?? '<missing>'})`
);
process.exit(1);
}
if (!x402) {
console.error('--x402-policy requires --x402');
process.exit(1);
}
x402Policy = value as X402PaymentPolicyPreset;
}

let x402MaxAmountAtomic: string | undefined;
const x402MaxAmountIndex = args.indexOf('--x402-max-amount');
if (x402MaxAmountIndex !== -1) {
const value = args[x402MaxAmountIndex + 1];
if (value === undefined || !/^[0-9]+$/.test(value) || BigInt(value) <= 0n) {
console.error('--x402-max-amount requires a positive atomic-unit integer');
process.exit(1);
}
x402MaxAmountAtomic = value;
}
if (x402Policy && !x402MaxAmountAtomic) {
console.error('--x402-policy requires --x402-max-amount');
process.exit(1);
}
if (x402MaxAmountAtomic && !x402Policy) {
console.error('--x402-max-amount requires --x402-policy');
process.exit(1);
}

// Parse --insecure flag (skip TLS certificate verification)
const insecure = args.includes('--insecure');

Expand Down Expand Up @@ -1975,6 +2062,12 @@ async function main(): Promise<void> {
if (x402) {
bridgeOptions.x402 = x402;
}
if (x402Policy) {
bridgeOptions.x402Policy = x402Policy;
}
if (x402MaxAmountAtomic) {
bridgeOptions.x402MaxAmountAtomic = x402MaxAmountAtomic;
}
if (insecure) {
bridgeOptions.insecure = true;
}
Expand Down
Loading