bsv-mcp 0.2.15 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (84) hide show
  1. package/CHANGELOG.md +94 -10
  2. package/README.md +95 -13
  3. package/dist/app.html +483 -106
  4. package/dist/index.js +227724 -117380
  5. package/index.ts +523 -63
  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/tools/a2b/discover.ts +2 -2
  16. package/tools/bap/friend.ts +17 -24
  17. package/tools/bap/generate.test.ts +34 -32
  18. package/tools/bap/generate.ts +48 -43
  19. package/tools/bap/getCurrentAddress.ts +1 -3
  20. package/tools/bap/getId.test.ts +18 -10
  21. package/tools/bap/getId.ts +8 -6
  22. package/tools/bap/utils.ts +1 -1
  23. package/tools/bsocial/bmapReadPosts.ts +16 -2
  24. package/tools/bsocial/createPost.ts +21 -10
  25. package/tools/bsocial/readPosts.ts +10 -4
  26. package/tools/bsv/decodeTransaction.ts +1 -1
  27. package/tools/bsv/explore.ts +0 -4
  28. package/tools/bsv/getPrice.ts +1 -1
  29. package/tools/bsv/token.ts +14 -6
  30. package/tools/constants.ts +6 -2
  31. package/tools/index.ts +22 -13
  32. package/tools/mnee/getBalance.ts +1 -1
  33. package/tools/mnee/parseTx.ts +1 -1
  34. package/tools/mnee/sendMnee.ts +30 -8
  35. package/tools/ordinals/getInscription.ts +1 -3
  36. package/tools/ordinals/getTokenByIdOrTicker.ts +1 -3
  37. package/tools/ordinals/index.ts +4 -1
  38. package/tools/ordinals/marketListings.ts +1 -1
  39. package/tools/ordinals/marketSales.ts +1 -1
  40. package/tools/ordinals/searchInscriptions.ts +1 -1
  41. package/tools/utils/aip.ts +31 -22
  42. package/tools/utils/installAgentMaster.ts +8 -9
  43. package/tools/utils/toolRegistration.ts +21 -22
  44. package/tools/utils/transactionBuilder.ts +21 -14
  45. package/tools/wallet/a2bPublishMcp.ts +15 -5
  46. package/tools/wallet/brc100.ts +567 -312
  47. package/tools/wallet/cancelListing.ts +29 -37
  48. package/tools/wallet/createOrdinals.ts +43 -27
  49. package/tools/wallet/droplit.ts +124 -0
  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 +53 -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 +151 -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 +17 -17
  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/test-mcp-server.ts +0 -157
  83. package/tools/wallet/sendToAddress.ts +0 -74
  84. 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,11 @@ 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";
31
+ import { DroplitClient, readDroplitSponsorConfig } from "./utils/droplit";
32
+ import {
33
+ initializeKeysForWalletMode,
34
+ readExternalWalletConfig,
35
+ } from "./utils/externalWalletConfig";
40
36
  import {
41
37
  type BSVJWTPayload,
42
38
  createMCPJWTValidator,
@@ -44,6 +40,12 @@ import {
44
40
  } from "./utils/jwtValidator.ts";
45
41
  import { SecureKeyManager } from "./utils/keyManager.ts";
46
42
  import { setServerInstance } from "./utils/passphrasePrompt.ts";
43
+ import {
44
+ destroyWallet,
45
+ initExternalWallet,
46
+ initWallet,
47
+ setSpendingApprovalServerInstance,
48
+ } from "./utils/walletInit.ts";
47
49
 
48
50
  // Initialize server variable (used for stdio mode and passphrase detection)
49
51
  let server: McpServer | undefined;
@@ -113,13 +115,13 @@ const CONFIG = {
113
115
  // --stdio CLI flag takes precedence over TRANSPORT env var (matches neighborhood plugin pattern)
114
116
  transportMode: process.argv.includes("--stdio")
115
117
  ? "stdio"
116
- : (process.env.TRANSPORT?.toLowerCase() || "http"), // 'stdio' or 'http'/default
118
+ : process.env.TRANSPORT?.toLowerCase() || "http", // 'stdio' or 'http'/default
117
119
  port: Number.parseInt(process.env.PORT || "3000", 10),
118
120
 
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 || "",
121
+ // --- Droplit API Configuration ---
122
+ useDroplitApi: process.env.USE_DROPLIT_API === "true",
123
+ droplitApiUrl: process.env.DROPLIT_API_URL || "http://127.0.0.1:4000",
124
+ droplitFaucetName: process.env.DROPLIT_FAUCET_NAME || "",
123
125
 
124
126
  // --- OAuth Configuration ---
125
127
  enableOAuth: process.env.ENABLE_OAUTH !== "false", // Enabled by default
@@ -279,7 +281,11 @@ async function initializeKeys(): Promise<{
279
281
  const APP_RESOURCE_URI = "ui://bsv-mcp/app.html";
280
282
  const __appDirname = dirname(fileURLToPath(import.meta.url));
281
283
 
282
- function registerMcpAppTools(server: McpServer, wallet?: Wallet, ctx?: import("@1sat/actions").OneSatContext) {
284
+ function registerMcpAppTools(
285
+ server: McpServer,
286
+ wallet?: Wallet,
287
+ ctx?: import("@1sat/actions").OneSatContext,
288
+ ) {
283
289
  // Primary dashboard tool — model calls this to open the UI
284
290
  registerAppTool(
285
291
  server,
@@ -333,8 +339,7 @@ function registerMcpAppTools(server: McpServer, wallet?: Wallet, ctx?: import("@
333
339
  const res = await fetch(
334
340
  `https://junglebus.gorillapool.io/v1/transaction/get/${txid}`,
335
341
  );
336
- if (!res.ok)
337
- throw new Error(`Transaction not found: ${res.status}`);
342
+ if (!res.ok) throw new Error(`Transaction not found: ${res.status}`);
338
343
  const jbData = (await res.json()) as Record<string, unknown>;
339
344
 
340
345
  const { Transaction, Utils } = await import("@bsv/sdk");
@@ -491,7 +496,8 @@ function registerMcpAppTools(server: McpServer, wallet?: Wallet, ctx?: import("@
491
496
  },
492
497
  ],
493
498
  structuredContent: {
494
- error: "No wallet configured. Set PRIVATE_KEY_WIF or generate keys.",
499
+ error:
500
+ "No wallet configured. Set PRIVATE_KEY_WIF or generate keys.",
495
501
  },
496
502
  _meta: { viewUUID: crypto.randomUUID() },
497
503
  };
@@ -660,12 +666,410 @@ function registerMcpAppTools(server: McpServer, wallet?: Wallet, ctx?: import("@
660
666
  },
661
667
  );
662
668
 
669
+ // App-only: scan an address for categorized UTXOs (funding, ordinals, BSV-21 tokens)
670
+ registerAppTool(
671
+ server,
672
+ "app_sweep_scan",
673
+ {
674
+ title: "Sweep Scan",
675
+ description:
676
+ "App-only: scans a Bitcoin address for categorized UTXOs — funding, ordinals, and BSV-21 tokens.",
677
+ inputSchema: {
678
+ address: z.string().describe("Bitcoin address to scan"),
679
+ },
680
+ _meta: {
681
+ ui: { resourceUri: APP_RESOURCE_URI, visibility: ["app"] },
682
+ },
683
+ },
684
+ async (args) => {
685
+ const { address } = args as { address: string };
686
+ try {
687
+ const res = await fetch(
688
+ `https://ordinals.gorillapool.io/api/txos/address/${address}/unspent?limit=1000`,
689
+ );
690
+ if (!res.ok) throw new Error(`GorillaPool API error: ${res.status}`);
691
+ const utxos = (await res.json()) as Array<Record<string, unknown>>;
692
+
693
+ const funding: Array<{
694
+ outpoint: string;
695
+ satoshis: number;
696
+ lockingScript: string;
697
+ }> = [];
698
+ const ordinals: Array<{
699
+ outpoint: string;
700
+ satoshis: number;
701
+ lockingScript: string;
702
+ }> = [];
703
+ const bsv21Raw: Array<{
704
+ outpoint: string;
705
+ satoshis: number;
706
+ lockingScript: string;
707
+ tokenId: string;
708
+ amount: string;
709
+ sym?: string;
710
+ dec: number;
711
+ }> = [];
712
+
713
+ for (const utxo of utxos) {
714
+ const txid = utxo.txid as string;
715
+ const vout = utxo.vout as number;
716
+ const outpoint = `${txid}_${vout}`;
717
+ const satoshis = (utxo.satoshis as number) || 0;
718
+ const script = (utxo.script as string) || "";
719
+ const origin = utxo.origin as Record<string, unknown> | undefined;
720
+ const originData = origin?.data as
721
+ | Record<string, unknown>
722
+ | undefined;
723
+
724
+ const base = { outpoint, satoshis, lockingScript: script };
725
+
726
+ if (originData?.bsv21) {
727
+ const bsv21 = originData.bsv21 as Record<string, unknown>;
728
+ bsv21Raw.push({
729
+ ...base,
730
+ tokenId: (bsv21.id as string) || "",
731
+ amount: (bsv21.amt as string) || "0",
732
+ sym: bsv21.sym as string | undefined,
733
+ dec: (bsv21.dec as number) ?? 0,
734
+ });
735
+ } else if (originData?.insc || origin?.outpoint) {
736
+ ordinals.push(base);
737
+ } else {
738
+ funding.push(base);
739
+ }
740
+ }
741
+
742
+ // Group BSV-21 tokens by tokenId
743
+ const tokenGroups = new Map<
744
+ string,
745
+ {
746
+ inputs: typeof bsv21Raw;
747
+ sym?: string;
748
+ dec: number;
749
+ }
750
+ >();
751
+ for (const item of bsv21Raw) {
752
+ let group = tokenGroups.get(item.tokenId);
753
+ if (!group) {
754
+ group = { inputs: [], sym: item.sym, dec: item.dec };
755
+ tokenGroups.set(item.tokenId, group);
756
+ }
757
+ group.inputs.push(item);
758
+ }
759
+
760
+ const bsv21Tokens: Array<{
761
+ tokenId: string;
762
+ symbol?: string;
763
+ decimals: number;
764
+ totalAmount: string;
765
+ inputs: typeof bsv21Raw;
766
+ }> = [];
767
+ for (const [tokenId, group] of tokenGroups) {
768
+ let total = BigInt(0);
769
+ for (const inp of group.inputs) {
770
+ total += BigInt(inp.amount);
771
+ }
772
+ bsv21Tokens.push({
773
+ tokenId,
774
+ symbol: group.sym,
775
+ decimals: group.dec,
776
+ totalAmount: total.toString(),
777
+ inputs: group.inputs,
778
+ });
779
+ }
780
+
781
+ const totalFundingSats = funding.reduce(
782
+ (sum, f) => sum + f.satoshis,
783
+ 0,
784
+ );
785
+
786
+ return {
787
+ content: [
788
+ {
789
+ type: "text" as const,
790
+ text: `Scanned ${address}: ${funding.length} funding, ${ordinals.length} ordinals, ${bsv21Tokens.length} token types`,
791
+ },
792
+ ],
793
+ structuredContent: {
794
+ address,
795
+ funding,
796
+ ordinals,
797
+ bsv21Tokens,
798
+ totalFundingSats,
799
+ },
800
+ _meta: { viewUUID: crypto.randomUUID() },
801
+ };
802
+ } catch (err) {
803
+ return {
804
+ content: [
805
+ {
806
+ type: "text" as const,
807
+ text: `Error: ${err instanceof Error ? err.message : String(err)}`,
808
+ },
809
+ ],
810
+ structuredContent: { error: String(err) },
811
+ _meta: { viewUUID: crypto.randomUUID() },
812
+ };
813
+ }
814
+ },
815
+ );
816
+
817
+ // App-only: prepare unsigned sweep transaction for client-side signing
818
+ registerAppTool(
819
+ server,
820
+ "app_sweep_prepare",
821
+ {
822
+ title: "Sweep Prepare",
823
+ description:
824
+ "App-only: builds an unsigned sweep transaction. Returns BEEF hex and reference for client-side signing.",
825
+ inputSchema: {
826
+ sweepType: z
827
+ .enum(["bsv", "ordinals", "bsv21"])
828
+ .describe("Type of assets to sweep"),
829
+ inputs: z
830
+ .array(
831
+ z.object({
832
+ outpoint: z.string().describe("Outpoint (txid_vout)"),
833
+ satoshis: z.number().int().describe("Satoshis in output"),
834
+ lockingScript: z.string().describe("Locking script hex"),
835
+ }),
836
+ )
837
+ .describe("UTXOs to sweep"),
838
+ },
839
+ _meta: {
840
+ ui: { resourceUri: APP_RESOURCE_URI, visibility: ["app"] },
841
+ },
842
+ },
843
+ async (args) => {
844
+ const { sweepType, inputs } = args as {
845
+ sweepType: "bsv" | "ordinals" | "bsv21";
846
+ inputs: Array<{
847
+ outpoint: string;
848
+ satoshis: number;
849
+ lockingScript: string;
850
+ }>;
851
+ };
852
+
853
+ if (!ctx) {
854
+ return {
855
+ content: [
856
+ {
857
+ type: "text" as const,
858
+ text: "BRC-100 wallet context not available",
859
+ },
860
+ ],
861
+ structuredContent: { error: "No wallet context" },
862
+ _meta: { viewUUID: crypto.randomUUID() },
863
+ };
864
+ }
865
+
866
+ try {
867
+ if (!ctx.services) throw new Error("Services not available");
868
+ if (!inputs.length) throw new Error("No inputs provided");
869
+
870
+ // Fetch and merge BEEF for all input transactions
871
+ const txids = [...new Set(inputs.map((i) => i.outpoint.split("_")[0]))];
872
+ const firstBeef = await ctx.services.getBeefForTxid(txids[0]);
873
+ for (let i = 1; i < txids.length; i++) {
874
+ const additionalBeef = await ctx.services.getBeefForTxid(txids[i]);
875
+ firstBeef.mergeBeef(additionalBeef);
876
+ }
877
+
878
+ // Build input descriptors using SDK format (txid.vout)
879
+ const inputDescriptors = inputs.map((input) => {
880
+ const [txid, voutStr] = input.outpoint.split("_");
881
+ return {
882
+ outpoint: `${txid}.${Number(voutStr)}`,
883
+ inputDescription: `Sweep ${sweepType} input`,
884
+ unlockingScriptLength: 108,
885
+ sequenceNumber: 0xffffffff,
886
+ };
887
+ });
888
+
889
+ const inputTotal = inputs.reduce((sum, i) => sum + i.satoshis, 0);
890
+
891
+ const createResult = await ctx.wallet.createAction({
892
+ description: `Sweep ${inputTotal} sats (${sweepType})`,
893
+ inputBEEF: firstBeef.toBinary(),
894
+ inputs: inputDescriptors,
895
+ outputs: [],
896
+ options: {
897
+ signAndProcess: false,
898
+ ...(sweepType !== "bsv" && {
899
+ randomizeOutputs: false,
900
+ }),
901
+ },
902
+ });
903
+
904
+ if ("error" in createResult && createResult.error) {
905
+ throw new Error(String(createResult.error));
906
+ }
907
+ if (!createResult.signableTransaction) {
908
+ throw new Error("No signable transaction returned");
909
+ }
910
+
911
+ // Map our inputs to their indices in the transaction
912
+ const { Transaction: TxClass, Utils: SdkUtils } = await import(
913
+ "@bsv/sdk"
914
+ );
915
+ const tx = TxClass.fromBEEF(createResult.signableTransaction.tx);
916
+ const ourOutpoints = new Set(
917
+ inputs.map((i) => {
918
+ const [txid, voutStr] = i.outpoint.split("_");
919
+ return `${txid}.${Number(voutStr)}`;
920
+ }),
921
+ );
922
+
923
+ const inputsToSign: Array<{
924
+ index: number;
925
+ outpoint: string;
926
+ satoshis: number;
927
+ lockingScript: string;
928
+ }> = [];
929
+ for (let idx = 0; idx < tx.inputs.length; idx++) {
930
+ const txInput = tx.inputs[idx];
931
+ const op = `${txInput.sourceTXID}.${txInput.sourceOutputIndex}`;
932
+ if (ourOutpoints.has(op)) {
933
+ const match = inputs.find((i) => {
934
+ const [t, v] = i.outpoint.split("_");
935
+ return `${t}.${Number(v)}` === op;
936
+ });
937
+ if (match) {
938
+ inputsToSign.push({
939
+ index: idx,
940
+ outpoint: match.outpoint,
941
+ satoshis: match.satoshis,
942
+ lockingScript: match.lockingScript,
943
+ });
944
+ }
945
+ }
946
+ }
947
+
948
+ const txHex = SdkUtils.toHex(createResult.signableTransaction.tx);
949
+
950
+ return {
951
+ content: [
952
+ {
953
+ type: "text" as const,
954
+ text: `Prepared ${sweepType} sweep: ${inputsToSign.length} inputs to sign`,
955
+ },
956
+ ],
957
+ structuredContent: {
958
+ txHex,
959
+ reference: createResult.signableTransaction.reference,
960
+ inputsToSign,
961
+ },
962
+ _meta: { viewUUID: crypto.randomUUID() },
963
+ };
964
+ } catch (err) {
965
+ return {
966
+ content: [
967
+ {
968
+ type: "text" as const,
969
+ text: `Error: ${err instanceof Error ? err.message : String(err)}`,
970
+ },
971
+ ],
972
+ structuredContent: { error: String(err) },
973
+ _meta: { viewUUID: crypto.randomUUID() },
974
+ };
975
+ }
976
+ },
977
+ );
978
+
979
+ // App-only: complete a sweep by broadcasting with client-signed spends
980
+ registerAppTool(
981
+ server,
982
+ "app_sweep_complete",
983
+ {
984
+ title: "Sweep Complete",
985
+ description:
986
+ "App-only: completes a sweep by broadcasting the transaction with client-signed unlocking scripts.",
987
+ inputSchema: {
988
+ reference: z
989
+ .string()
990
+ .describe("Opaque reference from app_sweep_prepare"),
991
+ spends: z
992
+ .record(
993
+ z.string(),
994
+ z.object({
995
+ unlockingScript: z
996
+ .string()
997
+ .describe("Signed unlocking script hex"),
998
+ }),
999
+ )
1000
+ .describe("Map of input index to signed unlocking script"),
1001
+ },
1002
+ _meta: {
1003
+ ui: { resourceUri: APP_RESOURCE_URI, visibility: ["app"] },
1004
+ },
1005
+ },
1006
+ async (args) => {
1007
+ const { reference, spends } = args as {
1008
+ reference: string;
1009
+ spends: Record<number, { unlockingScript: string }>;
1010
+ };
1011
+
1012
+ if (!ctx) {
1013
+ return {
1014
+ content: [
1015
+ {
1016
+ type: "text" as const,
1017
+ text: "BRC-100 wallet context not available",
1018
+ },
1019
+ ],
1020
+ structuredContent: { error: "No wallet context" },
1021
+ _meta: { viewUUID: crypto.randomUUID() },
1022
+ };
1023
+ }
1024
+
1025
+ try {
1026
+ const signResult = await ctx.wallet.signAction({
1027
+ reference,
1028
+ spends,
1029
+ options: { acceptDelayedBroadcast: false },
1030
+ });
1031
+
1032
+ if ("error" in signResult) {
1033
+ throw new Error(String(signResult.error));
1034
+ }
1035
+
1036
+ return {
1037
+ content: [
1038
+ {
1039
+ type: "text" as const,
1040
+ text: `Sweep broadcast: ${signResult.txid}`,
1041
+ },
1042
+ ],
1043
+ structuredContent: {
1044
+ txid: signResult.txid,
1045
+ success: true,
1046
+ },
1047
+ _meta: { viewUUID: crypto.randomUUID() },
1048
+ };
1049
+ } catch (err) {
1050
+ return {
1051
+ content: [
1052
+ {
1053
+ type: "text" as const,
1054
+ text: `Error: ${err instanceof Error ? err.message : String(err)}`,
1055
+ },
1056
+ ],
1057
+ structuredContent: { error: String(err) },
1058
+ _meta: { viewUUID: crypto.randomUUID() },
1059
+ };
1060
+ }
1061
+ },
1062
+ );
1063
+
663
1064
  // Register the HTML resource
664
1065
  registerAppResource(
665
1066
  server,
666
1067
  "BSV Dashboard",
667
1068
  APP_RESOURCE_URI,
668
- { description: "Interactive BSV dashboard with Explorer, Wallet, and Ordinals tabs" },
1069
+ {
1070
+ description:
1071
+ "Interactive BSV dashboard with Explorer, Wallet, and Ordinals tabs",
1072
+ },
669
1073
  async () => {
670
1074
  // When running from bundle (dist/index.js), app.html is a sibling.
671
1075
  // When running from source (index.ts), it's in dist/.
@@ -676,7 +1080,8 @@ function registerMcpAppTools(server: McpServer, wallet?: Wallet, ctx?: import("@
676
1080
  try {
677
1081
  html = await readFile(distPath, "utf-8");
678
1082
  } catch {
679
- html = "<html><body><p>Dashboard not built. Run <code>bun run build:view</code> to enable it.</p></body></html>";
1083
+ html =
1084
+ "<html><body><p>Dashboard not built. Run <code>bun run build:view</code> to enable it.</p></body></html>";
680
1085
  }
681
1086
  return {
682
1087
  contents: [
@@ -708,6 +1113,8 @@ Options:
708
1113
  Environment Variables:
709
1114
  TRANSPORT Transport mode: 'stdio' or 'http' (default: http)
710
1115
  PORT HTTP server port (default: 3000)
1116
+ BRC100_WALLET_URL Existing SDK HTTPWalletJSON signer RPC URL
1117
+ BRC100_WALLET_ORIGINATOR Signer permission origin (default: bsv-mcp.local)
711
1118
  PRIVATE_KEY_WIF Payment private key in WIF format
712
1119
  DISABLE_TOOLS Disable all tools (default: false)
713
1120
  DISABLE_WALLET_TOOLS Disable wallet tools (default: false)
@@ -718,7 +1125,7 @@ Environment Variables:
718
1125
  DISABLE_BSOCIAL_TOOLS Disable BSocial tools (default: false)
719
1126
  ENABLE_A2B_TOOLS Enable A2B tools (default: false)
720
1127
  DISABLE_BROADCASTING Disable transaction broadcasting (default: false)
721
- USE_DROPLET_API Use Droplet API for transactions (default: false)
1128
+ USE_DROPLIT_API Use Droplit API for transactions (default: false)
722
1129
 
723
1130
  Tool Categories:
724
1131
  BSV Tools: Price lookup, transaction decoding, validation
@@ -731,7 +1138,7 @@ Tool Categories:
731
1138
 
732
1139
  Authentication:
733
1140
  - Most tools work without authentication
734
- - Wallet operations require PRIVATE_KEY_WIF or generated keys
1141
+ - Wallet operations use BRC100_WALLET_URL, PRIVATE_KEY_WIF or generated keys
735
1142
  - BAP/A2B tools require identity keys (generated via bap_generate tool)
736
1143
  `);
737
1144
  process.exit(0);
@@ -743,7 +1150,25 @@ Authentication:
743
1150
  }
744
1151
 
745
1152
  // --- Initialize Keys ---
746
- const { payPk, identityPk, xprv, source: keySource } = await initializeKeys();
1153
+ const externalWallet = readExternalWalletConfig();
1154
+ const sponsorConfig = CONFIG.useDroplitApi
1155
+ ? undefined
1156
+ : readDroplitSponsorConfig();
1157
+ const keys = await initializeKeysForWalletMode(
1158
+ externalWallet,
1159
+ initializeKeys,
1160
+ );
1161
+ const {
1162
+ payPk,
1163
+ identityPk,
1164
+ xprv,
1165
+ source: keySource,
1166
+ } = keys ?? {
1167
+ payPk: undefined,
1168
+ identityPk: undefined,
1169
+ xprv: undefined,
1170
+ source: "external",
1171
+ };
747
1172
 
748
1173
  // Define persistence based on source
749
1174
  const hasPersistentPayKey =
@@ -755,6 +1180,15 @@ Authentication:
755
1180
  (keySource === "file" || keySource === "env" || keySource === "encrypted");
756
1181
 
757
1182
  const effectiveConfig = { ...CONFIG };
1183
+ if (externalWallet) {
1184
+ effectiveConfig.loadBapTools = false;
1185
+ effectiveConfig.loadBsocialTools = false;
1186
+ effectiveConfig.loadMneeTools = false;
1187
+ effectiveConfig.loadA2bTools = false;
1188
+ logFunc(
1189
+ "External BRC-100 signer selected; local key loading and generation bypassed.",
1190
+ );
1191
+ }
758
1192
 
759
1193
  // --- Configuration Logging ---
760
1194
  logFunc("\n--- BSV MCP Server Configuration ---");
@@ -770,10 +1204,10 @@ Authentication:
770
1204
  );
771
1205
  }
772
1206
  logFunc(
773
- ` PRIVATE_KEY_WIF: ${process.env.PRIVATE_KEY_WIF ? "Set (using env key)" : "Not Set (using file/generating)"}`,
1207
+ ` PRIVATE_KEY_WIF: ${externalWallet ? "Unused (external signer)" : process.env.PRIVATE_KEY_WIF ? "Set (using env key)" : "Not Set (using file/generating)"}`,
774
1208
  );
775
1209
  logFunc(
776
- ` IDENTITY_KEY_WIF: ${process.env.IDENTITY_KEY_WIF ? "Set (using env key)" : "Not Set (using file/generating)"}`,
1210
+ ` IDENTITY_KEY_WIF: ${externalWallet ? "Unused (external signer)" : process.env.IDENTITY_KEY_WIF ? "Set (using env key)" : "Not Set (using file/generating)"}`,
777
1211
  );
778
1212
  if (process.env.BSV_MCP_PASSPHRASE) {
779
1213
  logFunc(" BSV_MCP_PASSPHRASE: \x1b[31mDEPRECATED - Remove this!\x1b[0m");
@@ -812,11 +1246,11 @@ Authentication:
812
1246
  ` DISABLE_BROADCASTING: ${process.env.DISABLE_BROADCASTING === "true" ? "Set (true)" : "Not Set/false"}`,
813
1247
  );
814
1248
  logFunc(
815
- ` USE_DROPLET_API: ${CONFIG.useDropletApi ? "Set (true)" : "Not Set/false"}`,
1249
+ ` USE_DROPLIT_API: ${CONFIG.useDroplitApi ? "Set (true)" : "Not Set/false"}`,
816
1250
  );
817
- if (CONFIG.useDropletApi) {
818
- logFunc(` DROPLET_API_URL: ${CONFIG.dropletApiUrl}`);
819
- logFunc(` DROPLET_FAUCET_NAME: ${CONFIG.dropletFaucetName || "Not Set"}`);
1251
+ if (CONFIG.useDroplitApi) {
1252
+ logFunc(` DROPLIT_API_URL: ${CONFIG.droplitApiUrl}`);
1253
+ logFunc(` DROPLIT_FAUCET_NAME: ${CONFIG.droplitFaucetName || "Not Set"}`);
820
1254
  }
821
1255
 
822
1256
  logFunc("\nKey Source:");
@@ -870,11 +1304,11 @@ Authentication:
870
1304
  : "\x1b[31mDisabled\x1b[0m";
871
1305
 
872
1306
  let payKeyNote = "";
873
- if (!hasPersistentPayKey) {
1307
+ if (!externalWallet && !hasPersistentPayKey) {
874
1308
  payKeyNote = " \x1b[33m(Using generated payPk)\x1b[0m";
875
1309
  }
876
1310
  let identityKeyNote = "";
877
- if (!hasPersistentIdentityKey) {
1311
+ if (!externalWallet && !hasPersistentIdentityKey) {
878
1312
  identityKeyNote = " \x1b[33m(Using generated identityPk)\x1b[0m";
879
1313
  }
880
1314
 
@@ -910,28 +1344,36 @@ Authentication:
910
1344
  let remoteServices: import("@1sat/wallet-remote").OneSatServices | undefined;
911
1345
 
912
1346
  if (CONFIG.loadTools) {
913
- // Check if we should use Droplet API mode
914
- if (CONFIG.useDropletApi && CONFIG.dropletFaucetName) {
915
- // Initialize IntegratedWallet in Droplet mode
1347
+ // Check if we should use Droplit API mode
1348
+ if (externalWallet) {
1349
+ const chain = process.env.BSV_CHAIN ?? "main";
1350
+ if (chain !== "main" && chain !== "test")
1351
+ throw new Error("BSV_CHAIN must be main or test");
1352
+ const result = await initExternalWallet(externalWallet, chain);
1353
+ remoteCtx = result.ctx;
1354
+ remoteServices = result.services;
1355
+ logFunc(`External BRC-100 signer ready. Identity: ${result.identityKey}`);
1356
+ } else if (CONFIG.useDroplitApi && CONFIG.droplitFaucetName) {
1357
+ // Initialize IntegratedWallet in Droplit mode
916
1358
  if (CONFIG.loadWalletTools) {
917
1359
  try {
918
1360
  integratedWallet = new IntegratedWallet({
919
- useDropletApi: true,
920
- dropletConfig: {
921
- apiUrl: CONFIG.dropletApiUrl,
922
- faucetName: CONFIG.dropletFaucetName,
1361
+ useDroplitApi: true,
1362
+ droplitConfig: {
1363
+ apiUrl: CONFIG.droplitApiUrl,
1364
+ faucetName: CONFIG.droplitFaucetName,
923
1365
  },
924
1366
  paymentKey: payPk,
925
1367
  identityKey: identityPk,
926
1368
  });
927
1369
  logFunc(
928
- `\x1b[32mINFO: Droplet API mode initialized successfully (Faucet: ${CONFIG.dropletFaucetName}).\x1b[0m`,
1370
+ `\x1b[32mINFO: Droplit API mode initialized successfully (Faucet: ${CONFIG.droplitFaucetName}).\x1b[0m`,
929
1371
  );
930
1372
  logFunc(
931
- `\x1b[33mNOTE: Using Droplet API at ${CONFIG.dropletApiUrl}\x1b[0m`,
1373
+ `\x1b[33mNOTE: Using Droplit API at ${CONFIG.droplitApiUrl}\x1b[0m`,
932
1374
  );
933
1375
  logFunc(
934
- "\x1b[33mNOTE: Local keys are ignored in Droplet API mode\x1b[0m",
1376
+ "\x1b[33mNOTE: Local keys are ignored in Droplit API mode\x1b[0m",
935
1377
  );
936
1378
  wallet = integratedWallet.getLocalWallet();
937
1379
 
@@ -941,7 +1383,7 @@ Authentication:
941
1383
  effectiveConfig.loadBsocialTools = false;
942
1384
  } catch (e) {
943
1385
  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`,
1386
+ `\x1b[31mERROR: Failed to initialize Droplit API mode: ${e instanceof Error ? e.message : String(e)}. Wallet-dependent tools will be unavailable.\x1b[0m`,
945
1387
  );
946
1388
  integratedWallet = undefined;
947
1389
  effectiveConfig.loadWalletTools = false;
@@ -1006,6 +1448,16 @@ Authentication:
1006
1448
  }
1007
1449
  }
1008
1450
 
1451
+ const droplitClient =
1452
+ sponsorConfig && remoteCtx
1453
+ ? new DroplitClient({ ...sponsorConfig, wallet: remoteCtx.wallet })
1454
+ : undefined;
1455
+ if (sponsorConfig && CONFIG.loadTools && !remoteCtx) {
1456
+ throw new Error(
1457
+ "Configured Droplit sponsor requires an initialized BRC-100 wallet context",
1458
+ );
1459
+ }
1460
+
1009
1461
  // Build the shared tools config (used by server factory for each session)
1010
1462
  const toolsConfig: ToolsConfig = CONFIG.loadTools
1011
1463
  ? {
@@ -1025,6 +1477,7 @@ Authentication:
1025
1477
  disableBroadcasting: effectiveConfig.disableBroadcasting,
1026
1478
  ctx: remoteCtx,
1027
1479
  services: remoteServices,
1480
+ droplitClient,
1028
1481
  }
1029
1482
  : {
1030
1483
  enableBsvTools: false,
@@ -1058,6 +1511,7 @@ Authentication:
1058
1511
  // Stdio: single server instance, single transport
1059
1512
  server = createConfiguredServer(serverFactoryOpts);
1060
1513
  setServerInstance(server);
1514
+ setSpendingApprovalServerInstance(server);
1061
1515
  const transport = new StdioServerTransport();
1062
1516
  await server.connect(transport);
1063
1517
  logFunc("BSV MCP Server running on stdio");
@@ -1080,7 +1534,9 @@ Authentication:
1080
1534
  }
1081
1535
  >();
1082
1536
 
1083
- logFunc(`Starting BSV MCP Server in Streamable HTTP mode on port ${port}...`);
1537
+ logFunc(
1538
+ `Starting BSV MCP Server in Streamable HTTP mode on port ${port}...`,
1539
+ );
1084
1540
  if (CONFIG.enableOAuth) {
1085
1541
  logFunc(`OAuth 2.1 authentication enabled`);
1086
1542
  logFunc(` Issuer: ${CONFIG.oauthIssuer}`);
@@ -1096,8 +1552,7 @@ Authentication:
1096
1552
  "Access-Control-Allow-Methods": "GET, POST, DELETE, OPTIONS",
1097
1553
  "Access-Control-Allow-Headers":
1098
1554
  "Content-Type, Authorization, mcp-session-id, Last-Event-ID, mcp-protocol-version",
1099
- "Access-Control-Expose-Headers":
1100
- "mcp-session-id, mcp-protocol-version",
1555
+ "Access-Control-Expose-Headers": "mcp-session-id, mcp-protocol-version",
1101
1556
  } as const;
1102
1557
 
1103
1558
  /**
@@ -1154,10 +1609,7 @@ Authentication:
1154
1609
  "bsv:tokens",
1155
1610
  ],
1156
1611
  response_types_supported: ["code"],
1157
- grant_types_supported: [
1158
- "authorization_code",
1159
- "refresh_token",
1160
- ],
1612
+ grant_types_supported: ["authorization_code", "refresh_token"],
1161
1613
  token_endpoint_auth_methods_supported: ["none"],
1162
1614
  code_challenge_methods_supported: ["S256"],
1163
1615
  },
@@ -1203,7 +1655,9 @@ Authentication:
1203
1655
  // --- MCP Streamable HTTP endpoint ---
1204
1656
  if (url.pathname === "/mcp") {
1205
1657
  // Validate OAuth
1206
- let authInfo: { token: string; clientId: string; scopes: string[] } | undefined;
1658
+ let authInfo:
1659
+ | { token: string; clientId: string; scopes: string[] }
1660
+ | undefined;
1207
1661
  try {
1208
1662
  const userCtx = await validateAuth(req);
1209
1663
  if (userCtx) {
@@ -1247,10 +1701,18 @@ Authentication:
1247
1701
  error: { code: -32001, message: "Session not found" },
1248
1702
  id: null,
1249
1703
  }),
1250
- { status: 404, headers: { "Content-Type": "application/json", ...corsHeaders } },
1704
+ {
1705
+ status: 404,
1706
+ headers: {
1707
+ "Content-Type": "application/json",
1708
+ ...corsHeaders,
1709
+ },
1710
+ },
1251
1711
  );
1252
1712
  }
1253
- const response = await session.transport.handleRequest(req, { authInfo });
1713
+ const response = await session.transport.handleRequest(req, {
1714
+ authInfo,
1715
+ });
1254
1716
  // Add CORS headers to transport response
1255
1717
  for (const [k, v] of Object.entries(corsHeaders)) {
1256
1718
  if (!response.headers.has(k)) response.headers.set(k, v);
@@ -1290,9 +1752,7 @@ Authentication:
1290
1752
  },
1291
1753
  });
1292
1754
 
1293
- logFunc(
1294
- `Bun server listening on http://localhost:${port}`,
1295
- );
1755
+ logFunc(`Bun server listening on http://localhost:${port}`);
1296
1756
  logFunc(" MCP Endpoint: /mcp (Streamable HTTP)");
1297
1757
  logFunc(" OAuth Discovery: /.well-known/oauth-protected-resource");
1298
1758
  }