bsv-mcp 0.2.14 → 0.3.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/CHANGELOG.md +90 -10
- package/README.md +13 -13
- package/dist/app.html +483 -106
- package/dist/index.js +265787 -155292
- package/index.ts +505 -70
- package/package.json +24 -15
- package/prompts/bsvSdk/auth.ts +1 -1
- package/prompts/bsvSdk/cryptography.ts +1 -1
- package/prompts/bsvSdk/overview.ts +1 -1
- package/prompts/bsvSdk/primitives.ts +1 -1
- package/prompts/bsvSdk/script.ts +1 -1
- package/prompts/bsvSdk/transaction.ts +1 -1
- package/prompts/bsvSdk/wallet.ts +1 -1
- package/prompts/ordinals.ts +1 -1
- package/test-mcp-server.ts +26 -4
- package/tools/a2b/discover.ts +2 -2
- package/tools/bap/friend.ts +17 -24
- package/tools/bap/generate.test.ts +34 -32
- package/tools/bap/generate.ts +48 -43
- package/tools/bap/getCurrentAddress.ts +1 -3
- package/tools/bap/getId.test.ts +18 -10
- package/tools/bap/getId.ts +8 -6
- package/tools/bap/utils.ts +1 -1
- package/tools/bsocial/bmapReadPosts.ts +16 -2
- package/tools/bsocial/createPost.ts +21 -10
- package/tools/bsocial/readPosts.ts +10 -4
- package/tools/bsv/decodeTransaction.ts +1 -1
- package/tools/bsv/explore.ts +0 -4
- package/tools/bsv/getPrice.ts +1 -1
- package/tools/bsv/token.ts +14 -6
- package/tools/constants.ts +6 -2
- package/tools/index.ts +10 -10
- package/tools/mnee/getBalance.ts +1 -1
- package/tools/mnee/parseTx.ts +1 -1
- package/tools/mnee/sendMnee.ts +30 -8
- package/tools/ordinals/getInscription.ts +1 -3
- package/tools/ordinals/getTokenByIdOrTicker.ts +1 -3
- package/tools/ordinals/index.ts +4 -1
- package/tools/ordinals/marketListings.ts +1 -1
- package/tools/ordinals/marketSales.ts +1 -1
- package/tools/ordinals/searchInscriptions.ts +1 -1
- package/tools/utils/aip.ts +31 -22
- package/tools/utils/installAgentMaster.ts +8 -9
- package/tools/utils/toolRegistration.ts +21 -22
- package/tools/utils/transactionBuilder.ts +21 -14
- package/tools/wallet/a2bPublishMcp.ts +15 -5
- package/tools/wallet/brc100.ts +541 -310
- package/tools/wallet/cancelListing.ts +29 -37
- package/tools/wallet/createOrdinals.ts +43 -27
- package/tools/wallet/fetchPaymentUtxos.ts +7 -5
- package/tools/wallet/gatherCollectionInfo.ts +3 -5
- package/tools/wallet/getAddress.ts +1 -4
- package/tools/wallet/getBalance.ts +15 -15
- package/tools/wallet/{getBalanceDroplet.ts → getBalanceDroplit.ts} +6 -6
- package/tools/wallet/getBsv21Balances.ts +20 -14
- package/tools/wallet/getLockData.ts +11 -11
- package/tools/wallet/getOrdinals.ts +26 -20
- package/tools/wallet/integratedWallet.ts +55 -53
- package/tools/wallet/listOrdinal.ts +40 -39
- package/tools/wallet/listTokens.ts +18 -17
- package/tools/wallet/lockBsv.ts +36 -22
- package/tools/wallet/mintCollection.ts +15 -10
- package/tools/wallet/opnsDeregister.ts +29 -39
- package/tools/wallet/opnsRegister.ts +29 -39
- package/tools/wallet/purchaseListing.test.ts +27 -0
- package/tools/wallet/purchaseListing.ts +59 -40
- package/tools/wallet/refreshUtxos.ts +1 -1
- package/tools/wallet/schemas.ts +0 -13
- package/tools/wallet/sendAllBsv.ts +26 -20
- package/tools/wallet/sendBsv.ts +107 -0
- package/tools/wallet/setupDroplit.ts +157 -0
- package/tools/wallet/signBsm.ts +18 -18
- package/tools/wallet/sweepBsv.ts +48 -27
- package/tools/wallet/sweepBsv21.ts +42 -28
- package/tools/wallet/sweepOrdinals.ts +38 -26
- package/tools/wallet/tools.ts +11 -13
- package/tools/wallet/transactionHelper.ts +13 -5
- package/tools/wallet/transferOrdToken.ts +72 -84
- package/tools/wallet/unlockBsv.ts +23 -17
- package/tools/wallet/utxo.ts +1 -0
- package/tools/wallet/wallet.ts +1 -13
- package/tools/wallet/sendToAddress.ts +0 -74
- package/tools/wallet/setupDroplet.ts +0 -161
package/index.ts
CHANGED
|
@@ -1,23 +1,15 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
if (_isStdio) {
|
|
10
|
-
const _err = console.error.bind(console);
|
|
11
|
-
console.log = (...a: unknown[]) => _err("[log]", ...a);
|
|
12
|
-
console.warn = (...a: unknown[]) => _err("[warn]", ...a);
|
|
13
|
-
console.info = (...a: unknown[]) => _err("[info]", ...a);
|
|
14
|
-
console.debug = (...a: unknown[]) => _err("[debug]", ...a);
|
|
15
|
-
}
|
|
3
|
+
// Must stay the first import. The stdio transport owns stdout for JSON-RPC, so
|
|
4
|
+
// the console redirect has to be installed before any other module initializes
|
|
5
|
+
// and gets a chance to log. This was previously a statement block below the
|
|
6
|
+
// imports, which cannot work: ES imports are evaluated before any top-level
|
|
7
|
+
// statement, so every dependency had already initialized by the time it ran.
|
|
8
|
+
import "./utils/stdioGuard";
|
|
16
9
|
|
|
17
10
|
import { readFile } from "node:fs/promises";
|
|
18
11
|
import os from "node:os";
|
|
19
|
-
import path from "node:path";
|
|
20
|
-
import { dirname, join } from "node:path";
|
|
12
|
+
import path, { dirname, join } from "node:path";
|
|
21
13
|
import { fileURLToPath } from "node:url";
|
|
22
14
|
import { PrivateKey } from "@bsv/sdk";
|
|
23
15
|
import {
|
|
@@ -27,6 +19,7 @@ import {
|
|
|
27
19
|
} from "@modelcontextprotocol/ext-apps/server";
|
|
28
20
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
29
21
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
22
|
+
import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js";
|
|
30
23
|
import { z } from "zod";
|
|
31
24
|
import packageJson from "./package.json";
|
|
32
25
|
import { registerAllPrompts } from "./prompts/index.ts";
|
|
@@ -35,8 +28,6 @@ import { getBsvPriceWithCache } from "./tools/bsv/getPrice.ts";
|
|
|
35
28
|
import { registerAllTools, type ToolsConfig } from "./tools/index.ts";
|
|
36
29
|
import { IntegratedWallet } from "./tools/wallet/integratedWallet.ts";
|
|
37
30
|
import { Wallet } from "./tools/wallet/wallet.ts";
|
|
38
|
-
import { initWallet, destroyWallet } from "./utils/walletInit.ts";
|
|
39
|
-
import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js";
|
|
40
31
|
import {
|
|
41
32
|
type BSVJWTPayload,
|
|
42
33
|
createMCPJWTValidator,
|
|
@@ -44,6 +35,11 @@ import {
|
|
|
44
35
|
} from "./utils/jwtValidator.ts";
|
|
45
36
|
import { SecureKeyManager } from "./utils/keyManager.ts";
|
|
46
37
|
import { setServerInstance } from "./utils/passphrasePrompt.ts";
|
|
38
|
+
import {
|
|
39
|
+
destroyWallet,
|
|
40
|
+
initWallet,
|
|
41
|
+
setSpendingApprovalServerInstance,
|
|
42
|
+
} from "./utils/walletInit.ts";
|
|
47
43
|
|
|
48
44
|
// Initialize server variable (used for stdio mode and passphrase detection)
|
|
49
45
|
let server: McpServer | undefined;
|
|
@@ -52,6 +48,7 @@ let server: McpServer | undefined;
|
|
|
52
48
|
interface ServerFactoryOptions {
|
|
53
49
|
toolsConfig: ToolsConfig;
|
|
54
50
|
wallet?: Wallet;
|
|
51
|
+
ctx?: import("@1sat/actions").OneSatContext;
|
|
55
52
|
loadPrompts: boolean;
|
|
56
53
|
loadResources: boolean;
|
|
57
54
|
}
|
|
@@ -80,7 +77,7 @@ function createConfiguredServer(opts: ServerFactoryOptions): McpServer {
|
|
|
80
77
|
);
|
|
81
78
|
|
|
82
79
|
registerAllTools(srv, opts.toolsConfig);
|
|
83
|
-
registerMcpAppTools(srv, opts.wallet);
|
|
80
|
+
registerMcpAppTools(srv, opts.wallet, opts.ctx);
|
|
84
81
|
if (opts.loadPrompts) registerAllPrompts(srv);
|
|
85
82
|
if (opts.loadResources) registerResources(srv);
|
|
86
83
|
|
|
@@ -112,13 +109,13 @@ const CONFIG = {
|
|
|
112
109
|
// --stdio CLI flag takes precedence over TRANSPORT env var (matches neighborhood plugin pattern)
|
|
113
110
|
transportMode: process.argv.includes("--stdio")
|
|
114
111
|
? "stdio"
|
|
115
|
-
:
|
|
112
|
+
: process.env.TRANSPORT?.toLowerCase() || "http", // 'stdio' or 'http'/default
|
|
116
113
|
port: Number.parseInt(process.env.PORT || "3000", 10),
|
|
117
114
|
|
|
118
|
-
// ---
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
115
|
+
// --- Droplit API Configuration ---
|
|
116
|
+
useDroplitApi: process.env.USE_DROPLIT_API === "true",
|
|
117
|
+
droplitApiUrl: process.env.DROPLIT_API_URL || "http://127.0.0.1:4000",
|
|
118
|
+
droplitFaucetName: process.env.DROPLIT_FAUCET_NAME || "",
|
|
122
119
|
|
|
123
120
|
// --- OAuth Configuration ---
|
|
124
121
|
enableOAuth: process.env.ENABLE_OAUTH !== "false", // Enabled by default
|
|
@@ -278,7 +275,11 @@ async function initializeKeys(): Promise<{
|
|
|
278
275
|
const APP_RESOURCE_URI = "ui://bsv-mcp/app.html";
|
|
279
276
|
const __appDirname = dirname(fileURLToPath(import.meta.url));
|
|
280
277
|
|
|
281
|
-
function registerMcpAppTools(
|
|
278
|
+
function registerMcpAppTools(
|
|
279
|
+
server: McpServer,
|
|
280
|
+
wallet?: Wallet,
|
|
281
|
+
ctx?: import("@1sat/actions").OneSatContext,
|
|
282
|
+
) {
|
|
282
283
|
// Primary dashboard tool — model calls this to open the UI
|
|
283
284
|
registerAppTool(
|
|
284
285
|
server,
|
|
@@ -332,8 +333,7 @@ function registerMcpAppTools(server: McpServer, wallet?: Wallet) {
|
|
|
332
333
|
const res = await fetch(
|
|
333
334
|
`https://junglebus.gorillapool.io/v1/transaction/get/${txid}`,
|
|
334
335
|
);
|
|
335
|
-
if (!res.ok)
|
|
336
|
-
throw new Error(`Transaction not found: ${res.status}`);
|
|
336
|
+
if (!res.ok) throw new Error(`Transaction not found: ${res.status}`);
|
|
337
337
|
const jbData = (await res.json()) as Record<string, unknown>;
|
|
338
338
|
|
|
339
339
|
const { Transaction, Utils } = await import("@bsv/sdk");
|
|
@@ -468,7 +468,7 @@ function registerMcpAppTools(server: McpServer, wallet?: Wallet) {
|
|
|
468
468
|
},
|
|
469
469
|
);
|
|
470
470
|
|
|
471
|
-
// App-only: fetch wallet data
|
|
471
|
+
// App-only: fetch wallet data (uses BRC-100 context to match direct tools)
|
|
472
472
|
registerAppTool(
|
|
473
473
|
server,
|
|
474
474
|
"app_wallet_data",
|
|
@@ -481,7 +481,7 @@ function registerMcpAppTools(server: McpServer, wallet?: Wallet) {
|
|
|
481
481
|
},
|
|
482
482
|
},
|
|
483
483
|
async () => {
|
|
484
|
-
if (!wallet) {
|
|
484
|
+
if (!ctx && !wallet) {
|
|
485
485
|
return {
|
|
486
486
|
content: [
|
|
487
487
|
{
|
|
@@ -490,18 +490,50 @@ function registerMcpAppTools(server: McpServer, wallet?: Wallet) {
|
|
|
490
490
|
},
|
|
491
491
|
],
|
|
492
492
|
structuredContent: {
|
|
493
|
-
error:
|
|
493
|
+
error:
|
|
494
|
+
"No wallet configured. Set PRIVATE_KEY_WIF or generate keys.",
|
|
494
495
|
},
|
|
495
496
|
_meta: { viewUUID: crypto.randomUUID() },
|
|
496
497
|
};
|
|
497
498
|
}
|
|
498
499
|
|
|
499
500
|
try {
|
|
500
|
-
|
|
501
|
-
const address = wallet.getAddress();
|
|
501
|
+
let address: string | undefined;
|
|
502
502
|
let totalSatoshis = 0;
|
|
503
|
-
|
|
504
|
-
|
|
503
|
+
let utxoCount = 0;
|
|
504
|
+
let utxos: Array<{ txid: string; vout: number; satoshis: number }> = [];
|
|
505
|
+
|
|
506
|
+
// Use BRC-100 context (same source as wallet_getAddress / wallet_getBalance)
|
|
507
|
+
if (ctx) {
|
|
508
|
+
const { deriveDepositAddresses } = await import("@1sat/actions");
|
|
509
|
+
const { derivations } = await deriveDepositAddresses.execute(ctx, {
|
|
510
|
+
prefix: "mcp",
|
|
511
|
+
});
|
|
512
|
+
address = derivations[0]?.address;
|
|
513
|
+
|
|
514
|
+
const result = await ctx.wallet.listOutputs({ basket: "default" });
|
|
515
|
+
totalSatoshis = result.outputs.reduce(
|
|
516
|
+
(sum, output) => sum + output.satoshis,
|
|
517
|
+
0,
|
|
518
|
+
);
|
|
519
|
+
utxoCount = result.totalOutputs;
|
|
520
|
+
utxos = result.outputs.slice(0, 50).map((o) => {
|
|
521
|
+
const [txid = "", voutStr = "0"] = (o.outpoint ?? "").split(".");
|
|
522
|
+
return { txid, vout: Number(voutStr), satoshis: o.satoshis };
|
|
523
|
+
});
|
|
524
|
+
} else if (wallet) {
|
|
525
|
+
// Fallback to local wallet if no BRC-100 context
|
|
526
|
+
address = wallet.getAddress();
|
|
527
|
+
const { paymentUtxos } = await wallet.getUtxos();
|
|
528
|
+
for (const utxo of paymentUtxos) {
|
|
529
|
+
totalSatoshis += utxo.satoshis || 0;
|
|
530
|
+
}
|
|
531
|
+
utxoCount = paymentUtxos.length;
|
|
532
|
+
utxos = paymentUtxos.slice(0, 50).map((u) => ({
|
|
533
|
+
txid: u.txid,
|
|
534
|
+
vout: u.vout,
|
|
535
|
+
satoshis: u.satoshis,
|
|
536
|
+
}));
|
|
505
537
|
}
|
|
506
538
|
|
|
507
539
|
let price: number | undefined;
|
|
@@ -525,14 +557,10 @@ function registerMcpAppTools(server: McpServer, wallet?: Wallet) {
|
|
|
525
557
|
balance: {
|
|
526
558
|
satoshis: totalSatoshis,
|
|
527
559
|
bsv: bsvAmount,
|
|
528
|
-
utxoCount
|
|
560
|
+
utxoCount,
|
|
529
561
|
},
|
|
530
562
|
address,
|
|
531
|
-
utxos
|
|
532
|
-
txid: u.txid,
|
|
533
|
-
vout: u.vout,
|
|
534
|
-
satoshis: u.satoshis,
|
|
535
|
-
})),
|
|
563
|
+
utxos,
|
|
536
564
|
price,
|
|
537
565
|
},
|
|
538
566
|
_meta: { viewUUID: crypto.randomUUID() },
|
|
@@ -632,12 +660,410 @@ function registerMcpAppTools(server: McpServer, wallet?: Wallet) {
|
|
|
632
660
|
},
|
|
633
661
|
);
|
|
634
662
|
|
|
663
|
+
// App-only: scan an address for categorized UTXOs (funding, ordinals, BSV-21 tokens)
|
|
664
|
+
registerAppTool(
|
|
665
|
+
server,
|
|
666
|
+
"app_sweep_scan",
|
|
667
|
+
{
|
|
668
|
+
title: "Sweep Scan",
|
|
669
|
+
description:
|
|
670
|
+
"App-only: scans a Bitcoin address for categorized UTXOs — funding, ordinals, and BSV-21 tokens.",
|
|
671
|
+
inputSchema: {
|
|
672
|
+
address: z.string().describe("Bitcoin address to scan"),
|
|
673
|
+
},
|
|
674
|
+
_meta: {
|
|
675
|
+
ui: { resourceUri: APP_RESOURCE_URI, visibility: ["app"] },
|
|
676
|
+
},
|
|
677
|
+
},
|
|
678
|
+
async (args) => {
|
|
679
|
+
const { address } = args as { address: string };
|
|
680
|
+
try {
|
|
681
|
+
const res = await fetch(
|
|
682
|
+
`https://ordinals.gorillapool.io/api/txos/address/${address}/unspent?limit=1000`,
|
|
683
|
+
);
|
|
684
|
+
if (!res.ok) throw new Error(`GorillaPool API error: ${res.status}`);
|
|
685
|
+
const utxos = (await res.json()) as Array<Record<string, unknown>>;
|
|
686
|
+
|
|
687
|
+
const funding: Array<{
|
|
688
|
+
outpoint: string;
|
|
689
|
+
satoshis: number;
|
|
690
|
+
lockingScript: string;
|
|
691
|
+
}> = [];
|
|
692
|
+
const ordinals: Array<{
|
|
693
|
+
outpoint: string;
|
|
694
|
+
satoshis: number;
|
|
695
|
+
lockingScript: string;
|
|
696
|
+
}> = [];
|
|
697
|
+
const bsv21Raw: Array<{
|
|
698
|
+
outpoint: string;
|
|
699
|
+
satoshis: number;
|
|
700
|
+
lockingScript: string;
|
|
701
|
+
tokenId: string;
|
|
702
|
+
amount: string;
|
|
703
|
+
sym?: string;
|
|
704
|
+
dec: number;
|
|
705
|
+
}> = [];
|
|
706
|
+
|
|
707
|
+
for (const utxo of utxos) {
|
|
708
|
+
const txid = utxo.txid as string;
|
|
709
|
+
const vout = utxo.vout as number;
|
|
710
|
+
const outpoint = `${txid}_${vout}`;
|
|
711
|
+
const satoshis = (utxo.satoshis as number) || 0;
|
|
712
|
+
const script = (utxo.script as string) || "";
|
|
713
|
+
const origin = utxo.origin as Record<string, unknown> | undefined;
|
|
714
|
+
const originData = origin?.data as
|
|
715
|
+
| Record<string, unknown>
|
|
716
|
+
| undefined;
|
|
717
|
+
|
|
718
|
+
const base = { outpoint, satoshis, lockingScript: script };
|
|
719
|
+
|
|
720
|
+
if (originData?.bsv21) {
|
|
721
|
+
const bsv21 = originData.bsv21 as Record<string, unknown>;
|
|
722
|
+
bsv21Raw.push({
|
|
723
|
+
...base,
|
|
724
|
+
tokenId: (bsv21.id as string) || "",
|
|
725
|
+
amount: (bsv21.amt as string) || "0",
|
|
726
|
+
sym: bsv21.sym as string | undefined,
|
|
727
|
+
dec: (bsv21.dec as number) ?? 0,
|
|
728
|
+
});
|
|
729
|
+
} else if (originData?.insc || origin?.outpoint) {
|
|
730
|
+
ordinals.push(base);
|
|
731
|
+
} else {
|
|
732
|
+
funding.push(base);
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
// Group BSV-21 tokens by tokenId
|
|
737
|
+
const tokenGroups = new Map<
|
|
738
|
+
string,
|
|
739
|
+
{
|
|
740
|
+
inputs: typeof bsv21Raw;
|
|
741
|
+
sym?: string;
|
|
742
|
+
dec: number;
|
|
743
|
+
}
|
|
744
|
+
>();
|
|
745
|
+
for (const item of bsv21Raw) {
|
|
746
|
+
let group = tokenGroups.get(item.tokenId);
|
|
747
|
+
if (!group) {
|
|
748
|
+
group = { inputs: [], sym: item.sym, dec: item.dec };
|
|
749
|
+
tokenGroups.set(item.tokenId, group);
|
|
750
|
+
}
|
|
751
|
+
group.inputs.push(item);
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
const bsv21Tokens: Array<{
|
|
755
|
+
tokenId: string;
|
|
756
|
+
symbol?: string;
|
|
757
|
+
decimals: number;
|
|
758
|
+
totalAmount: string;
|
|
759
|
+
inputs: typeof bsv21Raw;
|
|
760
|
+
}> = [];
|
|
761
|
+
for (const [tokenId, group] of tokenGroups) {
|
|
762
|
+
let total = BigInt(0);
|
|
763
|
+
for (const inp of group.inputs) {
|
|
764
|
+
total += BigInt(inp.amount);
|
|
765
|
+
}
|
|
766
|
+
bsv21Tokens.push({
|
|
767
|
+
tokenId,
|
|
768
|
+
symbol: group.sym,
|
|
769
|
+
decimals: group.dec,
|
|
770
|
+
totalAmount: total.toString(),
|
|
771
|
+
inputs: group.inputs,
|
|
772
|
+
});
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
const totalFundingSats = funding.reduce(
|
|
776
|
+
(sum, f) => sum + f.satoshis,
|
|
777
|
+
0,
|
|
778
|
+
);
|
|
779
|
+
|
|
780
|
+
return {
|
|
781
|
+
content: [
|
|
782
|
+
{
|
|
783
|
+
type: "text" as const,
|
|
784
|
+
text: `Scanned ${address}: ${funding.length} funding, ${ordinals.length} ordinals, ${bsv21Tokens.length} token types`,
|
|
785
|
+
},
|
|
786
|
+
],
|
|
787
|
+
structuredContent: {
|
|
788
|
+
address,
|
|
789
|
+
funding,
|
|
790
|
+
ordinals,
|
|
791
|
+
bsv21Tokens,
|
|
792
|
+
totalFundingSats,
|
|
793
|
+
},
|
|
794
|
+
_meta: { viewUUID: crypto.randomUUID() },
|
|
795
|
+
};
|
|
796
|
+
} catch (err) {
|
|
797
|
+
return {
|
|
798
|
+
content: [
|
|
799
|
+
{
|
|
800
|
+
type: "text" as const,
|
|
801
|
+
text: `Error: ${err instanceof Error ? err.message : String(err)}`,
|
|
802
|
+
},
|
|
803
|
+
],
|
|
804
|
+
structuredContent: { error: String(err) },
|
|
805
|
+
_meta: { viewUUID: crypto.randomUUID() },
|
|
806
|
+
};
|
|
807
|
+
}
|
|
808
|
+
},
|
|
809
|
+
);
|
|
810
|
+
|
|
811
|
+
// App-only: prepare unsigned sweep transaction for client-side signing
|
|
812
|
+
registerAppTool(
|
|
813
|
+
server,
|
|
814
|
+
"app_sweep_prepare",
|
|
815
|
+
{
|
|
816
|
+
title: "Sweep Prepare",
|
|
817
|
+
description:
|
|
818
|
+
"App-only: builds an unsigned sweep transaction. Returns BEEF hex and reference for client-side signing.",
|
|
819
|
+
inputSchema: {
|
|
820
|
+
sweepType: z
|
|
821
|
+
.enum(["bsv", "ordinals", "bsv21"])
|
|
822
|
+
.describe("Type of assets to sweep"),
|
|
823
|
+
inputs: z
|
|
824
|
+
.array(
|
|
825
|
+
z.object({
|
|
826
|
+
outpoint: z.string().describe("Outpoint (txid_vout)"),
|
|
827
|
+
satoshis: z.number().int().describe("Satoshis in output"),
|
|
828
|
+
lockingScript: z.string().describe("Locking script hex"),
|
|
829
|
+
}),
|
|
830
|
+
)
|
|
831
|
+
.describe("UTXOs to sweep"),
|
|
832
|
+
},
|
|
833
|
+
_meta: {
|
|
834
|
+
ui: { resourceUri: APP_RESOURCE_URI, visibility: ["app"] },
|
|
835
|
+
},
|
|
836
|
+
},
|
|
837
|
+
async (args) => {
|
|
838
|
+
const { sweepType, inputs } = args as {
|
|
839
|
+
sweepType: "bsv" | "ordinals" | "bsv21";
|
|
840
|
+
inputs: Array<{
|
|
841
|
+
outpoint: string;
|
|
842
|
+
satoshis: number;
|
|
843
|
+
lockingScript: string;
|
|
844
|
+
}>;
|
|
845
|
+
};
|
|
846
|
+
|
|
847
|
+
if (!ctx) {
|
|
848
|
+
return {
|
|
849
|
+
content: [
|
|
850
|
+
{
|
|
851
|
+
type: "text" as const,
|
|
852
|
+
text: "BRC-100 wallet context not available",
|
|
853
|
+
},
|
|
854
|
+
],
|
|
855
|
+
structuredContent: { error: "No wallet context" },
|
|
856
|
+
_meta: { viewUUID: crypto.randomUUID() },
|
|
857
|
+
};
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
try {
|
|
861
|
+
if (!ctx.services) throw new Error("Services not available");
|
|
862
|
+
if (!inputs.length) throw new Error("No inputs provided");
|
|
863
|
+
|
|
864
|
+
// Fetch and merge BEEF for all input transactions
|
|
865
|
+
const txids = [...new Set(inputs.map((i) => i.outpoint.split("_")[0]))];
|
|
866
|
+
const firstBeef = await ctx.services.getBeefForTxid(txids[0]);
|
|
867
|
+
for (let i = 1; i < txids.length; i++) {
|
|
868
|
+
const additionalBeef = await ctx.services.getBeefForTxid(txids[i]);
|
|
869
|
+
firstBeef.mergeBeef(additionalBeef);
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
// Build input descriptors using SDK format (txid.vout)
|
|
873
|
+
const inputDescriptors = inputs.map((input) => {
|
|
874
|
+
const [txid, voutStr] = input.outpoint.split("_");
|
|
875
|
+
return {
|
|
876
|
+
outpoint: `${txid}.${Number(voutStr)}`,
|
|
877
|
+
inputDescription: `Sweep ${sweepType} input`,
|
|
878
|
+
unlockingScriptLength: 108,
|
|
879
|
+
sequenceNumber: 0xffffffff,
|
|
880
|
+
};
|
|
881
|
+
});
|
|
882
|
+
|
|
883
|
+
const inputTotal = inputs.reduce((sum, i) => sum + i.satoshis, 0);
|
|
884
|
+
|
|
885
|
+
const createResult = await ctx.wallet.createAction({
|
|
886
|
+
description: `Sweep ${inputTotal} sats (${sweepType})`,
|
|
887
|
+
inputBEEF: firstBeef.toBinary(),
|
|
888
|
+
inputs: inputDescriptors,
|
|
889
|
+
outputs: [],
|
|
890
|
+
options: {
|
|
891
|
+
signAndProcess: false,
|
|
892
|
+
...(sweepType !== "bsv" && {
|
|
893
|
+
randomizeOutputs: false,
|
|
894
|
+
}),
|
|
895
|
+
},
|
|
896
|
+
});
|
|
897
|
+
|
|
898
|
+
if ("error" in createResult && createResult.error) {
|
|
899
|
+
throw new Error(String(createResult.error));
|
|
900
|
+
}
|
|
901
|
+
if (!createResult.signableTransaction) {
|
|
902
|
+
throw new Error("No signable transaction returned");
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
// Map our inputs to their indices in the transaction
|
|
906
|
+
const { Transaction: TxClass, Utils: SdkUtils } = await import(
|
|
907
|
+
"@bsv/sdk"
|
|
908
|
+
);
|
|
909
|
+
const tx = TxClass.fromBEEF(createResult.signableTransaction.tx);
|
|
910
|
+
const ourOutpoints = new Set(
|
|
911
|
+
inputs.map((i) => {
|
|
912
|
+
const [txid, voutStr] = i.outpoint.split("_");
|
|
913
|
+
return `${txid}.${Number(voutStr)}`;
|
|
914
|
+
}),
|
|
915
|
+
);
|
|
916
|
+
|
|
917
|
+
const inputsToSign: Array<{
|
|
918
|
+
index: number;
|
|
919
|
+
outpoint: string;
|
|
920
|
+
satoshis: number;
|
|
921
|
+
lockingScript: string;
|
|
922
|
+
}> = [];
|
|
923
|
+
for (let idx = 0; idx < tx.inputs.length; idx++) {
|
|
924
|
+
const txInput = tx.inputs[idx];
|
|
925
|
+
const op = `${txInput.sourceTXID}.${txInput.sourceOutputIndex}`;
|
|
926
|
+
if (ourOutpoints.has(op)) {
|
|
927
|
+
const match = inputs.find((i) => {
|
|
928
|
+
const [t, v] = i.outpoint.split("_");
|
|
929
|
+
return `${t}.${Number(v)}` === op;
|
|
930
|
+
});
|
|
931
|
+
if (match) {
|
|
932
|
+
inputsToSign.push({
|
|
933
|
+
index: idx,
|
|
934
|
+
outpoint: match.outpoint,
|
|
935
|
+
satoshis: match.satoshis,
|
|
936
|
+
lockingScript: match.lockingScript,
|
|
937
|
+
});
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
const txHex = SdkUtils.toHex(createResult.signableTransaction.tx);
|
|
943
|
+
|
|
944
|
+
return {
|
|
945
|
+
content: [
|
|
946
|
+
{
|
|
947
|
+
type: "text" as const,
|
|
948
|
+
text: `Prepared ${sweepType} sweep: ${inputsToSign.length} inputs to sign`,
|
|
949
|
+
},
|
|
950
|
+
],
|
|
951
|
+
structuredContent: {
|
|
952
|
+
txHex,
|
|
953
|
+
reference: createResult.signableTransaction.reference,
|
|
954
|
+
inputsToSign,
|
|
955
|
+
},
|
|
956
|
+
_meta: { viewUUID: crypto.randomUUID() },
|
|
957
|
+
};
|
|
958
|
+
} catch (err) {
|
|
959
|
+
return {
|
|
960
|
+
content: [
|
|
961
|
+
{
|
|
962
|
+
type: "text" as const,
|
|
963
|
+
text: `Error: ${err instanceof Error ? err.message : String(err)}`,
|
|
964
|
+
},
|
|
965
|
+
],
|
|
966
|
+
structuredContent: { error: String(err) },
|
|
967
|
+
_meta: { viewUUID: crypto.randomUUID() },
|
|
968
|
+
};
|
|
969
|
+
}
|
|
970
|
+
},
|
|
971
|
+
);
|
|
972
|
+
|
|
973
|
+
// App-only: complete a sweep by broadcasting with client-signed spends
|
|
974
|
+
registerAppTool(
|
|
975
|
+
server,
|
|
976
|
+
"app_sweep_complete",
|
|
977
|
+
{
|
|
978
|
+
title: "Sweep Complete",
|
|
979
|
+
description:
|
|
980
|
+
"App-only: completes a sweep by broadcasting the transaction with client-signed unlocking scripts.",
|
|
981
|
+
inputSchema: {
|
|
982
|
+
reference: z
|
|
983
|
+
.string()
|
|
984
|
+
.describe("Opaque reference from app_sweep_prepare"),
|
|
985
|
+
spends: z
|
|
986
|
+
.record(
|
|
987
|
+
z.string(),
|
|
988
|
+
z.object({
|
|
989
|
+
unlockingScript: z
|
|
990
|
+
.string()
|
|
991
|
+
.describe("Signed unlocking script hex"),
|
|
992
|
+
}),
|
|
993
|
+
)
|
|
994
|
+
.describe("Map of input index to signed unlocking script"),
|
|
995
|
+
},
|
|
996
|
+
_meta: {
|
|
997
|
+
ui: { resourceUri: APP_RESOURCE_URI, visibility: ["app"] },
|
|
998
|
+
},
|
|
999
|
+
},
|
|
1000
|
+
async (args) => {
|
|
1001
|
+
const { reference, spends } = args as {
|
|
1002
|
+
reference: string;
|
|
1003
|
+
spends: Record<number, { unlockingScript: string }>;
|
|
1004
|
+
};
|
|
1005
|
+
|
|
1006
|
+
if (!ctx) {
|
|
1007
|
+
return {
|
|
1008
|
+
content: [
|
|
1009
|
+
{
|
|
1010
|
+
type: "text" as const,
|
|
1011
|
+
text: "BRC-100 wallet context not available",
|
|
1012
|
+
},
|
|
1013
|
+
],
|
|
1014
|
+
structuredContent: { error: "No wallet context" },
|
|
1015
|
+
_meta: { viewUUID: crypto.randomUUID() },
|
|
1016
|
+
};
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
try {
|
|
1020
|
+
const signResult = await ctx.wallet.signAction({
|
|
1021
|
+
reference,
|
|
1022
|
+
spends,
|
|
1023
|
+
options: { acceptDelayedBroadcast: false },
|
|
1024
|
+
});
|
|
1025
|
+
|
|
1026
|
+
if ("error" in signResult) {
|
|
1027
|
+
throw new Error(String(signResult.error));
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
return {
|
|
1031
|
+
content: [
|
|
1032
|
+
{
|
|
1033
|
+
type: "text" as const,
|
|
1034
|
+
text: `Sweep broadcast: ${signResult.txid}`,
|
|
1035
|
+
},
|
|
1036
|
+
],
|
|
1037
|
+
structuredContent: {
|
|
1038
|
+
txid: signResult.txid,
|
|
1039
|
+
success: true,
|
|
1040
|
+
},
|
|
1041
|
+
_meta: { viewUUID: crypto.randomUUID() },
|
|
1042
|
+
};
|
|
1043
|
+
} catch (err) {
|
|
1044
|
+
return {
|
|
1045
|
+
content: [
|
|
1046
|
+
{
|
|
1047
|
+
type: "text" as const,
|
|
1048
|
+
text: `Error: ${err instanceof Error ? err.message : String(err)}`,
|
|
1049
|
+
},
|
|
1050
|
+
],
|
|
1051
|
+
structuredContent: { error: String(err) },
|
|
1052
|
+
_meta: { viewUUID: crypto.randomUUID() },
|
|
1053
|
+
};
|
|
1054
|
+
}
|
|
1055
|
+
},
|
|
1056
|
+
);
|
|
1057
|
+
|
|
635
1058
|
// Register the HTML resource
|
|
636
1059
|
registerAppResource(
|
|
637
1060
|
server,
|
|
638
1061
|
"BSV Dashboard",
|
|
639
1062
|
APP_RESOURCE_URI,
|
|
640
|
-
{
|
|
1063
|
+
{
|
|
1064
|
+
description:
|
|
1065
|
+
"Interactive BSV dashboard with Explorer, Wallet, and Ordinals tabs",
|
|
1066
|
+
},
|
|
641
1067
|
async () => {
|
|
642
1068
|
// When running from bundle (dist/index.js), app.html is a sibling.
|
|
643
1069
|
// When running from source (index.ts), it's in dist/.
|
|
@@ -648,7 +1074,8 @@ function registerMcpAppTools(server: McpServer, wallet?: Wallet) {
|
|
|
648
1074
|
try {
|
|
649
1075
|
html = await readFile(distPath, "utf-8");
|
|
650
1076
|
} catch {
|
|
651
|
-
html =
|
|
1077
|
+
html =
|
|
1078
|
+
"<html><body><p>Dashboard not built. Run <code>bun run build:view</code> to enable it.</p></body></html>";
|
|
652
1079
|
}
|
|
653
1080
|
return {
|
|
654
1081
|
contents: [
|
|
@@ -690,7 +1117,7 @@ Environment Variables:
|
|
|
690
1117
|
DISABLE_BSOCIAL_TOOLS Disable BSocial tools (default: false)
|
|
691
1118
|
ENABLE_A2B_TOOLS Enable A2B tools (default: false)
|
|
692
1119
|
DISABLE_BROADCASTING Disable transaction broadcasting (default: false)
|
|
693
|
-
|
|
1120
|
+
USE_DROPLIT_API Use Droplit API for transactions (default: false)
|
|
694
1121
|
|
|
695
1122
|
Tool Categories:
|
|
696
1123
|
BSV Tools: Price lookup, transaction decoding, validation
|
|
@@ -784,11 +1211,11 @@ Authentication:
|
|
|
784
1211
|
` DISABLE_BROADCASTING: ${process.env.DISABLE_BROADCASTING === "true" ? "Set (true)" : "Not Set/false"}`,
|
|
785
1212
|
);
|
|
786
1213
|
logFunc(
|
|
787
|
-
`
|
|
1214
|
+
` USE_DROPLIT_API: ${CONFIG.useDroplitApi ? "Set (true)" : "Not Set/false"}`,
|
|
788
1215
|
);
|
|
789
|
-
if (CONFIG.
|
|
790
|
-
logFunc(`
|
|
791
|
-
logFunc(`
|
|
1216
|
+
if (CONFIG.useDroplitApi) {
|
|
1217
|
+
logFunc(` DROPLIT_API_URL: ${CONFIG.droplitApiUrl}`);
|
|
1218
|
+
logFunc(` DROPLIT_FAUCET_NAME: ${CONFIG.droplitFaucetName || "Not Set"}`);
|
|
792
1219
|
}
|
|
793
1220
|
|
|
794
1221
|
logFunc("\nKey Source:");
|
|
@@ -882,28 +1309,28 @@ Authentication:
|
|
|
882
1309
|
let remoteServices: import("@1sat/wallet-remote").OneSatServices | undefined;
|
|
883
1310
|
|
|
884
1311
|
if (CONFIG.loadTools) {
|
|
885
|
-
// Check if we should use
|
|
886
|
-
if (CONFIG.
|
|
887
|
-
// Initialize IntegratedWallet in
|
|
1312
|
+
// Check if we should use Droplit API mode
|
|
1313
|
+
if (CONFIG.useDroplitApi && CONFIG.droplitFaucetName) {
|
|
1314
|
+
// Initialize IntegratedWallet in Droplit mode
|
|
888
1315
|
if (CONFIG.loadWalletTools) {
|
|
889
1316
|
try {
|
|
890
1317
|
integratedWallet = new IntegratedWallet({
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
apiUrl: CONFIG.
|
|
894
|
-
faucetName: CONFIG.
|
|
1318
|
+
useDroplitApi: true,
|
|
1319
|
+
droplitConfig: {
|
|
1320
|
+
apiUrl: CONFIG.droplitApiUrl,
|
|
1321
|
+
faucetName: CONFIG.droplitFaucetName,
|
|
895
1322
|
},
|
|
896
1323
|
paymentKey: payPk,
|
|
897
1324
|
identityKey: identityPk,
|
|
898
1325
|
});
|
|
899
1326
|
logFunc(
|
|
900
|
-
`\x1b[32mINFO:
|
|
1327
|
+
`\x1b[32mINFO: Droplit API mode initialized successfully (Faucet: ${CONFIG.droplitFaucetName}).\x1b[0m`,
|
|
901
1328
|
);
|
|
902
1329
|
logFunc(
|
|
903
|
-
`\x1b[33mNOTE: Using
|
|
1330
|
+
`\x1b[33mNOTE: Using Droplit API at ${CONFIG.droplitApiUrl}\x1b[0m`,
|
|
904
1331
|
);
|
|
905
1332
|
logFunc(
|
|
906
|
-
"\x1b[33mNOTE: Local keys are ignored in
|
|
1333
|
+
"\x1b[33mNOTE: Local keys are ignored in Droplit API mode\x1b[0m",
|
|
907
1334
|
);
|
|
908
1335
|
wallet = integratedWallet.getLocalWallet();
|
|
909
1336
|
|
|
@@ -913,7 +1340,7 @@ Authentication:
|
|
|
913
1340
|
effectiveConfig.loadBsocialTools = false;
|
|
914
1341
|
} catch (e) {
|
|
915
1342
|
logFunc(
|
|
916
|
-
`\x1b[31mERROR: Failed to initialize
|
|
1343
|
+
`\x1b[31mERROR: Failed to initialize Droplit API mode: ${e instanceof Error ? e.message : String(e)}. Wallet-dependent tools will be unavailable.\x1b[0m`,
|
|
917
1344
|
);
|
|
918
1345
|
integratedWallet = undefined;
|
|
919
1346
|
effectiveConfig.loadWalletTools = false;
|
|
@@ -1013,6 +1440,7 @@ Authentication:
|
|
|
1013
1440
|
const serverFactoryOpts: ServerFactoryOptions = {
|
|
1014
1441
|
toolsConfig,
|
|
1015
1442
|
wallet,
|
|
1443
|
+
ctx: remoteCtx,
|
|
1016
1444
|
loadPrompts: effectiveConfig.loadPrompts,
|
|
1017
1445
|
loadResources: effectiveConfig.loadResources,
|
|
1018
1446
|
};
|
|
@@ -1029,6 +1457,7 @@ Authentication:
|
|
|
1029
1457
|
// Stdio: single server instance, single transport
|
|
1030
1458
|
server = createConfiguredServer(serverFactoryOpts);
|
|
1031
1459
|
setServerInstance(server);
|
|
1460
|
+
setSpendingApprovalServerInstance(server);
|
|
1032
1461
|
const transport = new StdioServerTransport();
|
|
1033
1462
|
await server.connect(transport);
|
|
1034
1463
|
logFunc("BSV MCP Server running on stdio");
|
|
@@ -1051,7 +1480,9 @@ Authentication:
|
|
|
1051
1480
|
}
|
|
1052
1481
|
>();
|
|
1053
1482
|
|
|
1054
|
-
logFunc(
|
|
1483
|
+
logFunc(
|
|
1484
|
+
`Starting BSV MCP Server in Streamable HTTP mode on port ${port}...`,
|
|
1485
|
+
);
|
|
1055
1486
|
if (CONFIG.enableOAuth) {
|
|
1056
1487
|
logFunc(`OAuth 2.1 authentication enabled`);
|
|
1057
1488
|
logFunc(` Issuer: ${CONFIG.oauthIssuer}`);
|
|
@@ -1067,8 +1498,7 @@ Authentication:
|
|
|
1067
1498
|
"Access-Control-Allow-Methods": "GET, POST, DELETE, OPTIONS",
|
|
1068
1499
|
"Access-Control-Allow-Headers":
|
|
1069
1500
|
"Content-Type, Authorization, mcp-session-id, Last-Event-ID, mcp-protocol-version",
|
|
1070
|
-
"Access-Control-Expose-Headers":
|
|
1071
|
-
"mcp-session-id, mcp-protocol-version",
|
|
1501
|
+
"Access-Control-Expose-Headers": "mcp-session-id, mcp-protocol-version",
|
|
1072
1502
|
} as const;
|
|
1073
1503
|
|
|
1074
1504
|
/**
|
|
@@ -1125,10 +1555,7 @@ Authentication:
|
|
|
1125
1555
|
"bsv:tokens",
|
|
1126
1556
|
],
|
|
1127
1557
|
response_types_supported: ["code"],
|
|
1128
|
-
grant_types_supported: [
|
|
1129
|
-
"authorization_code",
|
|
1130
|
-
"refresh_token",
|
|
1131
|
-
],
|
|
1558
|
+
grant_types_supported: ["authorization_code", "refresh_token"],
|
|
1132
1559
|
token_endpoint_auth_methods_supported: ["none"],
|
|
1133
1560
|
code_challenge_methods_supported: ["S256"],
|
|
1134
1561
|
},
|
|
@@ -1174,7 +1601,9 @@ Authentication:
|
|
|
1174
1601
|
// --- MCP Streamable HTTP endpoint ---
|
|
1175
1602
|
if (url.pathname === "/mcp") {
|
|
1176
1603
|
// Validate OAuth
|
|
1177
|
-
let authInfo:
|
|
1604
|
+
let authInfo:
|
|
1605
|
+
| { token: string; clientId: string; scopes: string[] }
|
|
1606
|
+
| undefined;
|
|
1178
1607
|
try {
|
|
1179
1608
|
const userCtx = await validateAuth(req);
|
|
1180
1609
|
if (userCtx) {
|
|
@@ -1218,10 +1647,18 @@ Authentication:
|
|
|
1218
1647
|
error: { code: -32001, message: "Session not found" },
|
|
1219
1648
|
id: null,
|
|
1220
1649
|
}),
|
|
1221
|
-
{
|
|
1650
|
+
{
|
|
1651
|
+
status: 404,
|
|
1652
|
+
headers: {
|
|
1653
|
+
"Content-Type": "application/json",
|
|
1654
|
+
...corsHeaders,
|
|
1655
|
+
},
|
|
1656
|
+
},
|
|
1222
1657
|
);
|
|
1223
1658
|
}
|
|
1224
|
-
const response = await session.transport.handleRequest(req, {
|
|
1659
|
+
const response = await session.transport.handleRequest(req, {
|
|
1660
|
+
authInfo,
|
|
1661
|
+
});
|
|
1225
1662
|
// Add CORS headers to transport response
|
|
1226
1663
|
for (const [k, v] of Object.entries(corsHeaders)) {
|
|
1227
1664
|
if (!response.headers.has(k)) response.headers.set(k, v);
|
|
@@ -1261,9 +1698,7 @@ Authentication:
|
|
|
1261
1698
|
},
|
|
1262
1699
|
});
|
|
1263
1700
|
|
|
1264
|
-
logFunc(
|
|
1265
|
-
`Bun server listening on http://localhost:${port}`,
|
|
1266
|
-
);
|
|
1701
|
+
logFunc(`Bun server listening on http://localhost:${port}`);
|
|
1267
1702
|
logFunc(" MCP Endpoint: /mcp (Streamable HTTP)");
|
|
1268
1703
|
logFunc(" OAuth Discovery: /.well-known/oauth-protected-resource");
|
|
1269
1704
|
}
|