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.
Files changed (83) hide show
  1. package/CHANGELOG.md +90 -10
  2. package/README.md +13 -13
  3. package/dist/app.html +483 -106
  4. package/dist/index.js +265787 -155292
  5. package/index.ts +505 -70
  6. package/package.json +24 -15
  7. package/prompts/bsvSdk/auth.ts +1 -1
  8. package/prompts/bsvSdk/cryptography.ts +1 -1
  9. package/prompts/bsvSdk/overview.ts +1 -1
  10. package/prompts/bsvSdk/primitives.ts +1 -1
  11. package/prompts/bsvSdk/script.ts +1 -1
  12. package/prompts/bsvSdk/transaction.ts +1 -1
  13. package/prompts/bsvSdk/wallet.ts +1 -1
  14. package/prompts/ordinals.ts +1 -1
  15. package/test-mcp-server.ts +26 -4
  16. package/tools/a2b/discover.ts +2 -2
  17. package/tools/bap/friend.ts +17 -24
  18. package/tools/bap/generate.test.ts +34 -32
  19. package/tools/bap/generate.ts +48 -43
  20. package/tools/bap/getCurrentAddress.ts +1 -3
  21. package/tools/bap/getId.test.ts +18 -10
  22. package/tools/bap/getId.ts +8 -6
  23. package/tools/bap/utils.ts +1 -1
  24. package/tools/bsocial/bmapReadPosts.ts +16 -2
  25. package/tools/bsocial/createPost.ts +21 -10
  26. package/tools/bsocial/readPosts.ts +10 -4
  27. package/tools/bsv/decodeTransaction.ts +1 -1
  28. package/tools/bsv/explore.ts +0 -4
  29. package/tools/bsv/getPrice.ts +1 -1
  30. package/tools/bsv/token.ts +14 -6
  31. package/tools/constants.ts +6 -2
  32. package/tools/index.ts +10 -10
  33. package/tools/mnee/getBalance.ts +1 -1
  34. package/tools/mnee/parseTx.ts +1 -1
  35. package/tools/mnee/sendMnee.ts +30 -8
  36. package/tools/ordinals/getInscription.ts +1 -3
  37. package/tools/ordinals/getTokenByIdOrTicker.ts +1 -3
  38. package/tools/ordinals/index.ts +4 -1
  39. package/tools/ordinals/marketListings.ts +1 -1
  40. package/tools/ordinals/marketSales.ts +1 -1
  41. package/tools/ordinals/searchInscriptions.ts +1 -1
  42. package/tools/utils/aip.ts +31 -22
  43. package/tools/utils/installAgentMaster.ts +8 -9
  44. package/tools/utils/toolRegistration.ts +21 -22
  45. package/tools/utils/transactionBuilder.ts +21 -14
  46. package/tools/wallet/a2bPublishMcp.ts +15 -5
  47. package/tools/wallet/brc100.ts +541 -310
  48. package/tools/wallet/cancelListing.ts +29 -37
  49. package/tools/wallet/createOrdinals.ts +43 -27
  50. package/tools/wallet/fetchPaymentUtxos.ts +7 -5
  51. package/tools/wallet/gatherCollectionInfo.ts +3 -5
  52. package/tools/wallet/getAddress.ts +1 -4
  53. package/tools/wallet/getBalance.ts +15 -15
  54. package/tools/wallet/{getBalanceDroplet.ts → getBalanceDroplit.ts} +6 -6
  55. package/tools/wallet/getBsv21Balances.ts +20 -14
  56. package/tools/wallet/getLockData.ts +11 -11
  57. package/tools/wallet/getOrdinals.ts +26 -20
  58. package/tools/wallet/integratedWallet.ts +55 -53
  59. package/tools/wallet/listOrdinal.ts +40 -39
  60. package/tools/wallet/listTokens.ts +18 -17
  61. package/tools/wallet/lockBsv.ts +36 -22
  62. package/tools/wallet/mintCollection.ts +15 -10
  63. package/tools/wallet/opnsDeregister.ts +29 -39
  64. package/tools/wallet/opnsRegister.ts +29 -39
  65. package/tools/wallet/purchaseListing.test.ts +27 -0
  66. package/tools/wallet/purchaseListing.ts +59 -40
  67. package/tools/wallet/refreshUtxos.ts +1 -1
  68. package/tools/wallet/schemas.ts +0 -13
  69. package/tools/wallet/sendAllBsv.ts +26 -20
  70. package/tools/wallet/sendBsv.ts +107 -0
  71. package/tools/wallet/setupDroplit.ts +157 -0
  72. package/tools/wallet/signBsm.ts +18 -18
  73. package/tools/wallet/sweepBsv.ts +48 -27
  74. package/tools/wallet/sweepBsv21.ts +42 -28
  75. package/tools/wallet/sweepOrdinals.ts +38 -26
  76. package/tools/wallet/tools.ts +11 -13
  77. package/tools/wallet/transactionHelper.ts +13 -5
  78. package/tools/wallet/transferOrdToken.ts +72 -84
  79. package/tools/wallet/unlockBsv.ts +23 -17
  80. package/tools/wallet/utxo.ts +1 -0
  81. package/tools/wallet/wallet.ts +1 -13
  82. package/tools/wallet/sendToAddress.ts +0 -74
  83. package/tools/wallet/setupDroplet.ts +0 -161
package/index.ts CHANGED
@@ -1,23 +1,15 @@
1
1
  #!/usr/bin/env bun
2
2
 
3
- // Redirect console.log/warn/info/debug to stderr in stdio mode.
4
- // MCP stdio transport uses stdout exclusively for JSON-RPC messages —
5
- // any stray stdout output corrupts the protocol. Must run before any import.
6
- const _isStdio =
7
- process.argv.includes("--stdio") ||
8
- process.env.TRANSPORT?.toLowerCase() === "stdio";
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
- : (process.env.TRANSPORT?.toLowerCase() || "http"), // 'stdio' or 'http'/default
112
+ : process.env.TRANSPORT?.toLowerCase() || "http", // 'stdio' or 'http'/default
116
113
  port: Number.parseInt(process.env.PORT || "3000", 10),
117
114
 
118
- // --- Droplet API Configuration ---
119
- useDropletApi: process.env.USE_DROPLET_API === "true",
120
- dropletApiUrl: process.env.DROPLET_API_URL || "http://127.0.0.1:4000",
121
- dropletFaucetName: process.env.DROPLET_FAUCET_NAME || "",
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(server: McpServer, wallet?: Wallet) {
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: "No wallet configured. Set PRIVATE_KEY_WIF or generate keys.",
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
- const { paymentUtxos } = await wallet.getUtxos();
501
- const address = wallet.getAddress();
501
+ let address: string | undefined;
502
502
  let totalSatoshis = 0;
503
- for (const utxo of paymentUtxos) {
504
- totalSatoshis += utxo.satoshis || 0;
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: paymentUtxos.length,
560
+ utxoCount,
529
561
  },
530
562
  address,
531
- utxos: paymentUtxos.slice(0, 50).map((u) => ({
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
- { description: "Interactive BSV dashboard with Explorer, Wallet, and Ordinals tabs" },
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 = "<html><body><p>Dashboard not built. Run <code>bun run build:view</code> to enable it.</p></body></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
- USE_DROPLET_API Use Droplet API for transactions (default: false)
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
- ` USE_DROPLET_API: ${CONFIG.useDropletApi ? "Set (true)" : "Not Set/false"}`,
1214
+ ` USE_DROPLIT_API: ${CONFIG.useDroplitApi ? "Set (true)" : "Not Set/false"}`,
788
1215
  );
789
- if (CONFIG.useDropletApi) {
790
- logFunc(` DROPLET_API_URL: ${CONFIG.dropletApiUrl}`);
791
- logFunc(` DROPLET_FAUCET_NAME: ${CONFIG.dropletFaucetName || "Not Set"}`);
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 Droplet API mode
886
- if (CONFIG.useDropletApi && CONFIG.dropletFaucetName) {
887
- // Initialize IntegratedWallet in Droplet mode
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
- useDropletApi: true,
892
- dropletConfig: {
893
- apiUrl: CONFIG.dropletApiUrl,
894
- faucetName: CONFIG.dropletFaucetName,
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: Droplet API mode initialized successfully (Faucet: ${CONFIG.dropletFaucetName}).\x1b[0m`,
1327
+ `\x1b[32mINFO: Droplit API mode initialized successfully (Faucet: ${CONFIG.droplitFaucetName}).\x1b[0m`,
901
1328
  );
902
1329
  logFunc(
903
- `\x1b[33mNOTE: Using Droplet API at ${CONFIG.dropletApiUrl}\x1b[0m`,
1330
+ `\x1b[33mNOTE: Using Droplit API at ${CONFIG.droplitApiUrl}\x1b[0m`,
904
1331
  );
905
1332
  logFunc(
906
- "\x1b[33mNOTE: Local keys are ignored in Droplet API mode\x1b[0m",
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 Droplet API mode: ${e instanceof Error ? e.message : String(e)}. Wallet-dependent tools will be unavailable.\x1b[0m`,
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(`Starting BSV MCP Server in Streamable HTTP mode on port ${port}...`);
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: { token: string; clientId: string; scopes: string[] } | undefined;
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
- { status: 404, headers: { "Content-Type": "application/json", ...corsHeaders } },
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, { authInfo });
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
  }