@bsvkey/inference-mcp 1.1.2 → 1.2.1
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/README.md +8 -5
- package/package.json +2 -2
- package/server.js +27 -9
package/README.md
CHANGED
|
@@ -14,11 +14,14 @@ optional `@bsvkey/x402-bsv-client` + `@bsv/sdk` packages (installed with this on
|
|
|
14
14
|
|
|
15
15
|
**Verifiable metering.** Each `infer` call returns a signed usage receipt, and the
|
|
16
16
|
tool auto-verifies it offline (with the optional packages installed): the result
|
|
17
|
-
includes `receiptVerified` (`true`, `false` + `receiptCheck`,
|
|
18
|
-
verifier isn't installed). It recovers the broker key (pinned
|
|
19
|
-
`GET /v1/receipt-key`)
|
|
20
|
-
replay/gap), and running totals within the funded amount
|
|
21
|
-
|
|
17
|
+
includes `receiptVerified` and `meterVerified` (`true`, `false` + `receiptCheck`,
|
|
18
|
+
or `null` if the verifier isn't installed). It recovers the broker key (pinned
|
|
19
|
+
from `GET /v1/receipt-key`); checks channel binding, a monotonic sequence (no
|
|
20
|
+
replay/gap), and running totals within the funded amount; **recomputes the charge**
|
|
21
|
+
from the published rate (you can never be overcharged); and **recomputes the token
|
|
22
|
+
count from the exact bytes** of your system/prompt and the completion, under the
|
|
23
|
+
pinned `bsvkey-meter/1` tokenizer. So the channel payment is on-chain and both the
|
|
24
|
+
meter and the charge are auditable, without trusting the broker's word. Spec:
|
|
22
25
|
https://inference.bsvkey.com/usage-receipts.md
|
|
23
26
|
|
|
24
27
|
## Quick start
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bsvkey/inference-mcp",
|
|
3
|
-
"version": "1.1
|
|
3
|
+
"version": "1.2.1",
|
|
4
4
|
"description": "MCP server: buy Claude & Grok inference metered per token, settled in BSV, through the hosted inference.bsvkey.com gateway. Channels or per-call x402.",
|
|
5
5
|
"mcpName": "io.github.BSVKey/inference-mcp",
|
|
6
6
|
"type": "module",
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
"engines": { "node": ">=18" },
|
|
10
10
|
"files": ["server.js", "SKILL.md", "README.md", "LICENSE"],
|
|
11
11
|
"optionalDependencies": {
|
|
12
|
-
"@bsvkey/x402-bsv-client": "^0.
|
|
12
|
+
"@bsvkey/x402-bsv-client": "^0.4.2",
|
|
13
13
|
"@bsv/sdk": "^2"
|
|
14
14
|
},
|
|
15
15
|
"keywords": ["mcp", "model-context-protocol", "bsv", "inference", "pay-per-token", "http-402", "x402", "claude", "grok", "micropayments", "agents"],
|
package/server.js
CHANGED
|
@@ -63,7 +63,7 @@ async function brokerReceiptKey() {
|
|
|
63
63
|
try { const r = await http('GET', '/receipt-key'); _brokerKey = r.ok ? (r.json.receiptPubKey || null) : null; } catch { _brokerKey = null; }
|
|
64
64
|
return _brokerKey;
|
|
65
65
|
}
|
|
66
|
-
async function verifyUsageReceipt(receipt) {
|
|
66
|
+
async function verifyUsageReceipt(receipt, bytes = {}) {
|
|
67
67
|
if (!receipt) return { verified: null, reason: 'no receipt returned' };
|
|
68
68
|
const V = await loadVerifier();
|
|
69
69
|
if (!V) return { verified: null, reason: 'verifier not installed (npm i @bsvkey/x402-bsv-client)' };
|
|
@@ -71,6 +71,18 @@ async function verifyUsageReceipt(receipt) {
|
|
|
71
71
|
if (!one.ok) return { verified: false, reason: one.reason };
|
|
72
72
|
const pinned = await brokerReceiptKey();
|
|
73
73
|
if (pinned && one.signer !== pinned) return { verified: false, reason: 'signer_not_pinned_broker_key' };
|
|
74
|
+
// The charge recomputes from the receipt's own rates (overcharge is a dispute).
|
|
75
|
+
if (typeof V.verifyCharge === 'function') {
|
|
76
|
+
const c = V.verifyCharge(receipt);
|
|
77
|
+
if (!c.ok) return { verified: false, reason: `charge:${c.reason}` };
|
|
78
|
+
}
|
|
79
|
+
// The meter: recompute token counts + byte digests from the exact bytes we hold.
|
|
80
|
+
let meterVerified = null;
|
|
81
|
+
if (typeof V.verifyMeter === 'function' && (bytes.messages !== undefined || bytes.prompt !== undefined || bytes.completion !== undefined)) {
|
|
82
|
+
const mv = V.verifyMeter(receipt, bytes);
|
|
83
|
+
if (!mv.ok) return { verified: false, reason: `meter:${mv.reason}`, meterVerified: false };
|
|
84
|
+
meterVerified = true;
|
|
85
|
+
}
|
|
74
86
|
if (receipt.cumSats > receipt.fundedSats) return { verified: false, reason: 'cumSats_exceeds_funded' };
|
|
75
87
|
const prev = _seen.get(receipt.channelId);
|
|
76
88
|
if (prev) {
|
|
@@ -80,7 +92,7 @@ async function verifyUsageReceipt(receipt) {
|
|
|
80
92
|
if (receipt.cumTokens !== prev.cumTokens + receipt.inputTokens + receipt.outputTokens) return { verified: false, reason: 'cumTokens_does_not_reconcile' };
|
|
81
93
|
}
|
|
82
94
|
_seen.set(receipt.channelId, { seq: receipt.seq, cumSats: receipt.cumSats, cumTokens: receipt.cumTokens });
|
|
83
|
-
return { verified: true, ...(prev ? {} : { note: 'baseline: signature + funded-conservation checked; seq continuity verified from here' }) };
|
|
95
|
+
return { verified: true, meterVerified, ...(prev ? {} : { note: 'baseline: signature + funded-conservation checked; seq continuity verified from here' }) };
|
|
84
96
|
}
|
|
85
97
|
|
|
86
98
|
export const TOOLS = [
|
|
@@ -162,14 +174,15 @@ async function callTool(name, args = {}) {
|
|
|
162
174
|
case 'infer': {
|
|
163
175
|
const k = keyParts(args.apiKey);
|
|
164
176
|
if (!k) throw new Error('No channel key. Pass apiKey "channelId:channelSecret" or set BSVKEY_API_KEY. Open one via the open_channel tool.');
|
|
177
|
+
const messages = [
|
|
178
|
+
...(args.system ? [{ role: 'system', content: args.system }] : []),
|
|
179
|
+
{ role: 'user', content: String(args.prompt || '') },
|
|
180
|
+
];
|
|
165
181
|
const r = await http('POST', '/chat/completions', {
|
|
166
182
|
headers: { authorization: `Bearer ${k.raw}` },
|
|
167
183
|
body: {
|
|
168
184
|
model: args.model || 'auto',
|
|
169
|
-
messages
|
|
170
|
-
...(args.system ? [{ role: 'system', content: args.system }] : []),
|
|
171
|
-
{ role: 'user', content: String(args.prompt || '') },
|
|
172
|
-
],
|
|
185
|
+
messages,
|
|
173
186
|
max_tokens: args.maxTokens || 512,
|
|
174
187
|
web_search: args.webSearch === true,
|
|
175
188
|
},
|
|
@@ -179,18 +192,23 @@ async function callTool(name, args = {}) {
|
|
|
179
192
|
throw new Error(typeof msg === 'string' ? msg : JSON.stringify(msg));
|
|
180
193
|
}
|
|
181
194
|
const x = r.json.x_bsv || {};
|
|
182
|
-
const
|
|
195
|
+
const completion = r.json.choices?.[0]?.message?.content ?? '';
|
|
196
|
+
// Meter over the SAME messages we sent: the OpenAI shim meters the flattened
|
|
197
|
+
// messages, so the verifier must reproduce that transform (needs @bsvkey/x402-bsv-client >= 0.4.2).
|
|
198
|
+
const rc = await verifyUsageReceipt(x.usageReceipt, { messages, completion });
|
|
183
199
|
return {
|
|
184
200
|
model: r.json.model || args.model,
|
|
185
|
-
completion
|
|
201
|
+
completion,
|
|
186
202
|
charge: x.charge,
|
|
187
203
|
routedTo: x.routedTo,
|
|
188
204
|
balanceSatsAfter: x.balanceSatsAfter,
|
|
189
205
|
truncated: x.truncated || false,
|
|
190
206
|
// Offline-verified signed usage receipt (see usage-receipt spec). null =
|
|
191
207
|
// not checked (verifier not installed); false w/ receiptCheck = a real
|
|
192
|
-
// mismatch, treat the meter as untrusted for this call.
|
|
208
|
+
// mismatch, treat the meter as untrusted for this call. meterVerified is
|
|
209
|
+
// true when the token count was recomputed from the exact bytes.
|
|
193
210
|
receiptVerified: rc.verified,
|
|
211
|
+
meterVerified: rc.meterVerified,
|
|
194
212
|
...(rc.reason ? { receiptCheck: rc.reason } : {}),
|
|
195
213
|
usageReceipt: x.usageReceipt,
|
|
196
214
|
};
|