coinopai-mcp 2.2.4 → 2.2.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "coinopai-mcp",
3
- "version": "2.2.4",
3
+ "version": "2.2.5",
4
4
  "mcpName": "io.github.forgemeshlabs/coinopai-mcp",
5
5
  "description": "Auditable market intelligence MCP with x402 payments, calibrated forecast ranges, anomaly review, risk context, and outcome audits. The hosted MCP at https://x402.coinopai.com/mcp has a free handshake (initialize, ping, tools/list); only tool calls are charged",
6
6
  "main": "index.js",
package/server.json CHANGED
@@ -6,12 +6,12 @@
6
6
  "url": "https://github.com/forgemeshlabs/coinopai-mcp",
7
7
  "source": "github"
8
8
  },
9
- "version": "2.2.4",
9
+ "version": "2.2.5",
10
10
  "packages": [
11
11
  {
12
12
  "registryType": "npm",
13
13
  "identifier": "coinopai-mcp",
14
- "version": "2.2.4",
14
+ "version": "2.2.5",
15
15
  "transport": {
16
16
  "type": "stdio"
17
17
  },
package/x402-guard.js CHANGED
@@ -99,6 +99,19 @@ function createGuard(opts) {
99
99
  }
100
100
  }
101
101
 
102
+ // Keep the service's refusal intact: HTTP status, body (JSON when parseable) and the charged flag.
103
+ function httpError(res, prefix, charged) {
104
+ let upstream;
105
+ try { upstream = JSON.parse(res.text); } catch { upstream = res.text.slice(0, 500); }
106
+ const body = typeof upstream === "string" ? upstream : JSON.stringify(upstream).slice(0, 1500);
107
+ const err = new Error(`${prefix}HTTP ${res.status}: ${body}`);
108
+ err.status = res.status;
109
+ err.upstream = upstream;
110
+ if (charged !== undefined) err.charged = charged;
111
+ else if (upstream && typeof upstream === "object" && typeof upstream.charged === "boolean") err.charged = upstream.charged;
112
+ return err;
113
+ }
114
+
102
115
  function parseJson(res) {
103
116
  try { return JSON.parse(res.text); } catch { throw new Error(`HTTP ${res.status}: non-JSON response`); }
104
117
  }
@@ -111,7 +124,7 @@ function createGuard(opts) {
111
124
  const init = { method, headers: { ...headers, ...(body ? { "content-type": "application/json" } : {}) }, ...(body ? { body: JSON.stringify(body) } : {}) };
112
125
  const res = await fetchBounded(url, init);
113
126
  if (res.status !== 402) {
114
- if (!res.ok) throw new Error(`HTTP ${res.status}: ${res.text.slice(0, 200)}`);
127
+ if (!res.ok) throw httpError(res, "", false); // nothing was signed, so nothing was charged
115
128
  return parseJson(res);
116
129
  }
117
130
  let challenge;
@@ -119,7 +132,7 @@ function createGuard(opts) {
119
132
  const paymentRequired = httpClient.getPaymentRequiredResponse((n) => res.headers.get(n), challenge);
120
133
  const payload = await httpClient.createPaymentPayload(paymentRequired); // policy runs (and reserves budget) inside
121
134
  const paid = await fetchBounded(url, { ...init, headers: { ...init.headers, ...httpClient.encodePaymentSignatureHeader(payload) } });
122
- if (!paid.ok) throw new Error(`Payment failed — HTTP ${paid.status}: ${paid.text.slice(0, 200)}`);
135
+ if (!paid.ok) throw httpError(paid, "Payment failed — ");
123
136
  let settle = null;
124
137
  try { settle = httpClient.getPaymentSettleResponse((n) => paid.headers.get(n)) || null; } catch { settle = null; }
125
138
  const contentType = paid.headers.get("content-type") || "";