callx402 1.0.3 → 1.0.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/release-intent/callx402-v1.0.5.txt +2 -0
- package/.github/workflows/consumer-compatibility.yml +20 -0
- package/.github/workflows/npm-publish.yml +32 -4
- package/README.md +70 -7
- package/bin/callx402.js +11 -7
- package/core/rail.js +1 -1
- package/core/subsystems.js +10 -2
- package/docs/problem-map.md +19 -8
- package/integrations/core.py +11 -2
- package/integrations/langchain/callx402_tools.py +5 -2
- package/mcp/README.md +2 -6
- package/mcp/free-core.js +45 -0
- package/mcp/index.js +76 -32
- package/package.json +1 -1
- package/server.json +3 -3
- package/test/external-mcp-package.test.js +84 -0
- package/test/mcp-full-subsystem-compat.test.js +42 -0
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
name: consumer-compatibility
|
|
2
|
+
on:
|
|
3
|
+
push:
|
|
4
|
+
branches:
|
|
5
|
+
- 'test/clean-npm-mcp-consumer-20261016'
|
|
6
|
+
pull_request:
|
|
7
|
+
branches:
|
|
8
|
+
- main
|
|
9
|
+
permissions:
|
|
10
|
+
contents: read
|
|
11
|
+
jobs:
|
|
12
|
+
verify:
|
|
13
|
+
runs-on: ubuntu-latest
|
|
14
|
+
timeout-minutes: 5
|
|
15
|
+
steps:
|
|
16
|
+
- uses: actions/checkout@v4
|
|
17
|
+
- uses: actions/setup-node@v4
|
|
18
|
+
with:
|
|
19
|
+
node-version: '22'
|
|
20
|
+
- run: npm test && node --test test/external-mcp-package.test.js
|
|
@@ -4,6 +4,10 @@ on:
|
|
|
4
4
|
release:
|
|
5
5
|
types: [published]
|
|
6
6
|
workflow_dispatch:
|
|
7
|
+
push:
|
|
8
|
+
branches: [main]
|
|
9
|
+
paths:
|
|
10
|
+
- '.github/release-intent/callx402-v1.0.5.txt'
|
|
7
11
|
|
|
8
12
|
permissions:
|
|
9
13
|
id-token: write
|
|
@@ -14,7 +18,6 @@ jobs:
|
|
|
14
18
|
runs-on: ubuntu-latest
|
|
15
19
|
steps:
|
|
16
20
|
- uses: actions/checkout@v4
|
|
17
|
-
|
|
18
21
|
- uses: actions/setup-node@v4
|
|
19
22
|
with:
|
|
20
23
|
node-version: '22'
|
|
@@ -23,7 +26,32 @@ jobs:
|
|
|
23
26
|
- run: npm ci --no-audit --no-fund
|
|
24
27
|
- run: npm test
|
|
25
28
|
- run: npm pack --dry-run
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
+
- name: Confirm release version
|
|
30
|
+
run: |
|
|
31
|
+
node -e "if (require('./package.json').version !== '1.0.5') process.exit(1)"
|
|
32
|
+
- name: Check whether version is already published
|
|
33
|
+
id: published
|
|
34
|
+
run: |
|
|
35
|
+
if npm view callx402@1.0.5 version >/dev/null 2>&1; then
|
|
36
|
+
echo "exists=true" >> "$GITHUB_OUTPUT"
|
|
37
|
+
else
|
|
38
|
+
echo "exists=false" >> "$GITHUB_OUTPUT"
|
|
39
|
+
fi
|
|
40
|
+
- name: Publish to npm via existing Trusted Publishing (OIDC)
|
|
41
|
+
if: steps.published.outputs.exists != 'true'
|
|
29
42
|
run: npm publish --provenance --access public
|
|
43
|
+
|
|
44
|
+
github-release:
|
|
45
|
+
if: github.event_name == 'push'
|
|
46
|
+
needs: publish
|
|
47
|
+
runs-on: ubuntu-latest
|
|
48
|
+
permissions:
|
|
49
|
+
contents: write
|
|
50
|
+
steps:
|
|
51
|
+
- name: Publish tagged source release after npm publication
|
|
52
|
+
env:
|
|
53
|
+
GH_TOKEN: ${{ github.token }}
|
|
54
|
+
run: |
|
|
55
|
+
if ! gh release view v1.0.5 --repo "$GITHUB_REPOSITORY" >/dev/null 2>&1; then
|
|
56
|
+
gh release create v1.0.5 --repo "$GITHUB_REPOSITORY" --target "$GITHUB_SHA" --title "callx402 v1.0.5" --notes "Free read-only MCP diagnostics work from a clean npm installation. Existing configured local/remote subsystems remain available. Financial execution remains disabled through MCP. Release verified by full test suite and npm-pack consumer tests."
|
|
57
|
+
fi
|
package/README.md
CHANGED
|
@@ -77,10 +77,7 @@ Ask the client to list its MCP tools: the six `x402_*` tools should appear.
|
|
|
77
77
|
Per-client notes and the raw stdio test handshake:
|
|
78
78
|
[`integrations/mcp-clients/`](integrations/mcp-clients/).
|
|
79
79
|
|
|
80
|
-
Note:
|
|
81
|
-
into the v2.0.0 subsystem tree, so they report `subsystem_unreachable`
|
|
82
|
-
until `CALLX402_V2_ROOT` points at the tree (see "Full subsystem commands"
|
|
83
|
-
above) and the matching flag is enabled.
|
|
80
|
+
Note: all six free MCP tools respond on a clean npm install using conservative local, read-only assessment of supplied evidence. Without an authoritative source they **do not** verify settlement, access a private ledger, or execute recovery. Existing configured local/remote subsystem integrations remain available.
|
|
84
81
|
|
|
85
82
|
## The relationship
|
|
86
83
|
|
|
@@ -328,13 +325,79 @@ redemption is not currently enabled. Stripe credits use a separate redemption pa
|
|
|
328
325
|
|
|
329
326
|
Live fee schedule: `GET https://payload-rail.fly.dev/v1/callx402/actions`
|
|
330
327
|
(responds `"model":"paid on-demand per action; no subscription required"`).
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
328
|
+
There are two prices per action, and they are not interchangeable: the
|
|
329
|
+
**card price** is the Stripe schedule fee; the **USDC price** on the x402
|
|
330
|
+
path is exactly that fee divided by 20 (the free quote endpoint is the
|
|
331
|
+
source of truth for the USDC column). Both, side by side:
|
|
332
|
+
|
|
333
|
+
| Action | Card (Stripe) | USDC (x402 path) |
|
|
334
|
+
|---|---|---|
|
|
335
|
+
| `diagnose` | $2.00 | $0.10 |
|
|
336
|
+
| `explain` | $1.00 | $0.05 |
|
|
337
|
+
| `evidence` | $2.00 | $0.10 |
|
|
338
|
+
| `recover` | $10.00 | $0.50 |
|
|
339
|
+
| `resolve` | $5.00 | $0.25 |
|
|
340
|
+
| `preflight` | $3.00 | $0.15 |
|
|
341
|
+
| `settlement_interpretation` | $3.00 | $0.15 |
|
|
342
|
+
| `failure_classification` | $2.00 | $0.10 |
|
|
343
|
+
| `duplicate_payment_risk` | $5.00 | $0.25 |
|
|
344
|
+
|
|
345
|
+
Rail `evidence` and `explain` read back a prior paid invocation (the
|
|
346
|
+
request carries that invocation's `invocation_id`), so they cannot be used
|
|
347
|
+
cold; the free local forms read your local operation ledger instead.
|
|
348
|
+
|
|
349
|
+
**Not sold on the rail:** `safe_retry`, `rescue`, `monitor`, and `execute`
|
|
350
|
+
are local-only. They run in the self-hosted runtime (v2.0.0 tree), are
|
|
351
|
+
absent from the rail catalog above, and the rail refuses them with
|
|
352
|
+
`422 HOSTED_ACTION_UNAVAILABLE` — there is no Stripe or x402 purchase path
|
|
353
|
+
for them. For the "is this retry safe?" question, the free local `recover`
|
|
354
|
+
verdict is the available read-only answer.
|
|
355
|
+
|
|
356
|
+
Never blind-retry a payment to reach a paid action: get the quote first.
|
|
334
357
|
|
|
335
358
|
Problem to action map (which paid action answers which failure, with the free
|
|
336
359
|
CLI/MCP path for each): [`docs/problem-map.md`](docs/problem-map.md).
|
|
337
360
|
|
|
361
|
+
## Public RevRule evaluation (0.01 USDC)
|
|
362
|
+
|
|
363
|
+
The rail also exposes one public RevRule evaluation endpoint. It is a
|
|
364
|
+
RevRule demo, not a callx402 diagnostic action:
|
|
365
|
+
`POST https://payload-rail.fly.dev/v1/x402/public/evaluate` evaluates one
|
|
366
|
+
economic event against the fixed, transparent public demo graph (platform
|
|
367
|
+
5% of gross; creator 70% of the remainder; referrer 25% of the remainder
|
|
368
|
+
when a referrer is named in the event) for 0.01 USDC on Base.
|
|
369
|
+
|
|
370
|
+
The full, correct request body is:
|
|
371
|
+
|
|
372
|
+
```json
|
|
373
|
+
{
|
|
374
|
+
"event": {
|
|
375
|
+
"eventId": "my-event-001",
|
|
376
|
+
"type": "SALE_COMPLETED",
|
|
377
|
+
"rail": "stripe",
|
|
378
|
+
"amountMicros": 100000000,
|
|
379
|
+
"currency": "USD",
|
|
380
|
+
"occurredAt": "2026-10-13T00:00:00.000Z"
|
|
381
|
+
},
|
|
382
|
+
"txHash": "0x<your settled 0.01 USDC payment on Base>"
|
|
383
|
+
}
|
|
384
|
+
```
|
|
385
|
+
|
|
386
|
+
- `event.eventId` (any unique non-empty string) and `event.rail` are
|
|
387
|
+
**required**, even though the 402's advertised bazaar schema omits both.
|
|
388
|
+
Valid `rail` values: `stripe`, `ach`, `x402`, `manual`, `stablecoin`.
|
|
389
|
+
- `event.type` must be one of: `SALE_COMPLETED`, `API_PAYMENT`,
|
|
390
|
+
`LICENSE_PAID`, `SUBSCRIPTION_PAYMENT`, `REFERRAL_CONVERTED`,
|
|
391
|
+
`ROYALTY_RECEIVED`, `CAMPAIGN_MILESTONE`, `MARKETPLACE_TRANSACTION`,
|
|
392
|
+
`TOUCH`.
|
|
393
|
+
- The event is validated **before** payment: a malformed body gets
|
|
394
|
+
`400 INVALID_EVENT`, not a 402 (verified live: the body above, minus
|
|
395
|
+
`txHash`, returns the 402). The endpoint fills in the rest itself —
|
|
396
|
+
`graphId` is fixed to the public demo graph, and `currency` defaults to
|
|
397
|
+
`"USD"` and `occurredAt` to now when omitted.
|
|
398
|
+
- Omit `txHash` to receive the x402 v2 `402` challenge; pay 0.01 USDC,
|
|
399
|
+
then retry the same body with the hash. Each txHash is single-use.
|
|
400
|
+
|
|
338
401
|
## Integrations
|
|
339
402
|
|
|
340
403
|
Working entry points for agent frameworks, automation, MCP clients, and x402
|
package/bin/callx402.js
CHANGED
|
@@ -222,9 +222,11 @@ never executes, never repays. Requires PAYLOAD_VEYLINE_RECOVERY=1.
|
|
|
222
222
|
|
|
223
223
|
This is the read-only answer to "is a retry safe?" — a SAFE_RETRY verdict
|
|
224
224
|
here is advisory; performing the safe action stays with your own adapters.
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
225
|
+
There is NO paid rail form of 'safe_retry': the hosted rail refuses it as
|
|
226
|
+
local-only (it is absent from the rail catalog and cannot be bought via
|
|
227
|
+
Stripe or x402). This free recover verdict is the read-only answer
|
|
228
|
+
available without the self-hosted runtime tree (see docs/problem-map.md).
|
|
229
|
+
The MCP tool x402_recover covers the same free diagnostic.
|
|
228
230
|
|
|
229
231
|
Exit codes: 0 ok · 1 failure · 2 usage (missing identity/evidence) ·
|
|
230
232
|
3 disabled/unreachable · 4 budget refused ·
|
|
@@ -256,14 +258,16 @@ Usage:
|
|
|
256
258
|
callx402 "natural language intent" intent mode (plans, never executes blindly)
|
|
257
259
|
callx402 --help | --version
|
|
258
260
|
|
|
259
|
-
Commands (hosted by default — zero setup
|
|
261
|
+
Commands (rail actions hosted by default — zero setup; local-only
|
|
262
|
+
commands marked, they need the v2.0.0 runtime tree and are NOT sold on
|
|
263
|
+
the hosted rail):
|
|
260
264
|
diagnose run the x402 doctor over supplied evidence [--target <url|json>] [--evidence <json|@file>]
|
|
261
|
-
rescue incident triage (auth-gated; read-only without auth) --incident <id>
|
|
265
|
+
rescue incident triage, local only (auth-gated; read-only without auth) --incident <id>
|
|
262
266
|
route select a tool/route for a goal --goal <text>
|
|
263
267
|
resolve resolve settlement state from evidence --evidence <json|@file>
|
|
264
268
|
doctor alias of diagnose
|
|
265
|
-
execute run the intent pipeline
|
|
266
|
-
monitor sentinel snapshot [--once] or stream [--watch]
|
|
269
|
+
execute run the intent pipeline, local only --intent <text> [--max-budget N] [--dry-run] [--idempotency-key K]
|
|
270
|
+
monitor sentinel snapshot, local only [--once] or stream [--watch]
|
|
267
271
|
status per-subsystem reachable/enabled/version report
|
|
268
272
|
preflight run MCP preflight checks
|
|
269
273
|
inspect capability-graph lookup/stats [--query <text>]
|
package/core/rail.js
CHANGED
|
@@ -156,7 +156,7 @@ async function invokeRail(command, args, opts = {}) {
|
|
|
156
156
|
if (!interactive) {
|
|
157
157
|
return {
|
|
158
158
|
ok: false,
|
|
159
|
-
error: `Payment of $${price} required. Re-run with --
|
|
159
|
+
error: `Payment of $${price} required. Re-run with --approve to approve non-interactively, or run interactively.`,
|
|
160
160
|
quote: { action, price_usd: price, quote_id: quoteId, quote_inputs: quote.quote_inputs },
|
|
161
161
|
};
|
|
162
162
|
}
|
package/core/subsystems.js
CHANGED
|
@@ -52,6 +52,14 @@ const NOTES = {
|
|
|
52
52
|
recovery: 'File-backed idempotency store used for duplicate-idempotencyKey dedupe.',
|
|
53
53
|
};
|
|
54
54
|
|
|
55
|
+
/** First line only: Node require errors append a multi-line "Require stack:"
|
|
56
|
+
* dump that helps nobody in a status report or an MCP tool result. The short
|
|
57
|
+
* form still names the missing module and stays actionable. */
|
|
58
|
+
function briefError(err) {
|
|
59
|
+
const msg = err && err.message ? String(err.message) : String(err || 'unknown error');
|
|
60
|
+
return msg.split('\n')[0].trim();
|
|
61
|
+
}
|
|
62
|
+
|
|
55
63
|
function defaultV2Root() {
|
|
56
64
|
return path.resolve(__dirname, '../../x402-paid-api-starter-kit/v2.0.0');
|
|
57
65
|
}
|
|
@@ -98,7 +106,7 @@ function getSubsystem(name, { config = {}, deps = {} } = {}) {
|
|
|
98
106
|
try {
|
|
99
107
|
mod = requireFiles(v2Root, MODULE_FILES[name]);
|
|
100
108
|
} catch (err) {
|
|
101
|
-
return { name, reachable: false, module: null, enabled: false, flag: null, version: null, note: NOTES[name] || null, subsystemStatus: 'unreachable', error: `require failed: ${err
|
|
109
|
+
return { name, reachable: false, module: null, enabled: false, flag: null, version: null, note: NOTES[name] || null, subsystemStatus: 'unreachable', error: `require failed: ${briefError(err)}` };
|
|
102
110
|
}
|
|
103
111
|
|
|
104
112
|
let flag = null;
|
|
@@ -107,7 +115,7 @@ function getSubsystem(name, { config = {}, deps = {} } = {}) {
|
|
|
107
115
|
flag = typeof mod.FLAG === 'string' ? mod.FLAG : null;
|
|
108
116
|
enabled = typeof mod.enabled === 'function' ? mod.enabled() : false;
|
|
109
117
|
} catch (err) {
|
|
110
|
-
return { name, reachable: true, module: mod, enabled: false, flag, version: null, note: NOTES[name] || null, subsystemStatus: 'disabled', error: `enabled() check failed: ${err
|
|
118
|
+
return { name, reachable: true, module: mod, enabled: false, flag, version: null, note: NOTES[name] || null, subsystemStatus: 'disabled', error: `enabled() check failed: ${briefError(err)}` };
|
|
111
119
|
}
|
|
112
120
|
|
|
113
121
|
const version = mod.version || mod.RESCUE_VERSION || mod.EFFECT_PROOF_VERSION || null;
|
package/docs/problem-map.md
CHANGED
|
@@ -25,7 +25,13 @@ v2.0.0 runtime tree and `CALLX402_LOCAL=1`). The paid form runs on the
|
|
|
25
25
|
production rail: quote first, payment verified exactly once, then the
|
|
26
26
|
diagnostic executes server-side and the result is returned — zero local setup.
|
|
27
27
|
The "Free path" column names the free equivalent; "rail only" means the action
|
|
28
|
-
exists only as a paid rail action.
|
|
28
|
+
exists only as a paid rail action. "Local only" means the opposite: the action
|
|
29
|
+
exists ONLY locally (self-hosted runtime tree). The hosted rail refuses it
|
|
30
|
+
with `422 HOSTED_ACTION_UNAVAILABLE`, it is absent from the rail catalog
|
|
31
|
+
(`GET /v1/callx402/actions` lists nine actions), and there is no Stripe or
|
|
32
|
+
x402 purchase path for it. Four actions are local-only: `safe_retry`,
|
|
33
|
+
`rescue`, `monitor`, `execute`. Their fee cells below show the schedule
|
|
34
|
+
figure for reference only — it cannot be paid on the rail.
|
|
29
35
|
|
|
30
36
|
**Fees.** The table lists the Stripe-path fee per action. The x402 path
|
|
31
37
|
quoted by the free quote endpoint is exactly that fee divided by 20
|
|
@@ -40,16 +46,16 @@ deterministic, not an estimate.
|
|
|
40
46
|
| 1 | Settlement uncertainty — can't tell from current info whether the payment settled | `resolve` | $5.00 | `callx402 resolve --evidence ...` / `x402_resolve` | Unknown stays unknown; never auto-retry, never repay |
|
|
41
47
|
| 2 | Lost result after payment — paid, but the result never arrived | `recover` | $10.00 | `callx402 recover <op> --identity ...` / `x402_recover` | Read-only: nothing charged, nothing executed; returns the safe recovery decision or the proven prior result, never a blind re-execution |
|
|
42
48
|
| 3 | Retry uncertainty — something failed; need what/why before deciding if retry is safe | `diagnose` | $2.00 | `callx402 diagnose --evidence ...` / `x402_diagnose` | Unknown failure class returns UNKNOWN, never a guess |
|
|
43
|
-
| 4 | Retry uncertainty, narrowed — is THIS specific retry safe to sign? | `safe_retry` | $5.00
|
|
49
|
+
| 4 | Retry uncertainty, narrowed — is THIS specific retry safe to sign? | `safe_retry` | $5.00 (local only — not purchasable on the rail) | local only (self-hosted runtime; no CLI command in this package and no rail purchase path — start free with `recover`: a SAFE_RETRY verdict there is the read-only answer) | A retry that would double-pay is refused |
|
|
44
50
|
| 5 | Duplicate-payment risk — a retry might sign a second authorization for the same intent | `duplicate_payment_risk` | $5.00 | rail only (start free: `resolve` the settlement, then `recover` for the retry verdict) | Ambiguous authorization state blocks the retry |
|
|
45
51
|
| 6 | Need proof — ground truth before acting on any incident | `evidence` | $2.00 | `callx402 evidence <op>` / `x402_evidence` | First step in any incident; returns what is recorded, nothing invented |
|
|
46
52
|
| 7 | Need explanation — plain-language state assessment before retry/refund/escalate | `explain` | $1.00 | `callx402 explain <op>` / `x402_explain` | INCOMPLETE means do not retry, do not repay |
|
|
47
53
|
| 8 | Pre-commit check — intent, payment readiness, risk before an economic operation commits | `preflight` | $3.00 | `callx402 preflight` (local checks) | Blocked stays blocked until the reasons clear |
|
|
48
54
|
| 9 | Failure triage — classify the failure so the right playbook runs | `failure_classification` | $2.00 | rail only (start free with `diagnose`) | Unclassifiable failures escalate to human review |
|
|
49
55
|
| 10 | Settlement-record interpretation — read a raw settlement record, state what it means | `settlement_interpretation` | $3.00 | rail only (start free with `resolve --evidence ...`) | Ambiguous records are reported ambiguous |
|
|
50
|
-
| 11 | Rescue a failed transaction — triage and recover the incident end to end | `rescue` | $15.00 | `callx402 rescue --incident <id>` (triage and quote only, never executes) | Never fabricates success; unreachable subsystems fail closed |
|
|
51
|
-
| 12 | Watch an operation/workload for state changes, failures, anomalies | `monitor` | $5.00 | `callx402 monitor --once` (local sentinel snapshot) | An unwatched gap is reported as a gap, not as healthy |
|
|
52
|
-
| 13 | Run an economic operation through the protected path (exactly-once) | `execute` | $10.00 | `callx402 execute --intent ... --dry-run` (plans only) | Over-budget intents refused with zero side effects |
|
|
56
|
+
| 11 | Rescue a failed transaction — triage and recover the incident end to end | `rescue` | $15.00 (local only — not purchasable on the rail) | `callx402 rescue --incident <id>` (triage and quote only, never executes) | Never fabricates success; unreachable subsystems fail closed |
|
|
57
|
+
| 12 | Watch an operation/workload for state changes, failures, anomalies | `monitor` | $5.00 (local only — not purchasable on the rail) | `callx402 monitor --once` (local sentinel snapshot) | An unwatched gap is reported as a gap, not as healthy |
|
|
58
|
+
| 13 | Run an economic operation through the protected path (exactly-once) | `execute` | $10.00 (local only — not purchasable on the rail) | `callx402 execute --intent ... --dry-run` (plans only) | Over-budget intents refused with zero side effects |
|
|
53
59
|
|
|
54
60
|
## Where each surface consumes this map
|
|
55
61
|
|
|
@@ -70,9 +76,14 @@ deterministic, not an estimate.
|
|
|
70
76
|
## Notes
|
|
71
77
|
|
|
72
78
|
- Action names are canonical in `snake_case`. The rail also accepts the
|
|
73
|
-
hyphenated aliases shown in the OpenAPI enum (`
|
|
74
|
-
`
|
|
75
|
-
`
|
|
79
|
+
hyphenated aliases shown in the OpenAPI enum (`duplicate-payment-risk`,
|
|
80
|
+
`settlement-interpretation`, `failure-classification`); both forms resolve
|
|
81
|
+
identically (verified live). `safe-retry` is not a rail action in either
|
|
82
|
+
spelling — it is local-only (see the two-tiers note above).
|
|
83
|
+
- Rail `evidence` / `explain` read back a PRIOR paid invocation: the request
|
|
84
|
+
must carry that invocation's `invocation_id`, and the rail returns
|
|
85
|
+
`400 MISSING_INPUT` without one, so they cannot be used cold. The free
|
|
86
|
+
local forms in the map read your local operation ledger instead.
|
|
76
87
|
- The x402-path quote is the valuation-layer quote, not the raw schedule;
|
|
77
88
|
the fixed schedule above is the Stripe-path floor.
|
|
78
89
|
- Local CLI / MCP-stdio diagnostics are free and read-only — no payment, no
|
package/integrations/core.py
CHANGED
|
@@ -5,7 +5,7 @@ Plain functions (no framework imports) so LangChain, CrewAI, AutoGen, or any
|
|
|
5
5
|
agent framework can wrap them directly. stdlib only.
|
|
6
6
|
|
|
7
7
|
The rail's public endpoints:
|
|
8
|
-
GET /v1/callx402/actions free — live
|
|
8
|
+
GET /v1/callx402/actions free — live fee schedule (9 hosted actions)
|
|
9
9
|
GET /v1/callx402/quote free — deterministic pre-execution quote
|
|
10
10
|
POST /v1/callx402/actions/{action} paid — unauthenticated -> x402 v2 402
|
|
11
11
|
(USDC on Base); credit_id / txHash /
|
|
@@ -30,6 +30,13 @@ RAIL = os.environ.get("CALLX402_RAIL", "https://payload-rail.fly.dev").rstrip("/
|
|
|
30
30
|
|
|
31
31
|
# Canonical action names (snake_case), verified live 2026-10-07.
|
|
32
32
|
# Hyphenated aliases are also accepted by the rail; these are canonical.
|
|
33
|
+
# AVAILABILITY (verified live 2026-10-13): only 9 of these 13 are hosted and
|
|
34
|
+
# purchasable on the rail — diagnose, explain, evidence, recover, resolve,
|
|
35
|
+
# preflight, settlement_interpretation, failure_classification,
|
|
36
|
+
# duplicate_payment_risk. The other four (safe_retry, rescue, monitor,
|
|
37
|
+
# execute) are LOCAL-ONLY: the rail refuses them with
|
|
38
|
+
# 422 HOSTED_ACTION_UNAVAILABLE, they are absent from the rail catalog, and
|
|
39
|
+
# they cannot be bought via Stripe or x402.
|
|
33
40
|
ACTIONS = (
|
|
34
41
|
"diagnose", "explain", "evidence", "recover", "resolve", "preflight",
|
|
35
42
|
"monitor", "execute", "rescue", "settlement_interpretation", "safe_retry",
|
|
@@ -303,7 +310,9 @@ FREE_LOCAL_ALTERNATIVE = (
|
|
|
303
310
|
"Local CLI / MCP-stdio diagnostics are free and read-only "
|
|
304
311
|
"(git clone https://github.com/Payloadhq/callx402; "
|
|
305
312
|
"node mcp/index.js as an MCP server). The paid rail actions above are "
|
|
306
|
-
"for hosted on-demand invocation with payment."
|
|
313
|
+
"for hosted on-demand invocation with payment. Note: safe_retry, "
|
|
314
|
+
"rescue, monitor, and execute are local-only and are NOT purchasable "
|
|
315
|
+
"on the hosted rail."
|
|
307
316
|
)
|
|
308
317
|
|
|
309
318
|
|
|
@@ -125,8 +125,11 @@ def callx402_explain(operation_id: str) -> str:
|
|
|
125
125
|
def callx402_safe_retry(retry_plan: str) -> str:
|
|
126
126
|
"""Go/no-go verdict on ONE specific retry plan before anything is signed.
|
|
127
127
|
Use when a retry is being considered and must be proven safe first.
|
|
128
|
-
|
|
129
|
-
|
|
128
|
+
AVAILABILITY: safe_retry is LOCAL-ONLY — the hosted rail refuses it
|
|
129
|
+
(422 HOSTED_ACTION_UNAVAILABLE) and it cannot be bought via Stripe or
|
|
130
|
+
x402, so this tool currently surfaces that refusal. For the
|
|
131
|
+
retry-safety question today, use callx402_recover's read-only verdict.
|
|
132
|
+
A retry that would double-pay is refused."""
|
|
130
133
|
return _render(invoke("safe_retry", {"retry_plan": retry_plan}, **_auth()))
|
|
131
134
|
|
|
132
135
|
|
package/mcp/README.md
CHANGED
|
@@ -17,13 +17,9 @@ over stdio. Zero dependencies (node stdlib only).
|
|
|
17
17
|
| `x402_status` | Subsystem reachability |
|
|
18
18
|
|
|
19
19
|
Only diagnostic, read-only actions are exposed. Nothing here charges, executes,
|
|
20
|
-
retries, or repays. If a subsystem is
|
|
21
|
-
instead of inventing an answer.
|
|
20
|
+
retries, or repays. If a verified local or remote subsystem is configured, the tools retain the existing full-subsystem dispatch. On a clean npm install, packaged free tools provide conservative, read-only analysis of *caller-supplied* evidence. They cannot independently verify settlement or access a private ledger.
|
|
22
21
|
|
|
23
|
-
Setup note:
|
|
24
|
-
dispatch into the v2.0.0 subsystem tree, so they report
|
|
25
|
-
`subsystem_unreachable` until `CALLX402_V2_ROOT` points at the tree and the
|
|
26
|
-
matching flag is enabled (see the main README, "Full subsystem commands").
|
|
22
|
+
Setup note: **all six MCP tools work on a clean npm install**. Missing private ledger evidence is reported as NO_BASIS; recovery is never auto-executed; settlement without chain verification remains UNKNOWN. Existing optional local v2.0.0 integrations continue to work when configured and enabled.
|
|
27
23
|
|
|
28
24
|
Tool names are namespaced `x402_*` — never `call_x402` (live collision on the
|
|
29
25
|
fiatdock marketplace).
|
package/mcp/free-core.js
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
// Local, read-only, dependency-free MCP diagnosis.
|
|
3
|
+
function run(name, args = {}) {
|
|
4
|
+
if(name === 'x402_status') return {status:'READY', mode:'FREE_MCP', tools:6};
|
|
5
|
+
let evidence = args.evidence || {};
|
|
6
|
+
if(typeof evidence === 'string') evidence = JSON.parse(evidence);
|
|
7
|
+
if(!evidence || typeof evidence !== 'object' || Array.isArray(evidence)) {
|
|
8
|
+
throw new Error('Evidence must be a JSON object');
|
|
9
|
+
}
|
|
10
|
+
const status = Number(evidence.httpStatus || evidence.status || evidence.response?.status || 0);
|
|
11
|
+
let challenge = evidence.challenge || evidence.paymentRequired || evidence.responseBody || {};
|
|
12
|
+
if(typeof challenge === 'string') {
|
|
13
|
+
try { challenge = JSON.parse(challenge); } catch { challenge = {}; }
|
|
14
|
+
}
|
|
15
|
+
const accept = (Array.isArray(challenge.accepts) && challenge.accepts[0]) ||
|
|
16
|
+
challenge.paymentRequirements || challenge;
|
|
17
|
+
const network = accept.network || evidence.network || null;
|
|
18
|
+
const legacy = ['base','base-sepolia','solana','solana-devnet','ethereum','arbitrum','optimism','polygon'];
|
|
19
|
+
const known = typeof network === 'string' && (
|
|
20
|
+
/^(eip155:\d+|solana:[A-Za-z0-9]+)$/.test(network) || legacy.includes(network));
|
|
21
|
+
const terms = !!(network && accept.asset && (accept.amount != null || accept.maxAmountRequired != null));
|
|
22
|
+
const version = challenge.x402Version || evidence.x402Version || null;
|
|
23
|
+
if(name === 'x402_diagnose') return {
|
|
24
|
+
classification: status === 402 ? (terms ? (known ? 'RECOGNIZED_CHALLENGE' : 'UNKNOWN_NETWORK')
|
|
25
|
+
: 'INCOMPLETE_CHALLENGE') :
|
|
26
|
+
status >= 500 ? 'SERVER_ERROR' :
|
|
27
|
+
status === 401 || status === 403 ? 'AUTH_DENIED' : 'INSUFFICIENT_EVIDENCE',
|
|
28
|
+
network, x402Version: version, termsPresent: terms,
|
|
29
|
+
legacySlugInV2: !!(version === 2 && known && network && !network.includes(':')),
|
|
30
|
+
evidenceSource: 'USER_SUPPLIED', verifiedSettlement:false,
|
|
31
|
+
note:'Recognition does not verify payability or on-chain settlement.'
|
|
32
|
+
};
|
|
33
|
+
if(name === 'x402_evidence') return {
|
|
34
|
+
operationId: args.operationId || null, evidence,
|
|
35
|
+
basis: Object.keys(evidence).length ? 'USER_SUPPLIED' : 'NO_BASIS'
|
|
36
|
+
};
|
|
37
|
+
if(name === 'x402_explain') return {
|
|
38
|
+
operationId: args.operationId || null, summary:'Local supplied evidence only',
|
|
39
|
+
basis: Object.keys(evidence).length ? 'USER_SUPPLIED' : 'NO_BASIS'
|
|
40
|
+
};
|
|
41
|
+
if(name === 'x402_recover') return {decision:'HUMAN_REVIEW', canRetry:false, executed:false};
|
|
42
|
+
if(name === 'x402_resolve') return {settlement:'UNKNOWN', canRetry:false};
|
|
43
|
+
throw new Error('Unknown MCP tool');
|
|
44
|
+
}
|
|
45
|
+
module.exports = {run};
|
package/mcp/index.js
CHANGED
|
@@ -18,23 +18,62 @@
|
|
|
18
18
|
* Only read-only / diagnostic actions are exposed. Actions with side effects
|
|
19
19
|
* (execute, rescue mutations) are intentionally NOT tools here.
|
|
20
20
|
*
|
|
21
|
+
* All six tools load without sibling packages. Existing enabled local/remote\n * subsystems are used when configured; otherwise free evidence and settlement
|
|
22
|
+
* assessments remain conservative: caller evidence is not authenticated,
|
|
23
|
+
* and local tools cannot certify a real payment or automatically retry.
|
|
24
|
+
*
|
|
21
25
|
* Usage (Claude Desktop / MCP client config):
|
|
22
26
|
* { "command": "node", "args": ["/path/to/callx402/mcp/index.js"] }
|
|
23
27
|
* Env: CALLX402_CONFIG, CALLX402_V2_ROOT, CALLX402_TIMEOUT_MS (default 30000).
|
|
24
28
|
*/
|
|
25
29
|
const readline = require('node:readline');
|
|
26
30
|
|
|
27
|
-
const VERSION = '1.0.
|
|
31
|
+
const VERSION = '1.0.5';
|
|
28
32
|
const PROTOCOL_VERSION = '2024-11-05';
|
|
29
33
|
const SERVER_NAME = 'callx402';
|
|
30
34
|
const TIMEOUT_MS = Number(process.env.CALLX402_TIMEOUT_MS || 30000);
|
|
31
35
|
|
|
32
|
-
|
|
33
|
-
|
|
36
|
+
// The free MCP artifact must be functional when installed alone. The CLI's
|
|
37
|
+
// optional v2 subsystem tree is not a dependency of these read-only tools.
|
|
38
|
+
const freeCore = require('./free-core.js');
|
|
39
|
+
const core = require('../core/index.js');
|
|
40
|
+
const { loadConfig } = require('../core/config.js');
|
|
41
|
+
const { getSubsystem } = require('../core/subsystems.js');
|
|
42
|
+
const { loadVeyline } = require('../core/veyline.js');
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Prefer a customer's already configured *real* local/remote subsystem.
|
|
46
|
+
* Otherwise use the packaged, read-only offline diagnostic instead of
|
|
47
|
+
* returning subsystem_unreachable on a fresh npm install.
|
|
48
|
+
*
|
|
49
|
+
* No payment, signer, or execute action is reachable through this MCP server.
|
|
50
|
+
*/
|
|
51
|
+
function hasConfiguredBackend(action) {
|
|
52
|
+
const cfg = loadConfig();
|
|
53
|
+
if (cfg.mode === 'remote' && cfg.remoteUrl) return true;
|
|
54
|
+
const subsystem = ({ diagnose: 'doctor', resolve: 'settlement' })[action];
|
|
55
|
+
if (subsystem) {
|
|
56
|
+
const handle = getSubsystem(subsystem, { config: cfg });
|
|
57
|
+
return handle.reachable && handle.enabled;
|
|
58
|
+
}
|
|
59
|
+
if (['evidence', 'explain', 'recover'].includes(action)) {
|
|
60
|
+
const flag = action === 'recover' ? 'PAYLOAD_VEYLINE_RECOVERY' : 'PAYLOAD_VEYLINE_LEDGER';
|
|
61
|
+
if (process.env[flag] !== '1') return false;
|
|
62
|
+
const loaded = loadVeyline(cfg, {});
|
|
63
|
+
return !!loaded.modules;
|
|
64
|
+
}
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
34
67
|
|
|
35
68
|
/* ------------------------------------------------------------------ */
|
|
36
|
-
/* Tool definitions:
|
|
69
|
+
/* Tool definitions: packaged read-only diagnostics only. */
|
|
70
|
+
/* */
|
|
71
|
+
/* Caller-supplied evidence is unverified. No private ledger or funds. */
|
|
37
72
|
/* ------------------------------------------------------------------ */
|
|
73
|
+
const TREE_REQUIRED = ' Local read-only inference from caller-supplied evidence, included in npm. Does not independently verify payments or access private infrastructure.';
|
|
74
|
+
const ledgerRequired = () => ' Returns only caller-supplied evidence; no remote/private ledger is included and no on-chain data is verified.';
|
|
75
|
+
const LEDGER_REQUIRED = ledgerRequired();
|
|
76
|
+
|
|
38
77
|
const TOOLS = [
|
|
39
78
|
{
|
|
40
79
|
name: 'x402_diagnose',
|
|
@@ -43,14 +82,14 @@ const TOOLS = [
|
|
|
43
82
|
'Run the x402 doctor over supplied evidence. Use when something went ' +
|
|
44
83
|
'wrong with an x402 payment or MCP/x402 interaction and you need a ' +
|
|
45
84
|
'diagnosis. Input: evidence as a JSON string or object describing the ' +
|
|
46
|
-
'failure (error strings, responses, tx hashes). Read-only.
|
|
47
|
-
|
|
85
|
+
'failure (error strings, responses, tx hashes). Read-only.' +
|
|
86
|
+
TREE_REQUIRED,
|
|
48
87
|
inputSchema: {
|
|
49
88
|
type: 'object',
|
|
50
89
|
properties: {
|
|
51
90
|
evidence: {
|
|
52
91
|
type: 'string',
|
|
53
|
-
description: 'JSON string
|
|
92
|
+
description: 'JSON string describing non-sensitive failure evidence; local file references are unsupported.',
|
|
54
93
|
},
|
|
55
94
|
target: {
|
|
56
95
|
type: 'string',
|
|
@@ -64,15 +103,14 @@ const TOOLS = [
|
|
|
64
103
|
name: 'x402_evidence',
|
|
65
104
|
action: 'evidence',
|
|
66
105
|
description:
|
|
67
|
-
'Show
|
|
68
|
-
'
|
|
69
|
-
|
|
70
|
-
' Free read-only diagnostic surface: no payment, no auth, never Veyline-gated. For the paid on-demand callx402 action path, see https://payloadhq.github.io/agents.json.',
|
|
106
|
+
'Show caller-supplied operation evidence only; no private or hosted ledger is included. ' +
|
|
107
|
+
'Missing evidence returns NO_BASIS, never an invented ledger record. Read-only.' +
|
|
108
|
+
LEDGER_REQUIRED,
|
|
71
109
|
inputSchema: {
|
|
72
110
|
type: 'object',
|
|
73
111
|
properties: {
|
|
74
112
|
operationId: { type: 'string', description: 'The operation ID to look up.' },
|
|
75
|
-
|
|
113
|
+
evidence: { type: 'string', description: 'Optional sanitized JSON evidence; otherwise NO_BASIS.' },
|
|
76
114
|
},
|
|
77
115
|
required: ['operationId'],
|
|
78
116
|
},
|
|
@@ -81,16 +119,14 @@ const TOOLS = [
|
|
|
81
119
|
name: 'x402_explain',
|
|
82
120
|
action: 'explain',
|
|
83
121
|
description:
|
|
84
|
-
'
|
|
85
|
-
'
|
|
86
|
-
|
|
87
|
-
'before deciding whether to retry, refund, or escalate. Read-only. ' +
|
|
88
|
-
' Free read-only diagnostic surface: no payment, no auth, never Veyline-gated. For the paid on-demand callx402 action path, see https://payloadhq.github.io/agents.json.',
|
|
122
|
+
'Provide a conservative interpretation of caller-supplied evidence only; ' +
|
|
123
|
+
'no independent payment or settlement verification is performed. Read-only.' +
|
|
124
|
+
LEDGER_REQUIRED,
|
|
89
125
|
inputSchema: {
|
|
90
126
|
type: 'object',
|
|
91
127
|
properties: {
|
|
92
128
|
operationId: { type: 'string', description: 'The operation ID to explain.' },
|
|
93
|
-
|
|
129
|
+
evidence: { type: 'string', description: 'Optional sanitized JSON evidence; otherwise NO_BASIS.' },
|
|
94
130
|
},
|
|
95
131
|
required: ['operationId'],
|
|
96
132
|
},
|
|
@@ -101,9 +137,9 @@ const TOOLS = [
|
|
|
101
137
|
description:
|
|
102
138
|
'Report the safe recovery decision for an operation whose result was lost ' +
|
|
103
139
|
'(paid but no result). READ-ONLY: nothing is charged, nothing is executed. ' +
|
|
104
|
-
'
|
|
105
|
-
'
|
|
106
|
-
|
|
140
|
+
'Without an authoritative settlement source, return HUMAN_REVIEW and never retry. ' +
|
|
141
|
+
'This free diagnostic cannot execute recovery.' +
|
|
142
|
+
ledgerRequired(),
|
|
107
143
|
inputSchema: {
|
|
108
144
|
type: 'object',
|
|
109
145
|
properties: {
|
|
@@ -118,9 +154,9 @@ const TOOLS = [
|
|
|
118
154
|
name: 'x402_resolve',
|
|
119
155
|
action: 'resolve',
|
|
120
156
|
description:
|
|
121
|
-
'
|
|
122
|
-
'
|
|
123
|
-
|
|
157
|
+
'Classify caller-provided settlement evidence conservatively. Returns UNKNOWN ' +
|
|
158
|
+
'without authoritative chain verification; no retry is performed. Read-only.' +
|
|
159
|
+
TREE_REQUIRED,
|
|
124
160
|
inputSchema: {
|
|
125
161
|
type: 'object',
|
|
126
162
|
properties: {
|
|
@@ -137,8 +173,9 @@ const TOOLS = [
|
|
|
137
173
|
action: 'status',
|
|
138
174
|
description:
|
|
139
175
|
'Check callx402 subsystem reachability (which diagnostic subsystems are ' +
|
|
140
|
-
'available). Use to verify the tool is operational before diagnosing.
|
|
141
|
-
'
|
|
176
|
+
'available). Use to verify the tool is operational before diagnosing. ' +
|
|
177
|
+
'Read-only. Works with zero setup on a bare npm install. ' +
|
|
178
|
+
'reports the availability of the packaged free tools independently of optional subsystems.',
|
|
142
179
|
inputSchema: { type: 'object', properties: {} },
|
|
143
180
|
},
|
|
144
181
|
];
|
|
@@ -167,12 +204,19 @@ async function handleToolsCall(id, params) {
|
|
|
167
204
|
}
|
|
168
205
|
const args = (params && params.arguments) || {};
|
|
169
206
|
try {
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
207
|
+
if (hasConfiguredBackend(tool.action)) {
|
|
208
|
+
const { result: r, exitCode } = await core.runAction(tool.action, args, { timeoutMs: TIMEOUT_MS });
|
|
209
|
+
result(id, {
|
|
210
|
+
content: [{ type: 'text', text: JSON.stringify(r, null, 2) }],
|
|
211
|
+
isError: exitCode !== 0,
|
|
212
|
+
});
|
|
213
|
+
} else {
|
|
214
|
+
const r = freeCore.run(tool.name, args);
|
|
215
|
+
result(id, {
|
|
216
|
+
content: [{ type: 'text', text: JSON.stringify(r, null, 2) }],
|
|
217
|
+
isError: false,
|
|
218
|
+
});
|
|
219
|
+
}
|
|
176
220
|
} catch (e) {
|
|
177
221
|
result(id, {
|
|
178
222
|
content: [{ type: 'text', text: JSON.stringify({ error: e && e.message ? e.message : String(e) }) }],
|
package/package.json
CHANGED
package/server.json
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
"transport": {
|
|
11
11
|
"type": "stdio"
|
|
12
12
|
},
|
|
13
|
-
"version": "1.0.
|
|
13
|
+
"version": "1.0.5"
|
|
14
14
|
}
|
|
15
15
|
],
|
|
16
16
|
"repository": {
|
|
@@ -18,6 +18,6 @@
|
|
|
18
18
|
"url": "https://github.com/Payloadhq/callx402"
|
|
19
19
|
},
|
|
20
20
|
"title": "callx402 MCP server",
|
|
21
|
-
"version": "1.0.
|
|
21
|
+
"version": "1.0.5",
|
|
22
22
|
"websiteUrl": "https://github.com/Payloadhq/callx402"
|
|
23
|
-
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
// Consumer smoke test for the actual npm artifact. Uses npm pack locally,
|
|
3
|
+
// installs nothing, sends no financial transactions or production requests.
|
|
4
|
+
const {test}=require('node:test');
|
|
5
|
+
const assert=require('node:assert/strict');
|
|
6
|
+
const fs=require('node:fs');
|
|
7
|
+
const os=require('node:os');
|
|
8
|
+
const path=require('node:path');
|
|
9
|
+
const cp=require('node:child_process');
|
|
10
|
+
const readline=require('node:readline');
|
|
11
|
+
|
|
12
|
+
test('npm-packed callx402 exposes usable free MCP diagnosis to a clean consumer',async()=>{
|
|
13
|
+
const root=path.resolve(__dirname,'..');
|
|
14
|
+
const temp=fs.mkdtempSync(path.join(os.tmpdir(),'callx402-npm-consumer-'));
|
|
15
|
+
let proc;
|
|
16
|
+
try{
|
|
17
|
+
const pack=cp.spawnSync('npm',['pack','--ignore-scripts','--json','--pack-destination',temp],{
|
|
18
|
+
cwd:root,encoding:'utf8',timeout:30000});
|
|
19
|
+
assert.equal(pack.status,0,pack.stderr);
|
|
20
|
+
// Newer npm versions differ in --json metadata; inspect the actual artifact
|
|
21
|
+
// instead of relying on a particular stdout schema.
|
|
22
|
+
const filename=fs.readdirSync(temp).find(name=>name.endsWith('.tgz'));
|
|
23
|
+
assert.ok(filename,'npm pack must produce a tarball in '+temp+
|
|
24
|
+
' (stdout: '+pack.stdout.slice(0,500)+')');
|
|
25
|
+
const bundle=path.join(temp,filename);
|
|
26
|
+
assert.ok(fs.existsSync(bundle),'packed npm tarball present');
|
|
27
|
+
const extracted=cp.spawnSync('tar',['-xzf',bundle,'-C',temp],{encoding:'utf8'});
|
|
28
|
+
assert.equal(extracted.status,0,extracted.stderr);
|
|
29
|
+
const entry=path.join(temp,'package','mcp','index.js');
|
|
30
|
+
assert.ok(fs.existsSync(entry),'MCP executable included in distributed npm archive');
|
|
31
|
+
proc=cp.spawn(process.execPath,[entry],{
|
|
32
|
+
cwd:temp,
|
|
33
|
+
env:{
|
|
34
|
+
...process.env,
|
|
35
|
+
CALLX402_V2_ROOT:path.join(temp,'absent-v2'),
|
|
36
|
+
CALLX402_CONFIG:path.join(temp,'empty-config.json'),
|
|
37
|
+
CALLX402_MODE:'local',
|
|
38
|
+
CALLX402_CORE_PATH:path.join(temp,'package','core')
|
|
39
|
+
},
|
|
40
|
+
stdio:['pipe','pipe','pipe']
|
|
41
|
+
});
|
|
42
|
+
let stderr='';proc.stderr.on('data',d=>stderr+=d);
|
|
43
|
+
let id=0;const wait=new Map();
|
|
44
|
+
readline.createInterface({input:proc.stdout}).on('line',line=>{
|
|
45
|
+
try{const r=JSON.parse(line),fn=wait.get(r.id);if(fn){wait.delete(r.id);fn(r);}}catch{}
|
|
46
|
+
});
|
|
47
|
+
const rpc=(method,params)=>new Promise((resolve,reject)=>{
|
|
48
|
+
const n=++id,t=setTimeout(()=>{wait.delete(n);reject(new Error('MCP timeout '+stderr.slice(0,300)));},5000);
|
|
49
|
+
wait.set(n,result=>{clearTimeout(t);resolve(result);});
|
|
50
|
+
proc.stdin.write(JSON.stringify({jsonrpc:'2.0',id:n,method,params})+'\n');
|
|
51
|
+
});
|
|
52
|
+
assert.ok((await rpc('initialize',{protocolVersion:'2024-11-05',capabilities:{},
|
|
53
|
+
clientInfo:{name:'fresh-npm-consumer',version:'1.0'}})).result);
|
|
54
|
+
const tools=(await rpc('tools/list',{})).result?.tools||[];
|
|
55
|
+
for(const name of ['x402_diagnose','x402_evidence','x402_explain','x402_recover','x402_resolve','x402_status'])
|
|
56
|
+
assert.ok(tools.some(t=>t.name===name),'missing advertised tool '+name);
|
|
57
|
+
const diagnostic=await rpc('tools/call',{
|
|
58
|
+
name:'x402_diagnose',
|
|
59
|
+
arguments:{evidence:JSON.stringify({httpStatus:402,
|
|
60
|
+
failure:'client could not parse challenge',network:'base'})}
|
|
61
|
+
});
|
|
62
|
+
assert.ok(diagnostic.result,'MCP tool response exists');
|
|
63
|
+
const output=JSON.stringify(diagnostic.result);
|
|
64
|
+
assert.ok(!/subsystem_unreachable|Cannot find module|missing v2\.0\.0|could not be loaded/i.test(output),
|
|
65
|
+
'fresh npm package must not demand an unpublished sibling module: '+output.slice(0,600));
|
|
66
|
+
assert.equal(diagnostic.result.isError,false,
|
|
67
|
+
'free diagnostic must work from a clean published-style npm artifact: '+output.slice(0,600));
|
|
68
|
+
for (const [name,args] of [
|
|
69
|
+
['x402_status',{}],
|
|
70
|
+
['x402_evidence',{operationId:'operation-fixture'}],
|
|
71
|
+
['x402_explain',{operationId:'operation-fixture'}],
|
|
72
|
+
['x402_recover',{operationId:'operation-fixture'}],
|
|
73
|
+
['x402_resolve',{evidence:JSON.stringify({txHash:'0x'+'a'.repeat(64),network:'base'})}]
|
|
74
|
+
]) {
|
|
75
|
+
const response=await rpc('tools/call',{name,arguments:args});
|
|
76
|
+
assert.equal(response.result?.isError,false,
|
|
77
|
+
name+' must respond safely on clean npm install: '+JSON.stringify(response));
|
|
78
|
+
const data=JSON.parse(response.result.content[0].text);
|
|
79
|
+
if(name==='x402_recover') assert.equal(data.canRetry,false,'no unauthenticated automatic recovery');
|
|
80
|
+
if(name==='x402_resolve') assert.equal(data.settlement,'UNKNOWN','no synthetic proof of settlement');
|
|
81
|
+
if(name==='x402_evidence') assert.equal(data.basis,'NO_BASIS','no invented private ledger entries');
|
|
82
|
+
}
|
|
83
|
+
}finally{if(proc)proc.kill('SIGTERM');fs.rmSync(temp,{recursive:true,force:true});}
|
|
84
|
+
});
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
const {test}=require('node:test');
|
|
3
|
+
const assert=require('node:assert/strict');
|
|
4
|
+
const fs=require('node:fs');
|
|
5
|
+
const os=require('node:os');
|
|
6
|
+
const path=require('node:path');
|
|
7
|
+
const {spawn}=require('node:child_process');
|
|
8
|
+
const readline=require('node:readline');
|
|
9
|
+
|
|
10
|
+
test('configured full doctor remains preferred to free fallback after npm packaging fix',async()=>{
|
|
11
|
+
const temp=fs.mkdtempSync(path.join(os.tmpdir(),'callx402-legacy-compat-'));
|
|
12
|
+
const d=path.join(temp,'lib','mcp');fs.mkdirSync(d,{recursive:true});
|
|
13
|
+
fs.writeFileSync(path.join(d,'doctor.js'),
|
|
14
|
+
"module.exports={FLAG:'FAKE_DOCTOR',enabled:()=>true,runMcpDoctor:()=>({whatFailed:'CONFIGURED_DOCTOR_USED',stages:[]})};\n");
|
|
15
|
+
const proc=spawn(process.execPath,[path.join(__dirname,'..','mcp','index.js')],{
|
|
16
|
+
cwd:temp,env:{
|
|
17
|
+
...process.env,CALLX402_V2_ROOT:temp,CALLX402_MODE:'local',
|
|
18
|
+
CALLX402_CONFIG:path.join(temp,'not-present.json'),
|
|
19
|
+
CALLX402_TIMEOUT_MS:'5000'
|
|
20
|
+
},stdio:['pipe','pipe','pipe']
|
|
21
|
+
});
|
|
22
|
+
let id=0,err='';const pending=new Map();
|
|
23
|
+
proc.stderr.on('data',b=>err+=b);
|
|
24
|
+
readline.createInterface({input:proc.stdout}).on('line',line=>{
|
|
25
|
+
try{const r=JSON.parse(line),fn=pending.get(r.id);if(fn){pending.delete(r.id);fn(r);}}catch{}
|
|
26
|
+
});
|
|
27
|
+
function rpc(method,params){
|
|
28
|
+
return new Promise((resolve,reject)=>{
|
|
29
|
+
const n=++id,t=setTimeout(()=>{pending.delete(n);reject(Error('timeout '+err.slice(0,200)));},4000);
|
|
30
|
+
pending.set(n,v=>{clearTimeout(t);resolve(v);});
|
|
31
|
+
proc.stdin.write(JSON.stringify({jsonrpc:'2.0',id:n,method,params})+'\n');
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
try {
|
|
35
|
+
const r=await rpc('tools/call',{name:'x402_diagnose',
|
|
36
|
+
arguments:{evidence:JSON.stringify({httpStatus:402})}});
|
|
37
|
+
assert.equal(r.result?.isError,false,JSON.stringify(r));
|
|
38
|
+
const payload=JSON.parse(r.result.content[0].text);
|
|
39
|
+
assert.equal(payload.data.report.whatFailed,'CONFIGURED_DOCTOR_USED',
|
|
40
|
+
'configured full-subsystem dispatch must not silently be replaced by free approximation');
|
|
41
|
+
}finally{proc.kill('SIGTERM');fs.rmSync(temp,{recursive:true,force:true});}
|
|
42
|
+
});
|