bsv-mcp 0.0.35 → 0.2.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 +112 -1
- package/README.md +233 -4
- package/dist/index.js +111762 -0
- package/index.ts +804 -86
- package/next-env.d.ts +6 -0
- package/package.json +57 -15
- package/prompts/bsvSdk/index.ts +5 -5
- package/prompts/bsvSdk/transaction.ts +2 -2
- package/prompts/bsvSdk/wallet.ts +1 -1
- package/prompts/index.ts +1 -1
- package/resources/bitcom.ts +132 -0
- package/resources/resources.ts +7 -3
- package/smithery.yaml +3 -0
- package/test-mcp-server.ts +157 -0
- package/tools/bap/friend.ts +267 -0
- package/tools/bap/generate.test.ts +350 -0
- package/tools/bap/generate.ts +433 -0
- package/tools/bap/getCurrentAddress.ts +76 -0
- package/tools/bap/getId.test.ts +121 -0
- package/tools/bap/getId.ts +169 -0
- package/tools/bap/index.ts +111 -0
- package/tools/bap/types.ts +66 -0
- package/tools/bap/utils.ts +33 -0
- package/tools/bigblocks/components.ts +304 -0
- package/tools/bigblocks/docs.ts +488 -0
- package/tools/bigblocks/examples.ts +527 -0
- package/tools/bigblocks/generator.ts +485 -0
- package/tools/bigblocks/index.ts +23 -0
- package/tools/bsocial/bigblocksApiClient.ts +189 -0
- package/tools/bsocial/bmapFollow.ts +125 -0
- package/tools/bsocial/bmapLikes.ts +111 -0
- package/tools/bsocial/bmapReadPosts.ts +148 -0
- package/tools/bsocial/createPost.ts +136 -0
- package/tools/bsocial/index.ts +45 -0
- package/tools/bsocial/readPosts.ts +255 -0
- package/tools/bsv/decodeTransaction.ts +5 -1
- package/tools/constants.ts +21 -0
- package/tools/index.ts +103 -17
- package/tools/mnee/index.ts +8 -0
- package/tools/mnee/parseTx.ts +1 -1
- package/tools/mnee/sendMnee.ts +1 -1
- package/tools/utils/aip.ts +46 -0
- package/tools/utils/conversion.ts +1 -0
- package/tools/utils/errorHandler.ts +76 -0
- package/tools/utils/index.ts +45 -1
- package/tools/utils/installAgentMaster.ts +84 -0
- package/tools/utils/logger.ts +20 -0
- package/tools/utils/toolConfig.ts +80 -0
- package/tools/utils/toolRegistration.ts +66 -0
- package/tools/utils/transactionBuilder.ts +196 -0
- package/tools/wallet/a2bPublishAgent.ts +8 -4
- package/tools/wallet/a2bPublishMcp.ts +28 -24
- package/tools/wallet/createOrdinals.ts +4 -2
- package/tools/wallet/fetchPaymentUtxos.ts +44 -43
- package/tools/wallet/gatherCollectionInfo.ts +374 -0
- package/tools/wallet/getAddress.ts +13 -21
- package/tools/wallet/getBalance.ts +66 -0
- package/tools/wallet/getBalanceDroplet.ts +59 -0
- package/tools/wallet/integratedWallet.ts +120 -0
- package/tools/wallet/mintCollection.ts +378 -0
- package/tools/wallet/purchaseListing.ts +4 -4
- package/tools/wallet/schemas.ts +18 -249
- package/tools/wallet/sendOrdinals.ts +4 -2
- package/tools/wallet/sendToAddress.ts +5 -1
- package/tools/wallet/setupDroplet.ts +172 -0
- package/tools/wallet/tools.ts +71 -156
- package/tools/wallet/transactionHelper.ts +121 -0
- package/tools/wallet/transferOrdToken.ts +7 -4
- package/tools/wallet/utxo.ts +62 -0
- package/tools/wallet/wallet.ts +237 -234
- package/tools/a2b/call.ts +0 -32
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { P2PKH, Transaction, Utils } from "@bsv/sdk";
|
|
2
2
|
import type { Utxo } from "js-1sat-ord";
|
|
3
|
-
|
|
3
|
+
import { V5_API_URL } from "../constants";
|
|
4
|
+
import { getBeefTransactionById, getTransactionById } from "./utxo";
|
|
5
|
+
|
|
6
|
+
const { toBase64, toHex, toArray } = Utils;
|
|
4
7
|
|
|
5
8
|
/**
|
|
6
9
|
* Type definition for WhatsOnChain UTXO response
|
|
@@ -56,13 +59,10 @@ export async function fetchPaymentUtxos(
|
|
|
56
59
|
const utxos: (Utxo | null)[] = await Promise.all(
|
|
57
60
|
data.map(async (utxo: WhatsOnChainUtxo) => {
|
|
58
61
|
// Get the transaction hex to extract the correct script
|
|
59
|
-
const
|
|
60
|
-
|
|
61
|
-
utxo.tx_pos,
|
|
62
|
-
);
|
|
63
|
-
|
|
62
|
+
const tx = await getBeefTransactionById(utxo.tx_hash);
|
|
63
|
+
const script = tx?.outputs[utxo.tx_pos]?.lockingScript.toHex();
|
|
64
64
|
if (!script) {
|
|
65
|
-
console.
|
|
65
|
+
console.error(
|
|
66
66
|
`Could not get script for UTXO: ${utxo.tx_hash}:${utxo.tx_pos}`,
|
|
67
67
|
);
|
|
68
68
|
return null;
|
|
@@ -72,7 +72,7 @@ export async function fetchPaymentUtxos(
|
|
|
72
72
|
txid: utxo.tx_hash,
|
|
73
73
|
vout: utxo.tx_pos,
|
|
74
74
|
satoshis: utxo.value,
|
|
75
|
-
script
|
|
75
|
+
script,
|
|
76
76
|
};
|
|
77
77
|
}),
|
|
78
78
|
);
|
|
@@ -87,44 +87,45 @@ export async function fetchPaymentUtxos(
|
|
|
87
87
|
}
|
|
88
88
|
}
|
|
89
89
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
90
|
+
type V5Utxo = {
|
|
91
|
+
outpoint: string;
|
|
92
|
+
height: number;
|
|
93
|
+
idx: number;
|
|
94
|
+
satoshis: number;
|
|
95
|
+
script: string;
|
|
96
|
+
owners: string[];
|
|
97
|
+
data: Record<string, unknown>;
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
export async function fetchPaymentUtxosFromV5(
|
|
101
|
+
address: string,
|
|
102
|
+
): Promise<Utxo[] | undefined> {
|
|
101
103
|
try {
|
|
102
|
-
const
|
|
103
|
-
|
|
104
|
-
);
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
104
|
+
const url = `${V5_API_URL}/own/${address}/utxos?refresh=true&txo=true&script=true&limit=250&tags=p2pkh`;
|
|
105
|
+
console.error(`Fetching UTXOs from V5: ${url}`);
|
|
106
|
+
const response = await fetch(url);
|
|
107
|
+
const data = (await response.json()) as V5Utxo[];
|
|
108
|
+
const utxos: Utxo[] = [];
|
|
109
|
+
for (const utxo of data) {
|
|
110
|
+
if (!utxo.data.p2pkh) {
|
|
111
|
+
// Skip if not a P2PKH
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
const [txid, vout] = utxo.outpoint.split("_");
|
|
115
|
+
if (!txid || !vout) {
|
|
116
|
+
console.error(`Invalid outpoint: ${utxo.outpoint}`);
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
utxos.push({
|
|
120
|
+
txid,
|
|
121
|
+
vout: Number.parseInt(vout),
|
|
122
|
+
satoshis: utxo.satoshis,
|
|
123
|
+
script: toHex(toArray(utxo.script, "base64")),
|
|
124
|
+
} as Utxo);
|
|
120
125
|
}
|
|
121
|
-
|
|
122
|
-
return toBase64(output.lockingScript.toBinary());
|
|
126
|
+
return utxos;
|
|
123
127
|
} catch (error) {
|
|
124
|
-
console.error(
|
|
125
|
-
`Error getting script for transaction ${txid}:${vout}:`,
|
|
126
|
-
error,
|
|
127
|
-
);
|
|
128
|
+
console.error("Error fetching payment UTXOs from V5:", error);
|
|
128
129
|
return undefined;
|
|
129
130
|
}
|
|
130
131
|
}
|
|
@@ -0,0 +1,374 @@
|
|
|
1
|
+
import { promises as fs } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
4
|
+
import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
import type { Wallet } from "./wallet";
|
|
7
|
+
|
|
8
|
+
const gatherCollectionInfoArgsSchema = z.object({
|
|
9
|
+
folderPath: z
|
|
10
|
+
.string()
|
|
11
|
+
.describe("Path to folder containing images to analyze for collection"),
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
type GatherCollectionInfoArgs = z.infer<typeof gatherCollectionInfoArgsSchema>;
|
|
15
|
+
|
|
16
|
+
interface ImageInfo {
|
|
17
|
+
fileName: string;
|
|
18
|
+
filePath: string;
|
|
19
|
+
size: number;
|
|
20
|
+
sizeKB: number;
|
|
21
|
+
contentType: string;
|
|
22
|
+
valid: boolean;
|
|
23
|
+
error?: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
interface CollectionAnalysis {
|
|
27
|
+
folderPath: string;
|
|
28
|
+
totalImages: number;
|
|
29
|
+
validImages: number;
|
|
30
|
+
invalidImages: number;
|
|
31
|
+
totalSizeKB: number;
|
|
32
|
+
imageTypes: Record<string, number>;
|
|
33
|
+
images: ImageInfo[];
|
|
34
|
+
suggestedMetadata: {
|
|
35
|
+
collectionName: string;
|
|
36
|
+
description: string;
|
|
37
|
+
quantity: number;
|
|
38
|
+
};
|
|
39
|
+
costEstimate: {
|
|
40
|
+
collectionInscriptionCost: number;
|
|
41
|
+
perItemCost: number;
|
|
42
|
+
totalItemsCost: number;
|
|
43
|
+
totalCost: number;
|
|
44
|
+
totalCostBSV: number;
|
|
45
|
+
};
|
|
46
|
+
walletInfo: {
|
|
47
|
+
address: string;
|
|
48
|
+
balance: number;
|
|
49
|
+
balanceBSV: number;
|
|
50
|
+
hasEnoughFunds: boolean;
|
|
51
|
+
shortfall?: number;
|
|
52
|
+
};
|
|
53
|
+
warnings: string[];
|
|
54
|
+
errors: string[];
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async function analyzeImageFile(filePath: string): Promise<ImageInfo> {
|
|
58
|
+
const fileName = path.basename(filePath);
|
|
59
|
+
const ext = path.extname(fileName).toLowerCase();
|
|
60
|
+
|
|
61
|
+
const contentTypeMap: Record<string, string> = {
|
|
62
|
+
".jpg": "image/jpeg",
|
|
63
|
+
".jpeg": "image/jpeg",
|
|
64
|
+
".png": "image/png",
|
|
65
|
+
".gif": "image/gif",
|
|
66
|
+
".webp": "image/webp",
|
|
67
|
+
".svg": "image/svg+xml",
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
try {
|
|
71
|
+
const stat = await fs.stat(filePath);
|
|
72
|
+
const contentType = contentTypeMap[ext];
|
|
73
|
+
|
|
74
|
+
if (!contentType) {
|
|
75
|
+
return {
|
|
76
|
+
fileName,
|
|
77
|
+
filePath,
|
|
78
|
+
size: stat.size,
|
|
79
|
+
sizeKB: Math.round(stat.size / 1024),
|
|
80
|
+
contentType: "unknown",
|
|
81
|
+
valid: false,
|
|
82
|
+
error: `Unsupported file type: ${ext}`,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Check file size (warn if over 50KB)
|
|
87
|
+
const sizeKB = Math.round(stat.size / 1024);
|
|
88
|
+
const warnings: string[] = [];
|
|
89
|
+
|
|
90
|
+
if (sizeKB > 50) {
|
|
91
|
+
warnings.push(
|
|
92
|
+
`Large file size: ${sizeKB}KB (consider optimizing for lower fees)`,
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return {
|
|
97
|
+
fileName,
|
|
98
|
+
filePath,
|
|
99
|
+
size: stat.size,
|
|
100
|
+
sizeKB,
|
|
101
|
+
contentType,
|
|
102
|
+
valid: true,
|
|
103
|
+
error: warnings.length > 0 ? warnings.join("; ") : undefined,
|
|
104
|
+
};
|
|
105
|
+
} catch (error) {
|
|
106
|
+
return {
|
|
107
|
+
fileName,
|
|
108
|
+
filePath,
|
|
109
|
+
size: 0,
|
|
110
|
+
sizeKB: 0,
|
|
111
|
+
contentType: "unknown",
|
|
112
|
+
valid: false,
|
|
113
|
+
error: error instanceof Error ? error.message : "Unknown error",
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function estimateInscriptionCost(sizeKB: number): number {
|
|
119
|
+
// Base cost for inscription (1 sat)
|
|
120
|
+
const baseCost = 1;
|
|
121
|
+
// Additional cost based on size (rough estimate: 10 sats per KB)
|
|
122
|
+
const sizeCost = Math.ceil(sizeKB * 10);
|
|
123
|
+
// Mining fee estimate
|
|
124
|
+
const miningFee = 500;
|
|
125
|
+
|
|
126
|
+
return baseCost + sizeCost + miningFee;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export function registerGatherCollectionInfoTool(
|
|
130
|
+
server: McpServer,
|
|
131
|
+
wallet: Wallet,
|
|
132
|
+
) {
|
|
133
|
+
server.tool(
|
|
134
|
+
"wallet_gatherCollectionInfo",
|
|
135
|
+
"Analyzes a folder of images and gathers all necessary information for minting an ordinals collection. This includes validating images, checking wallet balance, estimating costs, and suggesting metadata. Use this before minting to ensure everything is ready.",
|
|
136
|
+
{ args: gatherCollectionInfoArgsSchema },
|
|
137
|
+
async ({
|
|
138
|
+
args,
|
|
139
|
+
}: {
|
|
140
|
+
args: GatherCollectionInfoArgs;
|
|
141
|
+
}): Promise<CallToolResult> => {
|
|
142
|
+
try {
|
|
143
|
+
const analysis: CollectionAnalysis = {
|
|
144
|
+
folderPath: args.folderPath,
|
|
145
|
+
totalImages: 0,
|
|
146
|
+
validImages: 0,
|
|
147
|
+
invalidImages: 0,
|
|
148
|
+
totalSizeKB: 0,
|
|
149
|
+
imageTypes: {},
|
|
150
|
+
images: [],
|
|
151
|
+
suggestedMetadata: {
|
|
152
|
+
collectionName: "",
|
|
153
|
+
description: "",
|
|
154
|
+
quantity: 0,
|
|
155
|
+
},
|
|
156
|
+
costEstimate: {
|
|
157
|
+
collectionInscriptionCost: 0,
|
|
158
|
+
perItemCost: 0,
|
|
159
|
+
totalItemsCost: 0,
|
|
160
|
+
totalCost: 0,
|
|
161
|
+
totalCostBSV: 0,
|
|
162
|
+
},
|
|
163
|
+
walletInfo: {
|
|
164
|
+
address: "",
|
|
165
|
+
balance: 0,
|
|
166
|
+
balanceBSV: 0,
|
|
167
|
+
hasEnoughFunds: false,
|
|
168
|
+
},
|
|
169
|
+
warnings: [],
|
|
170
|
+
errors: [],
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
// Check if folder exists
|
|
174
|
+
try {
|
|
175
|
+
const folderStat = await fs.stat(args.folderPath);
|
|
176
|
+
if (!folderStat.isDirectory()) {
|
|
177
|
+
throw new Error(`Path is not a directory: ${args.folderPath}`);
|
|
178
|
+
}
|
|
179
|
+
} catch (error) {
|
|
180
|
+
analysis.errors.push(
|
|
181
|
+
`Folder not found or inaccessible: ${args.folderPath}`,
|
|
182
|
+
);
|
|
183
|
+
return {
|
|
184
|
+
content: [
|
|
185
|
+
{
|
|
186
|
+
type: "text",
|
|
187
|
+
text: JSON.stringify(analysis, null, 2),
|
|
188
|
+
},
|
|
189
|
+
],
|
|
190
|
+
isError: true,
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Get wallet info
|
|
195
|
+
const paymentPk = wallet.getPrivateKey();
|
|
196
|
+
if (!paymentPk) {
|
|
197
|
+
analysis.errors.push("No payment key available in wallet");
|
|
198
|
+
return {
|
|
199
|
+
content: [
|
|
200
|
+
{
|
|
201
|
+
type: "text",
|
|
202
|
+
text: JSON.stringify(analysis, null, 2),
|
|
203
|
+
},
|
|
204
|
+
],
|
|
205
|
+
isError: true,
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
analysis.walletInfo.address = paymentPk.toAddress().toString();
|
|
210
|
+
|
|
211
|
+
// Get wallet balance
|
|
212
|
+
try {
|
|
213
|
+
const { paymentUtxos } = await wallet.getUtxos();
|
|
214
|
+
const balance = paymentUtxos.reduce(
|
|
215
|
+
(sum, utxo) => sum + utxo.satoshis,
|
|
216
|
+
0,
|
|
217
|
+
);
|
|
218
|
+
analysis.walletInfo.balance = balance;
|
|
219
|
+
analysis.walletInfo.balanceBSV = balance / 100000000;
|
|
220
|
+
} catch (error) {
|
|
221
|
+
analysis.warnings.push("Could not fetch wallet balance");
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Scan folder for images
|
|
225
|
+
const files = await fs.readdir(args.folderPath);
|
|
226
|
+
const imageExtensions = [
|
|
227
|
+
".jpg",
|
|
228
|
+
".jpeg",
|
|
229
|
+
".png",
|
|
230
|
+
".gif",
|
|
231
|
+
".webp",
|
|
232
|
+
".svg",
|
|
233
|
+
];
|
|
234
|
+
|
|
235
|
+
for (const file of files) {
|
|
236
|
+
const filePath = path.join(args.folderPath, file);
|
|
237
|
+
const stat = await fs.stat(filePath);
|
|
238
|
+
|
|
239
|
+
if (stat.isFile()) {
|
|
240
|
+
const ext = path.extname(file).toLowerCase();
|
|
241
|
+
if (imageExtensions.includes(ext)) {
|
|
242
|
+
const imageInfo = await analyzeImageFile(filePath);
|
|
243
|
+
analysis.images.push(imageInfo);
|
|
244
|
+
analysis.totalImages++;
|
|
245
|
+
|
|
246
|
+
if (imageInfo.valid) {
|
|
247
|
+
analysis.validImages++;
|
|
248
|
+
analysis.totalSizeKB += imageInfo.sizeKB;
|
|
249
|
+
|
|
250
|
+
// Track image types
|
|
251
|
+
const type = imageInfo.contentType.split("/")[1] || "unknown";
|
|
252
|
+
analysis.imageTypes[type] =
|
|
253
|
+
(analysis.imageTypes[type] || 0) + 1;
|
|
254
|
+
} else {
|
|
255
|
+
analysis.invalidImages++;
|
|
256
|
+
if (imageInfo.error) {
|
|
257
|
+
analysis.errors.push(
|
|
258
|
+
`${imageInfo.fileName}: ${imageInfo.error}`,
|
|
259
|
+
);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// Sort images by name for consistent ordering
|
|
267
|
+
analysis.images.sort((a, b) => a.fileName.localeCompare(b.fileName));
|
|
268
|
+
|
|
269
|
+
if (analysis.validImages === 0) {
|
|
270
|
+
analysis.errors.push("No valid images found in the folder");
|
|
271
|
+
return {
|
|
272
|
+
content: [
|
|
273
|
+
{
|
|
274
|
+
type: "text",
|
|
275
|
+
text: JSON.stringify(analysis, null, 2),
|
|
276
|
+
},
|
|
277
|
+
],
|
|
278
|
+
isError: true,
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// Suggest metadata based on folder name
|
|
283
|
+
const folderName = path.basename(args.folderPath);
|
|
284
|
+
analysis.suggestedMetadata = {
|
|
285
|
+
collectionName: folderName
|
|
286
|
+
.replace(/[-_]/g, " ")
|
|
287
|
+
.replace(/\b\w/g, (l) => l.toUpperCase()),
|
|
288
|
+
description: `A collection of ${analysis.validImages} unique digital artifacts`,
|
|
289
|
+
quantity: analysis.validImages,
|
|
290
|
+
};
|
|
291
|
+
|
|
292
|
+
// Estimate costs
|
|
293
|
+
const collectionIconSizeKB = 1; // SVG icon is small
|
|
294
|
+
analysis.costEstimate.collectionInscriptionCost =
|
|
295
|
+
estimateInscriptionCost(collectionIconSizeKB);
|
|
296
|
+
|
|
297
|
+
// Average cost per item
|
|
298
|
+
const avgItemSizeKB = analysis.totalSizeKB / analysis.validImages;
|
|
299
|
+
analysis.costEstimate.perItemCost =
|
|
300
|
+
estimateInscriptionCost(avgItemSizeKB);
|
|
301
|
+
analysis.costEstimate.totalItemsCost =
|
|
302
|
+
analysis.costEstimate.perItemCost * analysis.validImages;
|
|
303
|
+
analysis.costEstimate.totalCost =
|
|
304
|
+
analysis.costEstimate.collectionInscriptionCost +
|
|
305
|
+
analysis.costEstimate.totalItemsCost;
|
|
306
|
+
analysis.costEstimate.totalCostBSV =
|
|
307
|
+
analysis.costEstimate.totalCost / 100000000;
|
|
308
|
+
|
|
309
|
+
// Check if wallet has enough funds
|
|
310
|
+
analysis.walletInfo.hasEnoughFunds =
|
|
311
|
+
analysis.walletInfo.balance >= analysis.costEstimate.totalCost;
|
|
312
|
+
if (!analysis.walletInfo.hasEnoughFunds) {
|
|
313
|
+
analysis.walletInfo.shortfall =
|
|
314
|
+
analysis.costEstimate.totalCost - analysis.walletInfo.balance;
|
|
315
|
+
analysis.warnings.push(
|
|
316
|
+
`Insufficient funds: need ${analysis.costEstimate.totalCost} sats, have ${analysis.walletInfo.balance} sats (shortfall: ${analysis.walletInfo.shortfall} sats)`,
|
|
317
|
+
);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// Add warnings for large collections
|
|
321
|
+
if (analysis.validImages > 100) {
|
|
322
|
+
analysis.warnings.push(
|
|
323
|
+
`Large collection (${analysis.validImages} items) will take time and multiple transactions to mint`,
|
|
324
|
+
);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
if (analysis.totalSizeKB > 1000) {
|
|
328
|
+
analysis.warnings.push(
|
|
329
|
+
`Large total size (${Math.round(analysis.totalSizeKB / 1024)}MB) will result in higher fees`,
|
|
330
|
+
);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// Generate summary
|
|
334
|
+
const summary = {
|
|
335
|
+
ready:
|
|
336
|
+
analysis.errors.length === 0 && analysis.walletInfo.hasEnoughFunds,
|
|
337
|
+
summary: `Found ${analysis.validImages} valid images in "${folderName}". Total size: ${Math.round(analysis.totalSizeKB)}KB. Estimated cost: ${analysis.costEstimate.totalCost} sats (${analysis.costEstimate.totalCostBSV.toFixed(8)} BSV). Wallet balance: ${analysis.walletInfo.balance} sats. ${
|
|
338
|
+
analysis.walletInfo.hasEnoughFunds
|
|
339
|
+
? "Ready to mint!"
|
|
340
|
+
: "Insufficient funds."
|
|
341
|
+
}`,
|
|
342
|
+
};
|
|
343
|
+
|
|
344
|
+
return {
|
|
345
|
+
content: [
|
|
346
|
+
{
|
|
347
|
+
type: "text",
|
|
348
|
+
text: JSON.stringify(
|
|
349
|
+
{
|
|
350
|
+
...analysis,
|
|
351
|
+
...summary,
|
|
352
|
+
},
|
|
353
|
+
null,
|
|
354
|
+
2,
|
|
355
|
+
),
|
|
356
|
+
},
|
|
357
|
+
],
|
|
358
|
+
};
|
|
359
|
+
} catch (error) {
|
|
360
|
+
const errorMessage =
|
|
361
|
+
error instanceof Error ? error.message : String(error);
|
|
362
|
+
return {
|
|
363
|
+
content: [
|
|
364
|
+
{
|
|
365
|
+
type: "text",
|
|
366
|
+
text: `Error analyzing collection: ${errorMessage}`,
|
|
367
|
+
},
|
|
368
|
+
],
|
|
369
|
+
isError: true,
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
},
|
|
373
|
+
);
|
|
374
|
+
}
|
|
@@ -1,12 +1,16 @@
|
|
|
1
|
-
import { PrivateKey } from "@bsv/sdk";
|
|
2
1
|
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
2
|
import { z } from "zod";
|
|
3
|
+
import type { Wallet } from "./wallet";
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* Register the tool to get the wallet address
|
|
7
7
|
* @param server The MCP server instance
|
|
8
|
+
* @param wallet The initialized custom Wallet instance
|
|
8
9
|
*/
|
|
9
|
-
export function registerGetAddressTool(
|
|
10
|
+
export function registerGetAddressTool(
|
|
11
|
+
server: McpServer,
|
|
12
|
+
wallet: Wallet,
|
|
13
|
+
): void {
|
|
10
14
|
server.tool(
|
|
11
15
|
"wallet_getAddress",
|
|
12
16
|
"Retrieves the current wallet's Bitcoin SV address. This address can be used to receive BSV, ordinals, or tokens, and is derived from the wallet's private key.",
|
|
@@ -20,26 +24,14 @@ export function registerGetAddressTool(server: McpServer): void {
|
|
|
20
24
|
},
|
|
21
25
|
async () => {
|
|
22
26
|
try {
|
|
23
|
-
const
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
text: JSON.stringify({
|
|
30
|
-
error: "No private key available",
|
|
31
|
-
message:
|
|
32
|
-
"Please set PRIVATE_KEY_WIF environment variable with a valid Bitcoin SV private key in WIF format.",
|
|
33
|
-
status: "error",
|
|
34
|
-
}),
|
|
35
|
-
},
|
|
36
|
-
],
|
|
37
|
-
isError: true,
|
|
38
|
-
};
|
|
27
|
+
const address = wallet.getAddress();
|
|
28
|
+
|
|
29
|
+
if (!address) {
|
|
30
|
+
throw new Error(
|
|
31
|
+
"Could not retrieve address from wallet instance. Key might be missing or getAddress method failed.",
|
|
32
|
+
);
|
|
39
33
|
}
|
|
40
34
|
|
|
41
|
-
const privKey = PrivateKey.fromWif(wif);
|
|
42
|
-
const address = privKey.toAddress();
|
|
43
35
|
return {
|
|
44
36
|
content: [
|
|
45
37
|
{
|
|
@@ -55,7 +47,7 @@ export function registerGetAddressTool(server: McpServer): void {
|
|
|
55
47
|
{
|
|
56
48
|
type: "text",
|
|
57
49
|
text: JSON.stringify({
|
|
58
|
-
error: "
|
|
50
|
+
error: "Failed to get wallet address",
|
|
59
51
|
message: msg,
|
|
60
52
|
status: "error",
|
|
61
53
|
}),
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import type { RequestHandlerExtra } from "@modelcontextprotocol/sdk/shared/protocol.js";
|
|
3
|
+
import type {
|
|
4
|
+
ServerNotification,
|
|
5
|
+
ServerRequest,
|
|
6
|
+
} from "@modelcontextprotocol/sdk/types.js";
|
|
7
|
+
import { toBitcoin } from "satoshi-token"; // For converting satoshis to BSV string
|
|
8
|
+
import { z } from "zod";
|
|
9
|
+
import type { Wallet } from "./wallet";
|
|
10
|
+
|
|
11
|
+
export const walletGetBalanceArgsSchema = z.object({}).optional();
|
|
12
|
+
export type WalletGetBalanceArgs = z.infer<typeof walletGetBalanceArgsSchema>;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Registers the wallet_getBalance tool
|
|
16
|
+
*/
|
|
17
|
+
export function registerWalletGetBalanceTool(
|
|
18
|
+
server: McpServer,
|
|
19
|
+
wallet: Wallet,
|
|
20
|
+
) {
|
|
21
|
+
server.tool(
|
|
22
|
+
"wallet_getBalance",
|
|
23
|
+
"Retrieves the current BSV balance for the wallet based on its UTXOs.",
|
|
24
|
+
{ args: walletGetBalanceArgsSchema },
|
|
25
|
+
async (
|
|
26
|
+
args: WalletGetBalanceArgs, // Args are empty but present for schema consistency
|
|
27
|
+
extra: RequestHandlerExtra<ServerRequest, ServerNotification>,
|
|
28
|
+
) => {
|
|
29
|
+
try {
|
|
30
|
+
const { paymentUtxos } = await wallet.getUtxos();
|
|
31
|
+
|
|
32
|
+
let totalSatoshis = 0;
|
|
33
|
+
if (paymentUtxos && paymentUtxos.length > 0) {
|
|
34
|
+
for (const utxo of paymentUtxos) {
|
|
35
|
+
totalSatoshis += utxo.satoshis || 0;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const bsvAmount = toBitcoin(totalSatoshis); // Convert to BSV string
|
|
40
|
+
|
|
41
|
+
return {
|
|
42
|
+
content: [
|
|
43
|
+
{
|
|
44
|
+
type: "text",
|
|
45
|
+
text: JSON.stringify({
|
|
46
|
+
satoshis: totalSatoshis,
|
|
47
|
+
bsv: bsvAmount,
|
|
48
|
+
utxoCount: paymentUtxos ? paymentUtxos.length : 0,
|
|
49
|
+
}),
|
|
50
|
+
},
|
|
51
|
+
],
|
|
52
|
+
};
|
|
53
|
+
} catch (error) {
|
|
54
|
+
return {
|
|
55
|
+
content: [
|
|
56
|
+
{
|
|
57
|
+
type: "text",
|
|
58
|
+
text: error instanceof Error ? error.message : String(error),
|
|
59
|
+
},
|
|
60
|
+
],
|
|
61
|
+
isError: true,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
},
|
|
65
|
+
);
|
|
66
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import type { RequestHandlerExtra } from "@modelcontextprotocol/sdk/shared/protocol.js";
|
|
3
|
+
import type {
|
|
4
|
+
ServerNotification,
|
|
5
|
+
ServerRequest,
|
|
6
|
+
} from "@modelcontextprotocol/sdk/types.js";
|
|
7
|
+
import type { DropletClient } from "../../utils/droplet";
|
|
8
|
+
import { emptyArgsSchema } from "./schemas";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Register the getBalance tool for Droplet API mode
|
|
12
|
+
*/
|
|
13
|
+
export function registerWalletGetBalanceDropletTool(
|
|
14
|
+
server: McpServer,
|
|
15
|
+
dropletClient: DropletClient,
|
|
16
|
+
) {
|
|
17
|
+
server.tool(
|
|
18
|
+
"wallet_getBalance",
|
|
19
|
+
"Gets the current balance of the wallet (Droplet mode)",
|
|
20
|
+
{
|
|
21
|
+
args: emptyArgsSchema,
|
|
22
|
+
},
|
|
23
|
+
async (
|
|
24
|
+
{ args }: { args: unknown },
|
|
25
|
+
extra: RequestHandlerExtra<ServerRequest, ServerNotification>,
|
|
26
|
+
) => {
|
|
27
|
+
try {
|
|
28
|
+
const status = await dropletClient.getFaucetStatus();
|
|
29
|
+
|
|
30
|
+
return {
|
|
31
|
+
content: [
|
|
32
|
+
{
|
|
33
|
+
type: "text",
|
|
34
|
+
text: JSON.stringify({
|
|
35
|
+
balance: status.balance_satoshis,
|
|
36
|
+
unspentUtxoCount: status.unspent_utxo_count,
|
|
37
|
+
spendableUtxoCount: status.spendable_utxo_count,
|
|
38
|
+
consolidatingBalance: status.consolidating_balance_satoshis,
|
|
39
|
+
consolidatingUtxoCount: status.consolidating_utxo_count,
|
|
40
|
+
fixedDropSats: status.fixed_drop_sats,
|
|
41
|
+
faucetName: status.faucet_name,
|
|
42
|
+
}),
|
|
43
|
+
},
|
|
44
|
+
],
|
|
45
|
+
};
|
|
46
|
+
} catch (error) {
|
|
47
|
+
return {
|
|
48
|
+
content: [
|
|
49
|
+
{
|
|
50
|
+
type: "text",
|
|
51
|
+
text: error instanceof Error ? error.message : String(error),
|
|
52
|
+
},
|
|
53
|
+
],
|
|
54
|
+
isError: true,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
},
|
|
58
|
+
);
|
|
59
|
+
}
|