@aibtc/mcp-server 1.60.0 → 1.62.0
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 +2 -0
- package/dist/bridge/index.js +26 -6
- package/dist/tools/dual-stacking.tools.js +25 -3
- package/dist/tools/index.js +3 -0
- package/dist/tools/inference-marketplace.tools.d.ts +30 -0
- package/dist/tools/inference-marketplace.tools.js +303 -0
- package/dist/tools/lightning.tools.js +30 -0
- package/package.json +1 -1
- package/skill/SKILL.md +28 -1
package/README.md
CHANGED
|
@@ -99,6 +99,8 @@ npx @aibtc/mcp-server@latest bridge --read-only --list-tools
|
|
|
99
99
|
npx @aibtc/mcp-server@latest bridge --read-only --allow transfer_stx "send 1 STX to SP3..."
|
|
100
100
|
```
|
|
101
101
|
|
|
102
|
+
Before any agent loop runs (and on `--list-tools`), the bridge prints a compact **safety receipt** to stderr — network, read-only mode, exposed/write/blocked tool counts, the session spend cap, and the number of known x402 endpoints — so the configured execution boundaries are visible up front. It reports boundaries only; it never claims any value moved.
|
|
103
|
+
|
|
102
104
|
The allowlist is re-enforced at execution time, so a model can never call a tool outside the exposed set. Any MCP-capable agent framework (`@openrouter/agent`, OpenAI Agents SDK, Claude Agent SDK) can also point at this server directly — the bridge is for driving it through OpenRouter's raw API without adopting a framework.
|
|
103
105
|
|
|
104
106
|
### Testnet Mode
|
package/dist/bridge/index.js
CHANGED
|
@@ -29,6 +29,7 @@
|
|
|
29
29
|
*/
|
|
30
30
|
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
31
31
|
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
|
|
32
|
+
import { ALL_ENDPOINTS } from "../endpoints/registry.js";
|
|
32
33
|
const OPENROUTER_URL = "https://openrouter.ai/api/v1/chat/completions";
|
|
33
34
|
const DEFAULT_MODEL = "anthropic/claude-3.5-haiku";
|
|
34
35
|
/**
|
|
@@ -136,6 +137,28 @@ function parseArgs(argv) {
|
|
|
136
137
|
opts.prompt = promptWords.join(" ").trim();
|
|
137
138
|
return opts;
|
|
138
139
|
}
|
|
140
|
+
/**
|
|
141
|
+
* Compact safety receipt printed before any agent loop runs (#583). Makes the
|
|
142
|
+
* configured execution boundaries visible up front so a user can trust the
|
|
143
|
+
* bridge without digging through flags/config. It reports ONLY configured
|
|
144
|
+
* boundaries — it never claims that any value moved. Emitted on stderr so it
|
|
145
|
+
* can't pollute stdout (the final assistant reply / --list-tools piping).
|
|
146
|
+
*/
|
|
147
|
+
function printSafetyReceipt(opts, exposed, totalTools) {
|
|
148
|
+
const writeToolCount = exposed.filter((t) => !isReadOnly(t.name)).length;
|
|
149
|
+
const lines = [
|
|
150
|
+
"aibtc-mcp bridge — safety receipt",
|
|
151
|
+
` network=${opts.network}`,
|
|
152
|
+
` read_only=${opts.readOnly}`,
|
|
153
|
+
` exposed_tools=${exposed.length}/${totalTools}`,
|
|
154
|
+
` write_tool_count=${writeToolCount}`,
|
|
155
|
+
` blocked_tool_count=${opts.block.size}`,
|
|
156
|
+
` session_spend_cap=${opts.maxSpendUstx ?? "default"} uSTX / ${opts.maxSpendSats ?? "default"} sats`,
|
|
157
|
+
` x402_endpoints_known=${ALL_ENDPOINTS.length}`,
|
|
158
|
+
" (reports configured execution boundaries only; no value has moved)",
|
|
159
|
+
];
|
|
160
|
+
console.error(lines.join("\n"));
|
|
161
|
+
}
|
|
139
162
|
/** Apply read-only + allow + block to the raw MCP tool list. */
|
|
140
163
|
function selectTools(tools, opts) {
|
|
141
164
|
return tools.filter((t) => {
|
|
@@ -193,6 +216,7 @@ export async function runBridge(argv) {
|
|
|
193
216
|
const { tools: allTools } = await client.listTools();
|
|
194
217
|
const exposed = selectTools(allTools, opts);
|
|
195
218
|
if (opts.listTools) {
|
|
219
|
+
printSafetyReceipt(opts, exposed, allTools.length);
|
|
196
220
|
console.log(`Exposed ${exposed.length}/${allTools.length} tools` +
|
|
197
221
|
(opts.readOnly ? " (read-only)" : "") + ":");
|
|
198
222
|
for (const t of exposed)
|
|
@@ -211,12 +235,8 @@ export async function runBridge(argv) {
|
|
|
211
235
|
await client.close();
|
|
212
236
|
process.exit(1);
|
|
213
237
|
}
|
|
214
|
-
|
|
215
|
-
console.error(`
|
|
216
|
-
(opts.readOnly ? " [read-only]" : ` [${writeCount} can write/spend]`) +
|
|
217
|
-
(opts.maxSpendUstx || opts.maxSpendSats
|
|
218
|
-
? ` (cap ${opts.maxSpendUstx ?? "-"} uSTX / ${opts.maxSpendSats ?? "-"} sats)`
|
|
219
|
-
: ""));
|
|
238
|
+
printSafetyReceipt(opts, exposed, allTools.length);
|
|
239
|
+
console.error(` model=${opts.model}, max_turns=${opts.maxTurns}`);
|
|
220
240
|
const openaiTools = exposed.map(toOpenAITool);
|
|
221
241
|
const messages = [
|
|
222
242
|
{
|
|
@@ -64,13 +64,33 @@ Note: Dual Stacking is only available on mainnet.`,
|
|
|
64
64
|
});
|
|
65
65
|
}
|
|
66
66
|
const resolvedAddress = address || (await getWalletAddress());
|
|
67
|
-
|
|
67
|
+
// Use allSettled, not all: `is-enrolled-this-cycle` fails contract-side
|
|
68
|
+
// on mainnet with RuntimeCheck(AtBlockUnavailable) (it reads historical
|
|
69
|
+
// block state Hiro has pruned). With Promise.all a single failing read
|
|
70
|
+
// sinks the whole response, hiding the other 4 useful fields. Recover
|
|
71
|
+
// per-call and surface a `warnings` array instead. (#554)
|
|
72
|
+
const warnings = [];
|
|
73
|
+
const settled = await Promise.allSettled([
|
|
68
74
|
callDualStackingReadOnly("is-enrolled-this-cycle", [principalCV(resolvedAddress)]),
|
|
69
75
|
callDualStackingReadOnly("is-enrolled-in-next-cycle", [principalCV(resolvedAddress)]),
|
|
70
76
|
callDualStackingReadOnly("get-minimum-enrollment-amount", []),
|
|
71
77
|
callDualStackingReadOnly("get-apr-data", []),
|
|
72
78
|
callDualStackingReadOnly("current-overview-data", []),
|
|
73
79
|
]);
|
|
80
|
+
const readNames = [
|
|
81
|
+
"is-enrolled-this-cycle",
|
|
82
|
+
"is-enrolled-in-next-cycle",
|
|
83
|
+
"get-minimum-enrollment-amount",
|
|
84
|
+
"get-apr-data",
|
|
85
|
+
"current-overview-data",
|
|
86
|
+
];
|
|
87
|
+
const values = settled.map((r, i) => {
|
|
88
|
+
if (r.status === "fulfilled")
|
|
89
|
+
return r.value;
|
|
90
|
+
warnings.push(`${readNames[i]}: ${r.reason instanceof Error ? r.reason.message : String(r.reason)}`);
|
|
91
|
+
return null;
|
|
92
|
+
});
|
|
93
|
+
const [enrolledThisCycleRaw, enrolledNextCycleRaw, minimumAmountRaw, aprDataRaw, cycleOverviewRaw,] = values;
|
|
74
94
|
// Parse APR data — returns {min-apr: uint, max-apr: uint} divided by 1_000_000 for %
|
|
75
95
|
let apr = {
|
|
76
96
|
minApr: 0,
|
|
@@ -107,10 +127,11 @@ Note: Dual Stacking is only available on mainnet.`,
|
|
|
107
127
|
const raw = minimumAmountRaw;
|
|
108
128
|
minimumEnrollmentSats = raw.value !== undefined ? Number(raw.value) : 0;
|
|
109
129
|
}
|
|
110
|
-
// Parse boolean enrollment flags
|
|
130
|
+
// Parse boolean enrollment flags. A failed read stays null (unknown)
|
|
131
|
+
// rather than collapsing to a misleading `false`.
|
|
111
132
|
const parseBoolean = (raw) => {
|
|
112
133
|
if (raw === null || raw === undefined)
|
|
113
|
-
return
|
|
134
|
+
return null;
|
|
114
135
|
if (typeof raw === "boolean")
|
|
115
136
|
return raw;
|
|
116
137
|
const obj = raw;
|
|
@@ -126,6 +147,7 @@ Note: Dual Stacking is only available on mainnet.`,
|
|
|
126
147
|
minimumEnrollmentSats,
|
|
127
148
|
apr,
|
|
128
149
|
cycleOverview,
|
|
150
|
+
...(warnings.length > 0 && { warnings }),
|
|
129
151
|
});
|
|
130
152
|
}
|
|
131
153
|
catch (error) {
|
package/dist/tools/index.js
CHANGED
|
@@ -34,6 +34,7 @@ import { registerOrdinalsMarketplaceTools } from "./ordinals-marketplace.tools.j
|
|
|
34
34
|
import { registerTaprootMultisigTools } from "./taproot-multisig.tools.js";
|
|
35
35
|
import { registerJingswapTools } from "./jingswap.tools.js";
|
|
36
36
|
import { registerSigningTools } from "./signing.tools.js";
|
|
37
|
+
import { registerInferenceMarketplaceTools } from "./inference-marketplace.tools.js";
|
|
37
38
|
import { registerNewsTools } from "./news.tools.js";
|
|
38
39
|
import { registerIdentityTools } from "./identity.tools.js";
|
|
39
40
|
import { registerCredentialsTools } from "./credentials.tools.js";
|
|
@@ -160,6 +161,8 @@ export function registerAllTools(server) {
|
|
|
160
161
|
registerJingswapTools(server);
|
|
161
162
|
// Message Signing (BTC BIP-322, Stacks SIWS, SIP-018 structured data, Nostr NIP-01)
|
|
162
163
|
registerSigningTools(server);
|
|
164
|
+
// AIBTC Inference Marketplace (list/manage a paid model endpoint via wallet signature)
|
|
165
|
+
registerInferenceMarketplaceTools(server);
|
|
163
166
|
// AIBTC News (signal feed, leaderboard, file signals)
|
|
164
167
|
registerNewsTools(server);
|
|
165
168
|
// Identity (ERC-8004 on-chain agent identity management)
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AIBTC Inference Marketplace Tools
|
|
3
|
+
*
|
|
4
|
+
* List an OpenAI-compatible model endpoint on the marketplace and manage it, all
|
|
5
|
+
* authorized by a wallet signature (the same wallet that receives sBTC payouts).
|
|
6
|
+
* Ownership is proven by signing a short message with the payout wallet — no
|
|
7
|
+
* accounts, no API keys. These tools sign locally with the unlocked wallet and
|
|
8
|
+
* call the gateway, so an agent can register/update/check in one step.
|
|
9
|
+
*
|
|
10
|
+
* - inference_register_provider: verify + list an endpoint (signed)
|
|
11
|
+
* - inference_update_provider: change name/models/payout/endpoint in place (signed)
|
|
12
|
+
* - inference_reveal_key: reveal or rotate the gateway↔endpoint shared key (signed)
|
|
13
|
+
* - inference_check_provider: re-run the health/functional probe (unsigned)
|
|
14
|
+
* - inference_list_providers: list registered providers + health (unsigned)
|
|
15
|
+
*
|
|
16
|
+
* The gateway verifies the signature recovers to `payoutAddress` (which also
|
|
17
|
+
* enforces the network), that the endpoint is reachable AND serving inference,
|
|
18
|
+
* and that model ids are real, text-generation, commercially-licensed HF repos.
|
|
19
|
+
*/
|
|
20
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
21
|
+
type AuthAction = "register" | "update" | "reveal-key";
|
|
22
|
+
/** The exact message the gateway rebuilds and verifies. Must match byte-for-byte. */
|
|
23
|
+
export declare function authMessage(action: AuthAction, providerId: string, timestamp: number): string;
|
|
24
|
+
/** Sign an auth message with the unlocked wallet and return the gateway headers. */
|
|
25
|
+
export declare function signAuthHeaders(account: {
|
|
26
|
+
privateKey: string;
|
|
27
|
+
}, action: AuthAction, providerId: string): Record<string, string>;
|
|
28
|
+
export declare function registerInferenceMarketplaceTools(server: McpServer): void;
|
|
29
|
+
export {};
|
|
30
|
+
//# sourceMappingURL=inference-marketplace.tools.d.ts.map
|
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AIBTC Inference Marketplace Tools
|
|
3
|
+
*
|
|
4
|
+
* List an OpenAI-compatible model endpoint on the marketplace and manage it, all
|
|
5
|
+
* authorized by a wallet signature (the same wallet that receives sBTC payouts).
|
|
6
|
+
* Ownership is proven by signing a short message with the payout wallet — no
|
|
7
|
+
* accounts, no API keys. These tools sign locally with the unlocked wallet and
|
|
8
|
+
* call the gateway, so an agent can register/update/check in one step.
|
|
9
|
+
*
|
|
10
|
+
* - inference_register_provider: verify + list an endpoint (signed)
|
|
11
|
+
* - inference_update_provider: change name/models/payout/endpoint in place (signed)
|
|
12
|
+
* - inference_reveal_key: reveal or rotate the gateway↔endpoint shared key (signed)
|
|
13
|
+
* - inference_check_provider: re-run the health/functional probe (unsigned)
|
|
14
|
+
* - inference_list_providers: list registered providers + health (unsigned)
|
|
15
|
+
*
|
|
16
|
+
* The gateway verifies the signature recovers to `payoutAddress` (which also
|
|
17
|
+
* enforces the network), that the endpoint is reachable AND serving inference,
|
|
18
|
+
* and that model ids are real, text-generation, commercially-licensed HF repos.
|
|
19
|
+
*/
|
|
20
|
+
import { z } from "zod";
|
|
21
|
+
import { signMessageHashRsv, privateKeyToPublic, compressPublicKey, } from "@stacks/transactions";
|
|
22
|
+
import { hashMessage } from "@stacks/encryption";
|
|
23
|
+
import { bytesToHex } from "@stacks/common";
|
|
24
|
+
import { createJsonResponse, createErrorResponse } from "../utils/index.js";
|
|
25
|
+
import { getWalletManager } from "../services/wallet-manager.js";
|
|
26
|
+
/** Default marketplace gateway; override with the `gateway` arg for local dev. */
|
|
27
|
+
const DEFAULT_GATEWAY = "https://inference.aibtc.com";
|
|
28
|
+
/** Bound network calls so a hung endpoint can't block the tool call forever. */
|
|
29
|
+
const GATEWAY_TIMEOUT_MS = 15_000;
|
|
30
|
+
/** Loopback hosts always allowed for local dev. */
|
|
31
|
+
const LOOPBACK_HOSTS = new Set(["localhost", "127.0.0.1", "[::1]", "::1"]);
|
|
32
|
+
/**
|
|
33
|
+
* These tools sign an ownership message with the payout wallet and may forward
|
|
34
|
+
* the endpoint's own `apiKey` — both secrets. If the gateway destination were
|
|
35
|
+
* free-form, a prompt-injected "use gateway https://attacker.example" could
|
|
36
|
+
* redirect that signed request and leak the signature + apiKey. So the override
|
|
37
|
+
* is restricted to localhost and the marketplace (`*.aibtc.com`); any other host
|
|
38
|
+
* requires an explicit `allowUnsafeGateway: true`, which natural language alone
|
|
39
|
+
* won't set.
|
|
40
|
+
*/
|
|
41
|
+
function isAllowedGatewayHost(hostname) {
|
|
42
|
+
const host = hostname.toLowerCase();
|
|
43
|
+
if (LOOPBACK_HOSTS.has(host))
|
|
44
|
+
return true;
|
|
45
|
+
return host === "aibtc.com" || host.endsWith(".aibtc.com");
|
|
46
|
+
}
|
|
47
|
+
/** Validate + normalize the gateway base URL (trailing slash stripped). */
|
|
48
|
+
function resolveGateway(gateway, allowUnsafe) {
|
|
49
|
+
const base = (gateway ?? DEFAULT_GATEWAY).replace(/\/$/, "");
|
|
50
|
+
let url;
|
|
51
|
+
try {
|
|
52
|
+
url = new URL(base);
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
throw new Error(`Invalid gateway URL: ${base}`);
|
|
56
|
+
}
|
|
57
|
+
if (!allowUnsafe && !isAllowedGatewayHost(url.hostname)) {
|
|
58
|
+
throw new Error(`Gateway host "${url.hostname}" is not allowed. Wallet-signed headers and your endpoint apiKey ` +
|
|
59
|
+
`are only sent to the marketplace (*.aibtc.com) or localhost. To target another host on ` +
|
|
60
|
+
`purpose, pass allowUnsafeGateway: true.`);
|
|
61
|
+
}
|
|
62
|
+
return base;
|
|
63
|
+
}
|
|
64
|
+
function requireUnlockedWallet() {
|
|
65
|
+
const account = getWalletManager().getActiveAccount();
|
|
66
|
+
if (!account) {
|
|
67
|
+
throw new Error("Wallet is not unlocked. Use wallet_unlock first to enable signing.");
|
|
68
|
+
}
|
|
69
|
+
return account;
|
|
70
|
+
}
|
|
71
|
+
/** The exact message the gateway rebuilds and verifies. Must match byte-for-byte. */
|
|
72
|
+
export function authMessage(action, providerId, timestamp) {
|
|
73
|
+
return `Inference Marketplace\nAction: ${action}\nProvider: ${providerId}\nTimestamp: ${timestamp}`;
|
|
74
|
+
}
|
|
75
|
+
/** Sign an auth message with the unlocked wallet and return the gateway headers. */
|
|
76
|
+
export function signAuthHeaders(account, action, providerId) {
|
|
77
|
+
const timestamp = Math.floor(Date.now() / 1000);
|
|
78
|
+
const message = authMessage(action, providerId, timestamp);
|
|
79
|
+
const signature = signMessageHashRsv({
|
|
80
|
+
messageHash: bytesToHex(hashMessage(message)),
|
|
81
|
+
privateKey: account.privateKey,
|
|
82
|
+
});
|
|
83
|
+
const publicKey = compressPublicKey(privateKeyToPublic(account.privateKey));
|
|
84
|
+
return {
|
|
85
|
+
"Content-Type": "application/json",
|
|
86
|
+
"X-Stacks-Signature": signature,
|
|
87
|
+
"X-Stacks-Public-Key": publicKey,
|
|
88
|
+
"X-Stacks-Timestamp": String(timestamp),
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
async function gatewayFetch(url, init) {
|
|
92
|
+
const res = await fetch(url, { ...init, signal: AbortSignal.timeout(GATEWAY_TIMEOUT_MS) });
|
|
93
|
+
const text = await res.text();
|
|
94
|
+
let body;
|
|
95
|
+
try {
|
|
96
|
+
body = JSON.parse(text);
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
body = { raw: text };
|
|
100
|
+
}
|
|
101
|
+
return { ok: res.ok, status: res.status, body };
|
|
102
|
+
}
|
|
103
|
+
const gatewayArg = z
|
|
104
|
+
.string()
|
|
105
|
+
.url()
|
|
106
|
+
.optional()
|
|
107
|
+
.describe(`Marketplace gateway base URL. Defaults to ${DEFAULT_GATEWAY}. Use http://localhost:8787 for local dev.`);
|
|
108
|
+
const allowUnsafeGatewayArg = z
|
|
109
|
+
.boolean()
|
|
110
|
+
.optional()
|
|
111
|
+
.describe("Allow a gateway host outside localhost / *.aibtc.com. Required to send wallet-signed headers " +
|
|
112
|
+
"(and any apiKey) to an arbitrary host — leave unset unless you deliberately self-host the gateway.");
|
|
113
|
+
export function registerInferenceMarketplaceTools(server) {
|
|
114
|
+
// Register (verify + list) an endpoint — signed
|
|
115
|
+
server.registerTool("inference_register_provider", {
|
|
116
|
+
description: "List an OpenAI-compatible model endpoint on the AIBTC Inference Marketplace and get paid per request in sBTC. " +
|
|
117
|
+
"Signs a registration message with the unlocked payout wallet (proving ownership) and POSTs it. " +
|
|
118
|
+
"The gateway verifies the signature recovers to payoutAddress, that the endpoint is reachable AND actually " +
|
|
119
|
+
"serving inference, and that model ids are real/commercial Hugging Face repos — then lists it. " +
|
|
120
|
+
"Requires an unlocked wallet on the gateway's network.",
|
|
121
|
+
inputSchema: {
|
|
122
|
+
name: z.string().describe("Display name for your node, e.g. \"Alice's Qwen node\"."),
|
|
123
|
+
endpoint: z
|
|
124
|
+
.string()
|
|
125
|
+
.url()
|
|
126
|
+
.describe("OpenAI-compatible base URL, e.g. https://my-host/v1 (must expose /models and /chat/completions)."),
|
|
127
|
+
models: z
|
|
128
|
+
.array(z.string())
|
|
129
|
+
.min(1)
|
|
130
|
+
.describe("Hugging Face repo ids you serve, e.g. [\"Qwen/Qwen2.5-7B-Instruct\"]."),
|
|
131
|
+
payoutAddress: z
|
|
132
|
+
.string()
|
|
133
|
+
.optional()
|
|
134
|
+
.describe("Stacks address that receives sBTC. Defaults to the unlocked wallet's address (must match the gateway network)."),
|
|
135
|
+
apiKey: z
|
|
136
|
+
.string()
|
|
137
|
+
.optional()
|
|
138
|
+
.describe("Optional: a key your endpoint requires. Locks the endpoint so only the gateway (which presents it) can call it."),
|
|
139
|
+
gateway: gatewayArg,
|
|
140
|
+
allowUnsafeGateway: allowUnsafeGatewayArg,
|
|
141
|
+
},
|
|
142
|
+
}, async ({ name, endpoint, models, payoutAddress, apiKey, gateway, allowUnsafeGateway }) => {
|
|
143
|
+
try {
|
|
144
|
+
const account = requireUnlockedWallet();
|
|
145
|
+
const base = resolveGateway(gateway, allowUnsafeGateway ?? false);
|
|
146
|
+
const payout = payoutAddress ?? account.address;
|
|
147
|
+
const headers = signAuthHeaders(account, "register", endpoint);
|
|
148
|
+
const { ok, status, body } = await gatewayFetch(`${base}/v1/providers`, {
|
|
149
|
+
method: "POST",
|
|
150
|
+
headers,
|
|
151
|
+
body: JSON.stringify({
|
|
152
|
+
name,
|
|
153
|
+
endpoint,
|
|
154
|
+
payoutAddress: payout,
|
|
155
|
+
models,
|
|
156
|
+
...(apiKey ? { apiKey } : {}),
|
|
157
|
+
}),
|
|
158
|
+
});
|
|
159
|
+
return createJsonResponse({
|
|
160
|
+
success: ok,
|
|
161
|
+
status,
|
|
162
|
+
payoutAddress: payout,
|
|
163
|
+
gateway: base,
|
|
164
|
+
result: body,
|
|
165
|
+
note: ok
|
|
166
|
+
? "Registered and verified. Your endpoint is listed and callable."
|
|
167
|
+
: "Registration failed — see result.error (e.g. wrong network, endpoint unreachable, or invalid model id).",
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
catch (error) {
|
|
171
|
+
return createErrorResponse(error);
|
|
172
|
+
}
|
|
173
|
+
});
|
|
174
|
+
// Update a listing in place — signed
|
|
175
|
+
server.registerTool("inference_update_provider", {
|
|
176
|
+
description: "Update your marketplace listing in place — name, models, payout address, endpoint, or description — no delete/re-add. " +
|
|
177
|
+
"Signs an update message with the unlocked payout wallet and PATCHes it. Only fields you pass change. " +
|
|
178
|
+
"Changing the endpoint re-verifies reachability before taking effect. Requires an unlocked wallet.",
|
|
179
|
+
inputSchema: {
|
|
180
|
+
providerId: z.string().describe("The provider id returned at registration (from inference_list_providers)."),
|
|
181
|
+
name: z.string().optional().describe("New display name."),
|
|
182
|
+
endpoint: z.string().url().optional().describe("New OpenAI-compatible base URL (re-verified before it takes effect)."),
|
|
183
|
+
models: z.array(z.string()).optional().describe("New Hugging Face repo id list."),
|
|
184
|
+
payoutAddress: z.string().optional().describe("New payout address (must match the gateway network)."),
|
|
185
|
+
description: z.string().optional().describe("New description."),
|
|
186
|
+
apiKey: z.string().optional().describe("Set/replace the key your endpoint requires (locks the endpoint)."),
|
|
187
|
+
gateway: gatewayArg,
|
|
188
|
+
allowUnsafeGateway: allowUnsafeGatewayArg,
|
|
189
|
+
},
|
|
190
|
+
}, async ({ providerId, name, endpoint, models, payoutAddress, description, apiKey, gateway, allowUnsafeGateway }) => {
|
|
191
|
+
try {
|
|
192
|
+
const account = requireUnlockedWallet();
|
|
193
|
+
const base = resolveGateway(gateway, allowUnsafeGateway ?? false);
|
|
194
|
+
const patch = {};
|
|
195
|
+
if (name !== undefined)
|
|
196
|
+
patch.name = name;
|
|
197
|
+
if (endpoint !== undefined)
|
|
198
|
+
patch.endpoint = endpoint;
|
|
199
|
+
if (models !== undefined)
|
|
200
|
+
patch.models = models;
|
|
201
|
+
if (payoutAddress !== undefined)
|
|
202
|
+
patch.payoutAddress = payoutAddress;
|
|
203
|
+
if (description !== undefined)
|
|
204
|
+
patch.description = description;
|
|
205
|
+
if (apiKey !== undefined)
|
|
206
|
+
patch.apiKey = apiKey;
|
|
207
|
+
if (Object.keys(patch).length === 0) {
|
|
208
|
+
throw new Error("Nothing to update — pass at least one of name/endpoint/models/payoutAddress/description/apiKey.");
|
|
209
|
+
}
|
|
210
|
+
const headers = signAuthHeaders(account, "update", providerId);
|
|
211
|
+
const { ok, status, body } = await gatewayFetch(`${base}/v1/providers/${providerId}`, {
|
|
212
|
+
method: "PATCH",
|
|
213
|
+
headers,
|
|
214
|
+
body: JSON.stringify(patch),
|
|
215
|
+
});
|
|
216
|
+
return createJsonResponse({
|
|
217
|
+
success: ok,
|
|
218
|
+
status,
|
|
219
|
+
gateway: base,
|
|
220
|
+
result: body,
|
|
221
|
+
note: ok ? "Listing updated." : "Update failed — see result.error.",
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
catch (error) {
|
|
225
|
+
return createErrorResponse(error);
|
|
226
|
+
}
|
|
227
|
+
});
|
|
228
|
+
// Reveal or rotate the shared key — signed
|
|
229
|
+
server.registerTool("inference_reveal_key", {
|
|
230
|
+
description: "Reveal or rotate your provider's shared key — the gateway↔endpoint credential the gateway presents when it " +
|
|
231
|
+
"calls your endpoint (NOT an ownership token; ownership is your wallet). The gateway stores it write-only, so a " +
|
|
232
|
+
"wallet signature is the only way to read it back. Pass rotate=true to issue a new one. Requires an unlocked wallet.",
|
|
233
|
+
inputSchema: {
|
|
234
|
+
providerId: z.string().describe("The provider id (from inference_list_providers)."),
|
|
235
|
+
rotate: z.boolean().optional().describe("true = generate and return a NEW key (invalidates the old one). false/omitted = reveal the current key."),
|
|
236
|
+
gateway: gatewayArg,
|
|
237
|
+
allowUnsafeGateway: allowUnsafeGatewayArg,
|
|
238
|
+
},
|
|
239
|
+
}, async ({ providerId, rotate, gateway, allowUnsafeGateway }) => {
|
|
240
|
+
try {
|
|
241
|
+
const account = requireUnlockedWallet();
|
|
242
|
+
const base = resolveGateway(gateway, allowUnsafeGateway ?? false);
|
|
243
|
+
const headers = signAuthHeaders(account, "reveal-key", providerId);
|
|
244
|
+
const { ok, status, body } = await gatewayFetch(`${base}/v1/providers/${providerId}/key`, {
|
|
245
|
+
method: "POST",
|
|
246
|
+
headers,
|
|
247
|
+
body: JSON.stringify(rotate ? { rotate: true } : {}),
|
|
248
|
+
});
|
|
249
|
+
return createJsonResponse({
|
|
250
|
+
success: ok,
|
|
251
|
+
status,
|
|
252
|
+
gateway: base,
|
|
253
|
+
result: body,
|
|
254
|
+
note: ok
|
|
255
|
+
? "Your endpoint must REQUIRE this key (proxy / Cloudflare Access) for it to prevent freeloading — an open endpoint ignores it."
|
|
256
|
+
: "Reveal/rotate failed — see result.error.",
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
catch (error) {
|
|
260
|
+
return createErrorResponse(error);
|
|
261
|
+
}
|
|
262
|
+
});
|
|
263
|
+
// Re-run the health/functional probe — unsigned
|
|
264
|
+
server.registerTool("inference_check_provider", {
|
|
265
|
+
description: "Re-run the marketplace's health + functional probe for a provider on demand (checks the endpoint is reachable and " +
|
|
266
|
+
"actually returns a completion). No signature required. Useful right after (re)starting your node or tunnel.",
|
|
267
|
+
inputSchema: {
|
|
268
|
+
providerId: z.string().describe("The provider id to check."),
|
|
269
|
+
gateway: gatewayArg,
|
|
270
|
+
allowUnsafeGateway: allowUnsafeGatewayArg,
|
|
271
|
+
},
|
|
272
|
+
}, async ({ providerId, gateway, allowUnsafeGateway }) => {
|
|
273
|
+
try {
|
|
274
|
+
const base = resolveGateway(gateway, allowUnsafeGateway ?? false);
|
|
275
|
+
const { ok, status, body } = await gatewayFetch(`${base}/v1/providers/${providerId}/check`, {
|
|
276
|
+
method: "POST",
|
|
277
|
+
});
|
|
278
|
+
return createJsonResponse({ success: ok, status, gateway: base, result: body });
|
|
279
|
+
}
|
|
280
|
+
catch (error) {
|
|
281
|
+
return createErrorResponse(error);
|
|
282
|
+
}
|
|
283
|
+
});
|
|
284
|
+
// List registered providers — unsigned
|
|
285
|
+
server.registerTool("inference_list_providers", {
|
|
286
|
+
description: "List providers registered on the marketplace with their models, health/status, and flagged state. No signature " +
|
|
287
|
+
"required. Use it to find your provider id (match on payoutAddress) after registering.",
|
|
288
|
+
inputSchema: {
|
|
289
|
+
gateway: gatewayArg,
|
|
290
|
+
allowUnsafeGateway: allowUnsafeGatewayArg,
|
|
291
|
+
},
|
|
292
|
+
}, async ({ gateway, allowUnsafeGateway }) => {
|
|
293
|
+
try {
|
|
294
|
+
const base = resolveGateway(gateway, allowUnsafeGateway ?? false);
|
|
295
|
+
const { ok, status, body } = await gatewayFetch(`${base}/v1/providers`, { method: "GET" });
|
|
296
|
+
return createJsonResponse({ success: ok, status, gateway: base, result: body });
|
|
297
|
+
}
|
|
298
|
+
catch (error) {
|
|
299
|
+
return createErrorResponse(error);
|
|
300
|
+
}
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
//# sourceMappingURL=inference-marketplace.tools.js.map
|
|
@@ -7,9 +7,11 @@
|
|
|
7
7
|
* same LightningProvider singleton to auto-pay invoice challenges on 402s.
|
|
8
8
|
*/
|
|
9
9
|
import { z } from "zod";
|
|
10
|
+
import { decode as decodeBolt11 } from "light-bolt11-decoder";
|
|
10
11
|
import { createJsonResponse, createErrorResponse } from "../utils/index.js";
|
|
11
12
|
import { getLightningManager } from "../services/lightning-manager.js";
|
|
12
13
|
import { getWalletManager } from "../services/wallet-manager.js";
|
|
14
|
+
import { getSpendLimiter } from "../services/spend-limiter.js";
|
|
13
15
|
import { MempoolApi, getMempoolTxUrl, } from "../services/mempool-api.js";
|
|
14
16
|
import { buildAndSignBtcTransaction } from "../transactions/bitcoin-builder.js";
|
|
15
17
|
import { UnisatIndexer } from "../services/unisat-indexer.js";
|
|
@@ -284,11 +286,39 @@ export function registerLightningTools(server) {
|
|
|
284
286
|
if (!provider) {
|
|
285
287
|
throw new Error("Lightning wallet is locked. Use lightning_unlock first.");
|
|
286
288
|
}
|
|
289
|
+
// Meter the invoice face value against the cumulative sats spend limit
|
|
290
|
+
// (#572). maxFeeSats only bounds the routing fee, not the payable amount,
|
|
291
|
+
// so without this a single invoice can pay out an unbounded amount.
|
|
292
|
+
// Decode the invoice to read the amount (millisats), mirroring the L402
|
|
293
|
+
// auto-pay path in x402.service.ts.
|
|
294
|
+
let decoded;
|
|
295
|
+
try {
|
|
296
|
+
decoded = decodeBolt11(bolt11);
|
|
297
|
+
}
|
|
298
|
+
catch (decodeErr) {
|
|
299
|
+
throw new Error(`BOLT-11 invoice could not be decoded: ${decodeErr instanceof Error ? decodeErr.message : String(decodeErr)}`);
|
|
300
|
+
}
|
|
301
|
+
const amountSection = decoded.sections.find((s) => s.name === "amount");
|
|
302
|
+
const amountMsat = amountSection?.value ? BigInt(amountSection.value) : 0n;
|
|
303
|
+
const amountSats = Number(amountMsat / 1000n);
|
|
304
|
+
if (amountSats === 0) {
|
|
305
|
+
throw new Error("Invoice has no amount; refusing to pay amountless invoices for safety (an unmeterable spend defeats the spending cap).");
|
|
306
|
+
}
|
|
307
|
+
// Key the sats ledger by the active Stacks address so BTC L1 / sBTC /
|
|
308
|
+
// L402 / Lightning spends share one budget. When the main wallet is
|
|
309
|
+
// locked (Lightning has its own session), fall back to a dedicated
|
|
310
|
+
// Lightning bucket so the pay is always metered. Note: a locked-STX
|
|
311
|
+
// user therefore gets a separate Lightning ledger from their STX-address
|
|
312
|
+
// ledger — see SECURITY.md.
|
|
313
|
+
const ledgerKey = getWalletManager().getActiveAccount()?.address ?? "__lightning__";
|
|
314
|
+
await getSpendLimiter().check("sats", BigInt(amountSats), ledgerKey);
|
|
287
315
|
const result = await provider.payInvoice(bolt11, maxFeeSats);
|
|
316
|
+
await getSpendLimiter().record("sats", BigInt(amountSats), ledgerKey);
|
|
288
317
|
return createJsonResponse({
|
|
289
318
|
success: true,
|
|
290
319
|
preimage: result.preimage,
|
|
291
320
|
feesPaidSats: result.feesPaid,
|
|
321
|
+
amountSats,
|
|
292
322
|
});
|
|
293
323
|
}
|
|
294
324
|
catch (error) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aibtc/mcp-server",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.62.0",
|
|
4
4
|
"description": "Bitcoin-native MCP server for AI agents: BTC/STX wallets, DeFi yield, sBTC peg, NFTs, and x402 payments.",
|
|
5
5
|
"mcpName": "io.github.aibtcdev/mcp-server",
|
|
6
6
|
"type": "module",
|
package/skill/SKILL.md
CHANGED
|
@@ -4,7 +4,7 @@ description: Bitcoin L1 wallet for agents - check balances, send BTC, manage UTX
|
|
|
4
4
|
license: MIT
|
|
5
5
|
metadata:
|
|
6
6
|
author: aibtcdev
|
|
7
|
-
version: 1.
|
|
7
|
+
version: 1.62.0 # x-release-please-version
|
|
8
8
|
npm: "@aibtc/mcp-server"
|
|
9
9
|
github: https://github.com/aibtcdev/aibtc-mcp-server
|
|
10
10
|
---
|
|
@@ -214,6 +214,33 @@ Always probe before executing paid endpoints. Never call `execute_x402_endpoint`
|
|
|
214
214
|
See: [references/stacks-defi.md](references/stacks-defi.md) for endpoint catalog
|
|
215
215
|
See: [references/x402-inbox.md](references/x402-inbox.md) for inbox-specific flow details
|
|
216
216
|
|
|
217
|
+
### Inference Marketplace (earn sBTC serving models)
|
|
218
|
+
|
|
219
|
+
List an OpenAI-compatible model endpoint on the AIBTC Inference Marketplace and
|
|
220
|
+
get paid per request in sBTC. Ownership is proven by a wallet signature — the
|
|
221
|
+
same wallet that receives payouts — so there are no accounts or API keys. The
|
|
222
|
+
tools sign locally with the unlocked wallet and call the gateway, so an agent can
|
|
223
|
+
register and manage a listing in one step.
|
|
224
|
+
|
|
225
|
+
```
|
|
226
|
+
"Register my endpoint https://my-host/v1 serving Qwen/Qwen2.5-7B-Instruct on the inference marketplace"
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
| Tool | Description | Signed |
|
|
230
|
+
|------|-------------|:------:|
|
|
231
|
+
| `inference_register_provider` | Verify + list an endpoint (name, models, payout, optional apiKey) | ✅ |
|
|
232
|
+
| `inference_update_provider` | Change name/models/payout/endpoint/description/apiKey in place | ✅ |
|
|
233
|
+
| `inference_reveal_key` | Reveal or rotate the gateway↔endpoint shared key (`rotate: true`) | ✅ |
|
|
234
|
+
| `inference_check_provider` | Re-run the health/functional probe | — |
|
|
235
|
+
| `inference_list_providers` | List providers + health (find your id by `payoutAddress`) | — |
|
|
236
|
+
|
|
237
|
+
The gateway verifies the signature recovers to `payoutAddress` (which also fixes
|
|
238
|
+
the network), that the endpoint is reachable AND actually serving inference, and
|
|
239
|
+
that model ids are real, commercially-licensed Hugging Face repos. Requires an
|
|
240
|
+
unlocked wallet on the gateway's network. Every tool takes an optional `gateway`
|
|
241
|
+
arg (defaults to `https://inference.aibtc.com`; use `http://localhost:8787` for
|
|
242
|
+
local dev).
|
|
243
|
+
|
|
217
244
|
### Genesis Lifecycle
|
|
218
245
|
|
|
219
246
|
Agent identity and reputation on Bitcoin and Stacks:
|