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.
- package/CHANGELOG.md +83 -10
- package/README.md +13 -13
- package/dist/app.html +483 -106
- package/dist/index.js +227181 -117395
- package/index.ts +463 -57
- package/package.json +24 -15
- package/prompts/bsvSdk/auth.ts +1 -1
- package/prompts/bsvSdk/cryptography.ts +1 -1
- package/prompts/bsvSdk/overview.ts +1 -1
- package/prompts/bsvSdk/primitives.ts +1 -1
- package/prompts/bsvSdk/script.ts +1 -1
- package/prompts/bsvSdk/transaction.ts +1 -1
- package/prompts/bsvSdk/wallet.ts +1 -1
- package/prompts/ordinals.ts +1 -1
- package/test-mcp-server.ts +26 -4
- package/tools/a2b/discover.ts +2 -2
- package/tools/bap/friend.ts +17 -24
- package/tools/bap/generate.test.ts +34 -32
- package/tools/bap/generate.ts +48 -43
- package/tools/bap/getCurrentAddress.ts +1 -3
- package/tools/bap/getId.test.ts +18 -10
- package/tools/bap/getId.ts +8 -6
- package/tools/bap/utils.ts +1 -1
- package/tools/bsocial/bmapReadPosts.ts +16 -2
- package/tools/bsocial/createPost.ts +21 -10
- package/tools/bsocial/readPosts.ts +10 -4
- package/tools/bsv/decodeTransaction.ts +1 -1
- package/tools/bsv/explore.ts +0 -4
- package/tools/bsv/getPrice.ts +1 -1
- package/tools/bsv/token.ts +14 -6
- package/tools/constants.ts +6 -2
- package/tools/index.ts +10 -10
- package/tools/mnee/getBalance.ts +1 -1
- package/tools/mnee/parseTx.ts +1 -1
- package/tools/mnee/sendMnee.ts +30 -8
- package/tools/ordinals/getInscription.ts +1 -3
- package/tools/ordinals/getTokenByIdOrTicker.ts +1 -3
- package/tools/ordinals/index.ts +4 -1
- package/tools/ordinals/marketListings.ts +1 -1
- package/tools/ordinals/marketSales.ts +1 -1
- package/tools/ordinals/searchInscriptions.ts +1 -1
- package/tools/utils/aip.ts +31 -22
- package/tools/utils/installAgentMaster.ts +8 -9
- package/tools/utils/toolRegistration.ts +21 -22
- package/tools/utils/transactionBuilder.ts +21 -14
- package/tools/wallet/a2bPublishMcp.ts +15 -5
- package/tools/wallet/brc100.ts +541 -310
- package/tools/wallet/cancelListing.ts +29 -37
- package/tools/wallet/createOrdinals.ts +43 -27
- package/tools/wallet/fetchPaymentUtxos.ts +7 -5
- package/tools/wallet/gatherCollectionInfo.ts +3 -5
- package/tools/wallet/getAddress.ts +1 -4
- package/tools/wallet/getBalance.ts +15 -15
- package/tools/wallet/{getBalanceDroplet.ts → getBalanceDroplit.ts} +6 -6
- package/tools/wallet/getBsv21Balances.ts +20 -14
- package/tools/wallet/getLockData.ts +11 -11
- package/tools/wallet/getOrdinals.ts +26 -20
- package/tools/wallet/integratedWallet.ts +55 -53
- package/tools/wallet/listOrdinal.ts +40 -39
- package/tools/wallet/listTokens.ts +18 -17
- package/tools/wallet/lockBsv.ts +36 -22
- package/tools/wallet/mintCollection.ts +15 -10
- package/tools/wallet/opnsDeregister.ts +29 -39
- package/tools/wallet/opnsRegister.ts +29 -39
- package/tools/wallet/purchaseListing.test.ts +27 -0
- package/tools/wallet/purchaseListing.ts +59 -40
- package/tools/wallet/refreshUtxos.ts +1 -1
- package/tools/wallet/schemas.ts +0 -13
- package/tools/wallet/sendAllBsv.ts +26 -20
- package/tools/wallet/sendBsv.ts +107 -0
- package/tools/wallet/setupDroplit.ts +157 -0
- package/tools/wallet/signBsm.ts +18 -18
- package/tools/wallet/sweepBsv.ts +48 -27
- package/tools/wallet/sweepBsv21.ts +42 -28
- package/tools/wallet/sweepOrdinals.ts +38 -26
- package/tools/wallet/tools.ts +11 -13
- package/tools/wallet/transactionHelper.ts +13 -5
- package/tools/wallet/transferOrdToken.ts +72 -84
- package/tools/wallet/unlockBsv.ts +23 -17
- package/tools/wallet/utxo.ts +1 -0
- package/tools/wallet/wallet.ts +1 -13
- package/tools/wallet/sendToAddress.ts +0 -74
- package/tools/wallet/setupDroplet.ts +0 -161
package/index.ts
CHANGED
|
@@ -1,23 +1,15 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
if (_isStdio) {
|
|
10
|
-
const _err = console.error.bind(console);
|
|
11
|
-
console.log = (...a: unknown[]) => _err("[log]", ...a);
|
|
12
|
-
console.warn = (...a: unknown[]) => _err("[warn]", ...a);
|
|
13
|
-
console.info = (...a: unknown[]) => _err("[info]", ...a);
|
|
14
|
-
console.debug = (...a: unknown[]) => _err("[debug]", ...a);
|
|
15
|
-
}
|
|
3
|
+
// Must stay the first import. The stdio transport owns stdout for JSON-RPC, so
|
|
4
|
+
// the console redirect has to be installed before any other module initializes
|
|
5
|
+
// and gets a chance to log. This was previously a statement block below the
|
|
6
|
+
// imports, which cannot work: ES imports are evaluated before any top-level
|
|
7
|
+
// statement, so every dependency had already initialized by the time it ran.
|
|
8
|
+
import "./utils/stdioGuard";
|
|
16
9
|
|
|
17
10
|
import { readFile } from "node:fs/promises";
|
|
18
11
|
import os from "node:os";
|
|
19
|
-
import path from "node:path";
|
|
20
|
-
import { dirname, join } from "node:path";
|
|
12
|
+
import path, { dirname, join } from "node:path";
|
|
21
13
|
import { fileURLToPath } from "node:url";
|
|
22
14
|
import { PrivateKey } from "@bsv/sdk";
|
|
23
15
|
import {
|
|
@@ -27,6 +19,7 @@ import {
|
|
|
27
19
|
} from "@modelcontextprotocol/ext-apps/server";
|
|
28
20
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
29
21
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
22
|
+
import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js";
|
|
30
23
|
import { z } from "zod";
|
|
31
24
|
import packageJson from "./package.json";
|
|
32
25
|
import { registerAllPrompts } from "./prompts/index.ts";
|
|
@@ -35,8 +28,6 @@ import { getBsvPriceWithCache } from "./tools/bsv/getPrice.ts";
|
|
|
35
28
|
import { registerAllTools, type ToolsConfig } from "./tools/index.ts";
|
|
36
29
|
import { IntegratedWallet } from "./tools/wallet/integratedWallet.ts";
|
|
37
30
|
import { Wallet } from "./tools/wallet/wallet.ts";
|
|
38
|
-
import { initWallet, destroyWallet } from "./utils/walletInit.ts";
|
|
39
|
-
import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js";
|
|
40
31
|
import {
|
|
41
32
|
type BSVJWTPayload,
|
|
42
33
|
createMCPJWTValidator,
|
|
@@ -44,6 +35,11 @@ import {
|
|
|
44
35
|
} from "./utils/jwtValidator.ts";
|
|
45
36
|
import { SecureKeyManager } from "./utils/keyManager.ts";
|
|
46
37
|
import { setServerInstance } from "./utils/passphrasePrompt.ts";
|
|
38
|
+
import {
|
|
39
|
+
destroyWallet,
|
|
40
|
+
initWallet,
|
|
41
|
+
setSpendingApprovalServerInstance,
|
|
42
|
+
} from "./utils/walletInit.ts";
|
|
47
43
|
|
|
48
44
|
// Initialize server variable (used for stdio mode and passphrase detection)
|
|
49
45
|
let server: McpServer | undefined;
|
|
@@ -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
|
-
:
|
|
112
|
+
: process.env.TRANSPORT?.toLowerCase() || "http", // 'stdio' or 'http'/default
|
|
117
113
|
port: Number.parseInt(process.env.PORT || "3000", 10),
|
|
118
114
|
|
|
119
|
-
// ---
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
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(
|
|
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:
|
|
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
|
-
{
|
|
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 =
|
|
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
|
-
|
|
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
|
-
`
|
|
1214
|
+
` USE_DROPLIT_API: ${CONFIG.useDroplitApi ? "Set (true)" : "Not Set/false"}`,
|
|
816
1215
|
);
|
|
817
|
-
if (CONFIG.
|
|
818
|
-
logFunc(`
|
|
819
|
-
logFunc(`
|
|
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
|
|
914
|
-
if (CONFIG.
|
|
915
|
-
// Initialize IntegratedWallet in
|
|
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
|
-
|
|
920
|
-
|
|
921
|
-
apiUrl: CONFIG.
|
|
922
|
-
faucetName: CONFIG.
|
|
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:
|
|
1327
|
+
`\x1b[32mINFO: Droplit API mode initialized successfully (Faucet: ${CONFIG.droplitFaucetName}).\x1b[0m`,
|
|
929
1328
|
);
|
|
930
1329
|
logFunc(
|
|
931
|
-
`\x1b[33mNOTE: Using
|
|
1330
|
+
`\x1b[33mNOTE: Using Droplit API at ${CONFIG.droplitApiUrl}\x1b[0m`,
|
|
932
1331
|
);
|
|
933
1332
|
logFunc(
|
|
934
|
-
"\x1b[33mNOTE: Local keys are ignored in
|
|
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
|
|
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(
|
|
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:
|
|
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
|
-
{
|
|
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, {
|
|
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
|
}
|