callx402 1.0.4 → 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 +1 -4
- package/mcp/README.md +2 -6
- package/mcp/free-core.js +45 -0
- package/mcp/index.js +67 -56
- 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
|
|
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,13 +18,9 @@
|
|
|
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
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
* return `subsystem_unreachable` until CALLX402_V2_ROOT points at that tree
|
|
25
|
-
* and the matching feature flag is enabled. These tools never call the
|
|
26
|
-
* hosted rail; paid hosted equivalents live at
|
|
27
|
-
* https://payloadhq.github.io/agents.json.
|
|
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.
|
|
28
24
|
*
|
|
29
25
|
* Usage (Claude Desktop / MCP client config):
|
|
30
26
|
* { "command": "node", "args": ["/path/to/callx402/mcp/index.js"] }
|
|
@@ -32,38 +28,51 @@
|
|
|
32
28
|
*/
|
|
33
29
|
const readline = require('node:readline');
|
|
34
30
|
|
|
35
|
-
const VERSION = '1.0.
|
|
31
|
+
const VERSION = '1.0.5';
|
|
36
32
|
const PROTOCOL_VERSION = '2024-11-05';
|
|
37
33
|
const SERVER_NAME = 'callx402';
|
|
38
34
|
const TIMEOUT_MS = Number(process.env.CALLX402_TIMEOUT_MS || 30000);
|
|
39
35
|
|
|
40
|
-
|
|
41
|
-
|
|
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
|
+
}
|
|
42
67
|
|
|
43
68
|
/* ------------------------------------------------------------------ */
|
|
44
|
-
/* Tool definitions:
|
|
69
|
+
/* Tool definitions: packaged read-only diagnostics only. */
|
|
45
70
|
/* */
|
|
46
|
-
/*
|
|
47
|
-
/* description is what an MCP client actually shows the user: */
|
|
48
|
-
/* TREE_REQUIRED — dispatches into the v2.0.0 subsystem tree, which */
|
|
49
|
-
/* is NOT part of this package. */
|
|
50
|
-
/* LEDGER_REQUIRED — reads the local Veyline operation ledger via */
|
|
51
|
-
/* modules that are likewise not bundled here. */
|
|
71
|
+
/* Caller-supplied evidence is unverified. No private ledger or funds. */
|
|
52
72
|
/* ------------------------------------------------------------------ */
|
|
53
|
-
const TREE_REQUIRED =
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
'tree and enable the subsystem flag, or this tool returns ' +
|
|
57
|
-
'subsystem_unreachable and does nothing. With the tree configured it is free ' +
|
|
58
|
-
'and read-only: no payment, no auth. A paid hosted equivalent runs on the ' +
|
|
59
|
-
'Payload rail — see https://payloadhq.github.io/agents.json.';
|
|
60
|
-
const ledgerRequired = (flag) =>
|
|
61
|
-
' Requires the local Veyline ledger modules (NOT included in this package; ' +
|
|
62
|
-
`enable with ${flag}=1 where the ledger lives) and reads ` +
|
|
63
|
-
'operations recorded on THIS machine — it cannot read back hosted rail ' +
|
|
64
|
-
'invocations. Until configured it returns subsystem_unreachable and does ' +
|
|
65
|
-
'nothing. When configured it is free and read-only: no payment, no auth.';
|
|
66
|
-
const LEDGER_REQUIRED = ledgerRequired('PAYLOAD_VEYLINE_LEDGER');
|
|
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();
|
|
67
76
|
|
|
68
77
|
const TOOLS = [
|
|
69
78
|
{
|
|
@@ -80,7 +89,7 @@ const TOOLS = [
|
|
|
80
89
|
properties: {
|
|
81
90
|
evidence: {
|
|
82
91
|
type: 'string',
|
|
83
|
-
description: 'JSON string
|
|
92
|
+
description: 'JSON string describing non-sensitive failure evidence; local file references are unsupported.',
|
|
84
93
|
},
|
|
85
94
|
target: {
|
|
86
95
|
type: 'string',
|
|
@@ -94,15 +103,14 @@ const TOOLS = [
|
|
|
94
103
|
name: 'x402_evidence',
|
|
95
104
|
action: 'evidence',
|
|
96
105
|
description:
|
|
97
|
-
'Show
|
|
98
|
-
'
|
|
99
|
-
'Use FIRST in any incident — establishes ground truth before acting. Read-only.' +
|
|
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.' +
|
|
100
108
|
LEDGER_REQUIRED,
|
|
101
109
|
inputSchema: {
|
|
102
110
|
type: 'object',
|
|
103
111
|
properties: {
|
|
104
112
|
operationId: { type: 'string', description: 'The operation ID to look up.' },
|
|
105
|
-
|
|
113
|
+
evidence: { type: 'string', description: 'Optional sanitized JSON evidence; otherwise NO_BASIS.' },
|
|
106
114
|
},
|
|
107
115
|
required: ['operationId'],
|
|
108
116
|
},
|
|
@@ -111,16 +119,14 @@ const TOOLS = [
|
|
|
111
119
|
name: 'x402_explain',
|
|
112
120
|
action: 'explain',
|
|
113
121
|
description:
|
|
114
|
-
'
|
|
115
|
-
'
|
|
116
|
-
'KNOWN_SAFE, RECOVERY_CANDIDATE, or PARTIAL. Use to understand an incident ' +
|
|
117
|
-
'before deciding whether to retry, refund, or escalate. Read-only.' +
|
|
122
|
+
'Provide a conservative interpretation of caller-supplied evidence only; ' +
|
|
123
|
+
'no independent payment or settlement verification is performed. Read-only.' +
|
|
118
124
|
LEDGER_REQUIRED,
|
|
119
125
|
inputSchema: {
|
|
120
126
|
type: 'object',
|
|
121
127
|
properties: {
|
|
122
128
|
operationId: { type: 'string', description: 'The operation ID to explain.' },
|
|
123
|
-
|
|
129
|
+
evidence: { type: 'string', description: 'Optional sanitized JSON evidence; otherwise NO_BASIS.' },
|
|
124
130
|
},
|
|
125
131
|
required: ['operationId'],
|
|
126
132
|
},
|
|
@@ -131,9 +137,9 @@ const TOOLS = [
|
|
|
131
137
|
description:
|
|
132
138
|
'Report the safe recovery decision for an operation whose result was lost ' +
|
|
133
139
|
'(paid but no result). READ-ONLY: nothing is charged, nothing is executed. ' +
|
|
134
|
-
'
|
|
135
|
-
'
|
|
136
|
-
ledgerRequired(
|
|
140
|
+
'Without an authoritative settlement source, return HUMAN_REVIEW and never retry. ' +
|
|
141
|
+
'This free diagnostic cannot execute recovery.' +
|
|
142
|
+
ledgerRequired(),
|
|
137
143
|
inputSchema: {
|
|
138
144
|
type: 'object',
|
|
139
145
|
properties: {
|
|
@@ -148,8 +154,8 @@ const TOOLS = [
|
|
|
148
154
|
name: 'x402_resolve',
|
|
149
155
|
action: 'resolve',
|
|
150
156
|
description:
|
|
151
|
-
'
|
|
152
|
-
'
|
|
157
|
+
'Classify caller-provided settlement evidence conservatively. Returns UNKNOWN ' +
|
|
158
|
+
'without authoritative chain verification; no retry is performed. Read-only.' +
|
|
153
159
|
TREE_REQUIRED,
|
|
154
160
|
inputSchema: {
|
|
155
161
|
type: 'object',
|
|
@@ -168,10 +174,8 @@ const TOOLS = [
|
|
|
168
174
|
description:
|
|
169
175
|
'Check callx402 subsystem reachability (which diagnostic subsystems are ' +
|
|
170
176
|
'available). Use to verify the tool is operational before diagnosing. ' +
|
|
171
|
-
'Read-only.
|
|
172
|
-
'
|
|
173
|
-
'honestly reports 0/15 subsystems reachable: the other five tools need ' +
|
|
174
|
-
'the v2.0.0 subsystem tree (CALLX402_V2_ROOT) before they can answer.',
|
|
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.',
|
|
175
179
|
inputSchema: { type: 'object', properties: {} },
|
|
176
180
|
},
|
|
177
181
|
];
|
|
@@ -200,12 +204,19 @@ async function handleToolsCall(id, params) {
|
|
|
200
204
|
}
|
|
201
205
|
const args = (params && params.arguments) || {};
|
|
202
206
|
try {
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
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
|
+
}
|
|
209
220
|
} catch (e) {
|
|
210
221
|
result(id, {
|
|
211
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
|
+
});
|