bsv-mcp 0.2.15 → 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 +83 -10
  2. package/README.md +13 -13
  3. package/dist/app.html +483 -106
  4. package/dist/index.js +227181 -117395
  5. package/index.ts +463 -57
  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;
@@ -113,13 +109,13 @@ const CONFIG = {
113
109
  // --stdio CLI flag takes precedence over TRANSPORT env var (matches neighborhood plugin pattern)
114
110
  transportMode: process.argv.includes("--stdio")
115
111
  ? "stdio"
116
- : (process.env.TRANSPORT?.toLowerCase() || "http"), // 'stdio' or 'http'/default
112
+ : process.env.TRANSPORT?.toLowerCase() || "http", // 'stdio' or 'http'/default
117
113
  port: Number.parseInt(process.env.PORT || "3000", 10),
118
114
 
119
- // --- Droplet API Configuration ---
120
- useDropletApi: process.env.USE_DROPLET_API === "true",
121
- dropletApiUrl: process.env.DROPLET_API_URL || "http://127.0.0.1:4000",
122
- 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 || "",
123
119
 
124
120
  // --- OAuth Configuration ---
125
121
  enableOAuth: process.env.ENABLE_OAUTH !== "false", // Enabled by default
@@ -279,7 +275,11 @@ async function initializeKeys(): Promise<{
279
275
  const APP_RESOURCE_URI = "ui://bsv-mcp/app.html";
280
276
  const __appDirname = dirname(fileURLToPath(import.meta.url));
281
277
 
282
- function registerMcpAppTools(server: McpServer, wallet?: Wallet, ctx?: import("@1sat/actions").OneSatContext) {
278
+ function registerMcpAppTools(
279
+ server: McpServer,
280
+ wallet?: Wallet,
281
+ ctx?: import("@1sat/actions").OneSatContext,
282
+ ) {
283
283
  // Primary dashboard tool — model calls this to open the UI
284
284
  registerAppTool(
285
285
  server,
@@ -333,8 +333,7 @@ function registerMcpAppTools(server: McpServer, wallet?: Wallet, ctx?: import("@
333
333
  const res = await fetch(
334
334
  `https://junglebus.gorillapool.io/v1/transaction/get/${txid}`,
335
335
  );
336
- if (!res.ok)
337
- throw new Error(`Transaction not found: ${res.status}`);
336
+ if (!res.ok) throw new Error(`Transaction not found: ${res.status}`);
338
337
  const jbData = (await res.json()) as Record<string, unknown>;
339
338
 
340
339
  const { Transaction, Utils } = await import("@bsv/sdk");
@@ -491,7 +490,8 @@ function registerMcpAppTools(server: McpServer, wallet?: Wallet, ctx?: import("@
491
490
  },
492
491
  ],
493
492
  structuredContent: {
494
- error: "No wallet configured. Set PRIVATE_KEY_WIF or generate keys.",
493
+ error:
494
+ "No wallet configured. Set PRIVATE_KEY_WIF or generate keys.",
495
495
  },
496
496
  _meta: { viewUUID: crypto.randomUUID() },
497
497
  };
@@ -660,12 +660,410 @@ function registerMcpAppTools(server: McpServer, wallet?: Wallet, ctx?: import("@
660
660
  },
661
661
  );
662
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
+
663
1058
  // Register the HTML resource
664
1059
  registerAppResource(
665
1060
  server,
666
1061
  "BSV Dashboard",
667
1062
  APP_RESOURCE_URI,
668
- { description: "Interactive BSV dashboard with Explorer, Wallet, and Ordinals tabs" },
1063
+ {
1064
+ description:
1065
+ "Interactive BSV dashboard with Explorer, Wallet, and Ordinals tabs",
1066
+ },
669
1067
  async () => {
670
1068
  // When running from bundle (dist/index.js), app.html is a sibling.
671
1069
  // When running from source (index.ts), it's in dist/.
@@ -676,7 +1074,8 @@ function registerMcpAppTools(server: McpServer, wallet?: Wallet, ctx?: import("@
676
1074
  try {
677
1075
  html = await readFile(distPath, "utf-8");
678
1076
  } catch {
679
- 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>";
680
1079
  }
681
1080
  return {
682
1081
  contents: [
@@ -718,7 +1117,7 @@ Environment Variables:
718
1117
  DISABLE_BSOCIAL_TOOLS Disable BSocial tools (default: false)
719
1118
  ENABLE_A2B_TOOLS Enable A2B tools (default: false)
720
1119
  DISABLE_BROADCASTING Disable transaction broadcasting (default: false)
721
- USE_DROPLET_API Use Droplet API for transactions (default: false)
1120
+ USE_DROPLIT_API Use Droplit API for transactions (default: false)
722
1121
 
723
1122
  Tool Categories:
724
1123
  BSV Tools: Price lookup, transaction decoding, validation
@@ -812,11 +1211,11 @@ Authentication:
812
1211
  ` DISABLE_BROADCASTING: ${process.env.DISABLE_BROADCASTING === "true" ? "Set (true)" : "Not Set/false"}`,
813
1212
  );
814
1213
  logFunc(
815
- ` USE_DROPLET_API: ${CONFIG.useDropletApi ? "Set (true)" : "Not Set/false"}`,
1214
+ ` USE_DROPLIT_API: ${CONFIG.useDroplitApi ? "Set (true)" : "Not Set/false"}`,
816
1215
  );
817
- if (CONFIG.useDropletApi) {
818
- logFunc(` DROPLET_API_URL: ${CONFIG.dropletApiUrl}`);
819
- 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"}`);
820
1219
  }
821
1220
 
822
1221
  logFunc("\nKey Source:");
@@ -910,28 +1309,28 @@ Authentication:
910
1309
  let remoteServices: import("@1sat/wallet-remote").OneSatServices | undefined;
911
1310
 
912
1311
  if (CONFIG.loadTools) {
913
- // Check if we should use Droplet API mode
914
- if (CONFIG.useDropletApi && CONFIG.dropletFaucetName) {
915
- // 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
916
1315
  if (CONFIG.loadWalletTools) {
917
1316
  try {
918
1317
  integratedWallet = new IntegratedWallet({
919
- useDropletApi: true,
920
- dropletConfig: {
921
- apiUrl: CONFIG.dropletApiUrl,
922
- faucetName: CONFIG.dropletFaucetName,
1318
+ useDroplitApi: true,
1319
+ droplitConfig: {
1320
+ apiUrl: CONFIG.droplitApiUrl,
1321
+ faucetName: CONFIG.droplitFaucetName,
923
1322
  },
924
1323
  paymentKey: payPk,
925
1324
  identityKey: identityPk,
926
1325
  });
927
1326
  logFunc(
928
- `\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`,
929
1328
  );
930
1329
  logFunc(
931
- `\x1b[33mNOTE: Using Droplet API at ${CONFIG.dropletApiUrl}\x1b[0m`,
1330
+ `\x1b[33mNOTE: Using Droplit API at ${CONFIG.droplitApiUrl}\x1b[0m`,
932
1331
  );
933
1332
  logFunc(
934
- "\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",
935
1334
  );
936
1335
  wallet = integratedWallet.getLocalWallet();
937
1336
 
@@ -941,7 +1340,7 @@ Authentication:
941
1340
  effectiveConfig.loadBsocialTools = false;
942
1341
  } catch (e) {
943
1342
  logFunc(
944
- `\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`,
945
1344
  );
946
1345
  integratedWallet = undefined;
947
1346
  effectiveConfig.loadWalletTools = false;
@@ -1058,6 +1457,7 @@ Authentication:
1058
1457
  // Stdio: single server instance, single transport
1059
1458
  server = createConfiguredServer(serverFactoryOpts);
1060
1459
  setServerInstance(server);
1460
+ setSpendingApprovalServerInstance(server);
1061
1461
  const transport = new StdioServerTransport();
1062
1462
  await server.connect(transport);
1063
1463
  logFunc("BSV MCP Server running on stdio");
@@ -1080,7 +1480,9 @@ Authentication:
1080
1480
  }
1081
1481
  >();
1082
1482
 
1083
- 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
+ );
1084
1486
  if (CONFIG.enableOAuth) {
1085
1487
  logFunc(`OAuth 2.1 authentication enabled`);
1086
1488
  logFunc(` Issuer: ${CONFIG.oauthIssuer}`);
@@ -1096,8 +1498,7 @@ Authentication:
1096
1498
  "Access-Control-Allow-Methods": "GET, POST, DELETE, OPTIONS",
1097
1499
  "Access-Control-Allow-Headers":
1098
1500
  "Content-Type, Authorization, mcp-session-id, Last-Event-ID, mcp-protocol-version",
1099
- "Access-Control-Expose-Headers":
1100
- "mcp-session-id, mcp-protocol-version",
1501
+ "Access-Control-Expose-Headers": "mcp-session-id, mcp-protocol-version",
1101
1502
  } as const;
1102
1503
 
1103
1504
  /**
@@ -1154,10 +1555,7 @@ Authentication:
1154
1555
  "bsv:tokens",
1155
1556
  ],
1156
1557
  response_types_supported: ["code"],
1157
- grant_types_supported: [
1158
- "authorization_code",
1159
- "refresh_token",
1160
- ],
1558
+ grant_types_supported: ["authorization_code", "refresh_token"],
1161
1559
  token_endpoint_auth_methods_supported: ["none"],
1162
1560
  code_challenge_methods_supported: ["S256"],
1163
1561
  },
@@ -1203,7 +1601,9 @@ Authentication:
1203
1601
  // --- MCP Streamable HTTP endpoint ---
1204
1602
  if (url.pathname === "/mcp") {
1205
1603
  // Validate OAuth
1206
- let authInfo: { token: string; clientId: string; scopes: string[] } | undefined;
1604
+ let authInfo:
1605
+ | { token: string; clientId: string; scopes: string[] }
1606
+ | undefined;
1207
1607
  try {
1208
1608
  const userCtx = await validateAuth(req);
1209
1609
  if (userCtx) {
@@ -1247,10 +1647,18 @@ Authentication:
1247
1647
  error: { code: -32001, message: "Session not found" },
1248
1648
  id: null,
1249
1649
  }),
1250
- { status: 404, headers: { "Content-Type": "application/json", ...corsHeaders } },
1650
+ {
1651
+ status: 404,
1652
+ headers: {
1653
+ "Content-Type": "application/json",
1654
+ ...corsHeaders,
1655
+ },
1656
+ },
1251
1657
  );
1252
1658
  }
1253
- const response = await session.transport.handleRequest(req, { authInfo });
1659
+ const response = await session.transport.handleRequest(req, {
1660
+ authInfo,
1661
+ });
1254
1662
  // Add CORS headers to transport response
1255
1663
  for (const [k, v] of Object.entries(corsHeaders)) {
1256
1664
  if (!response.headers.has(k)) response.headers.set(k, v);
@@ -1290,9 +1698,7 @@ Authentication:
1290
1698
  },
1291
1699
  });
1292
1700
 
1293
- logFunc(
1294
- `Bun server listening on http://localhost:${port}`,
1295
- );
1701
+ logFunc(`Bun server listening on http://localhost:${port}`);
1296
1702
  logFunc(" MCP Endpoint: /mcp (Streamable HTTP)");
1297
1703
  logFunc(" OAuth Discovery: /.well-known/oauth-protected-resource");
1298
1704
  }