bsv-mcp 0.0.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.
@@ -0,0 +1,175 @@
1
+ import { PrivateKey } from "@bsv/sdk";
2
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
+ import type { RequestHandlerExtra } from "@modelcontextprotocol/sdk/shared/protocol.js";
4
+ import {
5
+ type ExistingListing,
6
+ type Utxo,
7
+ oneSatBroadcaster,
8
+ purchaseOrdListing,
9
+ } from "js-1sat-ord";
10
+ import type { z } from "zod";
11
+ import { purchaseListingArgsSchema } from "./schemas";
12
+ import type { Wallet } from "./wallet";
13
+
14
+ // Define types for 1Sat API response
15
+ interface ListingResponse {
16
+ txid: string;
17
+ vout: number;
18
+ satoshis: number;
19
+ script: string;
20
+ data?: {
21
+ list?: {
22
+ price: number;
23
+ payout: string;
24
+ };
25
+ };
26
+ }
27
+
28
+ /**
29
+ * Register the purchaseListing tool
30
+ *
31
+ * This tool:
32
+ * 1. Parses the listing outpoint to get the txid and vout
33
+ * 2. Fetches the listing UTXO from the ordinals API
34
+ * 3. Gets the wallet's payment UTXOs (using the wallet's internal UTXO management)
35
+ * 4. Uses purchaseOrdListing to create a purchase transaction
36
+ * 5. Broadcasts the transaction
37
+ * 6. Returns the transaction details
38
+ */
39
+ export function registerPurchaseListingTool(server: McpServer, wallet: Wallet) {
40
+ // Store a reference to check if wallet is persistent
41
+ console.log("Registering purchaseListing tool with wallet:", wallet);
42
+
43
+ server.tool(
44
+ "wallet_purchaseListing",
45
+ { args: purchaseListingArgsSchema },
46
+ async (
47
+ { args }: { args: z.infer<typeof purchaseListingArgsSchema> },
48
+ extra: RequestHandlerExtra,
49
+ ) => {
50
+ try {
51
+ console.log(`Attempting to purchase listing: ${args.listingOutpoint}`);
52
+ console.log("Using wallet instance:", wallet);
53
+ console.log("Wallet has UTXOs:", await wallet.getUtxos());
54
+
55
+ // Fetch the listing info directly from the API
56
+ const response = await fetch(
57
+ `https://ordinals.gorillapool.io/api/txos/${args.listingOutpoint}?script=true`,
58
+ );
59
+ if (!response.ok) {
60
+ throw new Error(
61
+ `Failed to fetch listing data: ${response.statusText}`,
62
+ );
63
+ }
64
+
65
+ const listingData = (await response.json()) as ListingResponse;
66
+
67
+ // Check if the listing is valid and has a price
68
+ if (!listingData.data?.list?.price) {
69
+ throw new Error("Listing is either not for sale or invalid");
70
+ }
71
+
72
+ // Check if payout is available
73
+ if (!listingData.data.list.payout) {
74
+ throw new Error("Listing doesn't have payout information");
75
+ }
76
+
77
+ // Parse the listing outpoint to get txid and vout
78
+ const [txid, voutStr] = args.listingOutpoint.split("_");
79
+ if (!txid) {
80
+ throw new Error("Invalid outpoint format. Expected txid_vout");
81
+ }
82
+ const vout = Number.parseInt(voutStr || "0", 10);
83
+
84
+ // Create listing UTXO object in the format required by js-1sat-ord
85
+ const listingUtxo: Utxo = {
86
+ txid,
87
+ vout,
88
+ script: listingData.script,
89
+ satoshis: listingData.satoshis,
90
+ };
91
+
92
+ // Create the ExistingListing object
93
+ const listing: ExistingListing = {
94
+ payout: listingData.data.list.payout,
95
+ listingUtxo,
96
+ };
97
+
98
+ // Get private key from the wallet
99
+ const paymentPk = wallet.getPrivateKey();
100
+ if (!paymentPk) {
101
+ throw new Error("No private key available in wallet");
102
+ }
103
+
104
+ // Get payment address
105
+ const paymentAddress = paymentPk.toAddress().toString();
106
+ console.log(`Using payment address: ${paymentAddress}`);
107
+
108
+ // Get payment UTXOs from the wallet's managed UTXOs
109
+ const { paymentUtxos } = await wallet.getUtxos();
110
+ if (!paymentUtxos || paymentUtxos.length === 0) {
111
+ // Provide more helpful error message with instructions
112
+ throw new Error(
113
+ `No payment UTXOs available for address ${paymentAddress}.
114
+ Please fund this wallet address with enough BSV to cover the purchase price
115
+ (${listingData.data.list.price} satoshis) plus transaction fees.`,
116
+ );
117
+ }
118
+
119
+ // Create the purchase transaction using the library's config type
120
+ const transaction = await purchaseOrdListing({
121
+ utxos: paymentUtxos,
122
+ paymentPk,
123
+ ordAddress: args.ordAddress,
124
+ listing,
125
+ });
126
+
127
+ // After successful transaction creation, refresh the wallet's UTXOs
128
+ // This ensures the wallet doesn't try to reuse spent UTXOs
129
+ try {
130
+ await wallet.refreshUtxos();
131
+ } catch (refreshError) {
132
+ console.warn(
133
+ "Failed to refresh UTXOs after transaction:",
134
+ refreshError,
135
+ );
136
+ }
137
+
138
+ // Broadcast the transaction
139
+ const broadcastResult = await transaction.tx.broadcast(
140
+ oneSatBroadcaster(),
141
+ );
142
+
143
+ // Handle broadcast response
144
+ const resultStatus =
145
+ typeof broadcastResult === "object" && "status" in broadcastResult
146
+ ? broadcastResult.status
147
+ : "unknown";
148
+
149
+ const resultMessage =
150
+ typeof broadcastResult === "object" && "error" in broadcastResult
151
+ ? broadcastResult.error
152
+ : "Transaction broadcast successful";
153
+
154
+ return {
155
+ content: [
156
+ {
157
+ type: "text",
158
+ text: JSON.stringify({
159
+ status: resultStatus,
160
+ message: resultMessage,
161
+ txid: transaction.tx.id("hex"),
162
+ listingOutpoint: args.listingOutpoint,
163
+ destinationAddress: args.ordAddress,
164
+ price: listingData.data.list.price,
165
+ }),
166
+ },
167
+ ],
168
+ };
169
+ } catch (err: unknown) {
170
+ const msg = err instanceof Error ? err.message : String(err);
171
+ return { content: [{ type: "text", text: msg }], isError: true };
172
+ }
173
+ },
174
+ );
175
+ }
@@ -0,0 +1,284 @@
1
+ import type { SecurityLevel } from "@bsv/sdk";
2
+ import { z } from "zod";
3
+
4
+ // Define SecurityLevel to match the BSV SDK exactly
5
+ const SecurityLevelEnum = z.union([z.literal(0), z.literal(1), z.literal(2)]);
6
+
7
+ // Create a custom validator without tuples
8
+ export const walletProtocolSchema = z.custom<[SecurityLevel, string]>((val) => {
9
+ return (
10
+ Array.isArray(val) &&
11
+ val.length === 2 &&
12
+ (val[0] === 0 || val[0] === 1 || val[0] === 2) &&
13
+ typeof val[1] === "string"
14
+ );
15
+ });
16
+
17
+ // Empty args schema for functions that don't take arguments
18
+ export const emptyArgsSchema = z.object({});
19
+
20
+ // Get public key arguments
21
+ export const getPublicKeyArgsSchema = z.object({});
22
+
23
+ // Create signature arguments
24
+ export const createSignatureArgsSchema = z.object({
25
+ data: z.array(z.number()).optional(),
26
+ hashToDirectlySign: z.array(z.number()).optional(),
27
+ protocolID: walletProtocolSchema,
28
+ keyID: z.string(),
29
+ privilegedReason: z.string().optional(),
30
+ counterparty: z
31
+ .union([z.string(), z.literal("self"), z.literal("anyone")])
32
+ .optional(),
33
+ privileged: z.boolean().optional(),
34
+ });
35
+
36
+ // Verify signature arguments
37
+ export const verifySignatureArgsSchema = z.object({
38
+ data: z.array(z.number()).optional(),
39
+ hashToDirectlyVerify: z.array(z.number()).optional(),
40
+ signature: z.array(z.number()),
41
+ protocolID: walletProtocolSchema,
42
+ keyID: z.string(),
43
+ privilegedReason: z.string().optional(),
44
+ counterparty: z
45
+ .union([z.string(), z.literal("self"), z.literal("anyone")])
46
+ .optional(),
47
+ forSelf: z.boolean().optional(),
48
+ privileged: z.boolean().optional(),
49
+ });
50
+
51
+ // Wallet encryption args
52
+ export const walletEncryptArgsSchema = z.object({
53
+ plaintext: z.array(z.number()),
54
+ protocolID: walletProtocolSchema,
55
+ keyID: z.string(),
56
+ privilegedReason: z.string().optional(),
57
+ counterparty: z
58
+ .union([z.string(), z.literal("self"), z.literal("anyone")])
59
+ .optional(),
60
+ privileged: z.boolean().optional(),
61
+ });
62
+
63
+ // Wallet decryption args
64
+ export const walletDecryptArgsSchema = z.object({
65
+ ciphertext: z.array(z.number()),
66
+ protocolID: walletProtocolSchema,
67
+ keyID: z.string(),
68
+ privilegedReason: z.string().optional(),
69
+ counterparty: z
70
+ .union([z.string(), z.literal("self"), z.literal("anyone")])
71
+ .optional(),
72
+ privileged: z.boolean().optional(),
73
+ });
74
+
75
+ // Create HMAC arguments
76
+ export const createHmacArgsSchema = z.object({
77
+ message: z.string(),
78
+ encoding: z.enum(["utf8", "hex", "base64"]).optional(),
79
+ });
80
+
81
+ // Verify HMAC arguments
82
+ export const verifyHmacArgsSchema = z.object({
83
+ message: z.string(),
84
+ hmac: z.string(),
85
+ publicKey: z.string(),
86
+ encoding: z.enum(["utf8", "hex", "base64"]).optional(),
87
+ });
88
+
89
+ // Transaction input schema
90
+ export const transactionInputSchema = z.object({
91
+ outpoint: z.string(),
92
+ inputDescription: z.string(), // Required field
93
+ sequence: z.number().optional(),
94
+ });
95
+
96
+ // Transaction output schema
97
+ export const transactionOutputSchema = z.object({
98
+ lockingScript: z.string(),
99
+ satoshis: z.number(),
100
+ outputDescription: z.string(), // Required field
101
+ change: z.boolean().optional(),
102
+ });
103
+
104
+ // Create action args
105
+ export const createActionArgsSchema = z.object({
106
+ description: z.string(),
107
+ labels: z.array(z.string()).optional(),
108
+ lockTime: z.number().optional(),
109
+ version: z.number().optional(),
110
+ inputBEEF: z.array(z.number()).optional(),
111
+ inputs: z.array(transactionInputSchema).optional(),
112
+ outputs: z.array(transactionOutputSchema).optional(),
113
+ options: z.record(z.union([z.string(), z.number(), z.boolean()])).optional(),
114
+ });
115
+
116
+ // Sign action args
117
+ export const signActionArgsSchema = z.object({
118
+ reference: z.string(),
119
+ spends: z.record(z.any()).optional(),
120
+ options: z.record(z.union([z.string(), z.number(), z.boolean()])).optional(),
121
+ });
122
+
123
+ // List actions args
124
+ export const listActionsArgsSchema = z.object({
125
+ labels: z.array(z.string()),
126
+ labelQueryMode: z.enum(["any", "all"]).optional(),
127
+ limit: z.number().optional(),
128
+ offset: z.number().optional(),
129
+ includeInputs: z.boolean().optional(),
130
+ includeOutputs: z.boolean().optional(),
131
+ includeLabels: z.boolean().optional(),
132
+ includeInputSourceLockingScripts: z.boolean().optional(),
133
+ includeInputUnlockingScripts: z.boolean().optional(),
134
+ seekPermission: z.boolean().optional(),
135
+ });
136
+
137
+ // List outputs args
138
+ export const listOutputsArgsSchema = z.object({
139
+ basket: z.string(),
140
+ tags: z.array(z.string()).optional(),
141
+ tagQueryMode: z.enum(["all", "any"]).optional(),
142
+ limit: z.number().optional(),
143
+ offset: z.number().optional(),
144
+ include: z.enum(["locking scripts", "entire transactions"]).optional(),
145
+ includeLabels: z.boolean().optional(),
146
+ includeTags: z.boolean().optional(),
147
+ includeCustomInstructions: z.boolean().optional(),
148
+ seekPermission: z.boolean().optional(),
149
+ });
150
+
151
+ // Reveal counterparty key linkage args
152
+ export const revealCounterpartyKeyLinkageArgsSchema = z.object({
153
+ counterparty: z.string(),
154
+ verifier: z.string(),
155
+ privileged: z.boolean().optional(),
156
+ privilegedReason: z.string().optional(),
157
+ });
158
+
159
+ // Reveal specific key linkage args
160
+ export const revealSpecificKeyLinkageArgsSchema = z.object({
161
+ keyID: z.number(),
162
+ verifier: z.string(),
163
+ privileged: z.boolean().optional(),
164
+ privilegedReason: z.string().optional(),
165
+ });
166
+
167
+ // Abort action args
168
+ export const abortActionArgsSchema = z.object({
169
+ reference: z.string(),
170
+ });
171
+
172
+ // Internalize action args
173
+ export const internalizeActionArgsSchema = z.object({
174
+ tx: z.array(z.number()),
175
+ outputs: z.array(
176
+ z.object({
177
+ outputIndex: z.number(),
178
+ protocol: z.enum(["wallet payment", "basket insertion"]),
179
+ lockingScript: z.string(),
180
+ satoshis: z.number(),
181
+ }),
182
+ ),
183
+ description: z.string(),
184
+ labels: z.array(z.string()).optional(),
185
+ seekPermission: z.boolean().optional(),
186
+ });
187
+
188
+ // Relinquish output args
189
+ export const relinquishOutputArgsSchema = z.object({
190
+ basket: z.string(),
191
+ output: z.string(),
192
+ });
193
+
194
+ // Acquire certificate args
195
+ export const acquireCertificateArgsSchema = z.object({
196
+ type: z.string(),
197
+ certifier: z.string(),
198
+ acquisitionProtocol: z.enum(["direct", "issuance"]),
199
+ fields: z.record(z.string()),
200
+ certifierUrl: z.string().optional(),
201
+ serialNumber: z.string().optional(),
202
+ signature: z.string().optional(),
203
+ revocationOutpoint: z.string().optional(),
204
+ keyringForSubject: z.record(z.string()).optional(),
205
+ keyringRevealer: z.string().optional(),
206
+ privileged: z.boolean().optional(),
207
+ privilegedReason: z.string().optional(),
208
+ });
209
+
210
+ // List certificates args
211
+ export const listCertificatesArgsSchema = z.object({
212
+ certifiers: z.array(z.string()),
213
+ types: z.array(z.string()),
214
+ limit: z.number().optional(),
215
+ offset: z.number().optional(),
216
+ privileged: z.boolean().optional(),
217
+ privilegedReason: z.string().optional(),
218
+ });
219
+
220
+ // Prove certificate args
221
+ export const proveCertificateArgsSchema = z.object({
222
+ certificate: z.object({}),
223
+ fieldsToReveal: z.array(z.string()),
224
+ verifier: z.string(),
225
+ privileged: z.boolean().optional(),
226
+ privilegedReason: z.string().optional(),
227
+ });
228
+
229
+ // Relinquish certificate args
230
+ export const relinquishCertificateArgsSchema = z.object({
231
+ type: z.string(),
232
+ serialNumber: z.string(),
233
+ certifier: z.string(),
234
+ });
235
+
236
+ // Discover by identity key args
237
+ export const discoverByIdentityKeyArgsSchema = z.object({
238
+ identityKey: z.string(),
239
+ limit: z.number().optional(),
240
+ offset: z.number().optional(),
241
+ seekPermission: z.boolean().optional(),
242
+ });
243
+
244
+ // Discover by attributes args
245
+ export const discoverByAttributesArgsSchema = z.object({
246
+ attributes: z.record(z.string()),
247
+ limit: z.number().optional(),
248
+ offset: z.number().optional(),
249
+ seekPermission: z.boolean().optional(),
250
+ });
251
+
252
+ // Get header for height args
253
+ export const getHeaderArgsSchema = z.object({
254
+ height: z.number(),
255
+ });
256
+
257
+ // Get address args
258
+ export const getAddressArgsSchema = z.object({});
259
+
260
+ // Send to address args
261
+ export const sendToAddressArgsSchema = z.object({
262
+ address: z.string(),
263
+ amount: z.number(),
264
+ currency: z.enum(["BSV", "USD"]).optional(),
265
+ description: z.string().optional(),
266
+ });
267
+
268
+ /**
269
+ * Schema for purchase listing arguments
270
+ */
271
+ export const purchaseListingArgsSchema = z.object({
272
+ listingOutpoint: z
273
+ .string()
274
+ .describe("The outpoint of the listing to purchase (txid_vout format)"),
275
+ ordAddress: z
276
+ .string()
277
+ .describe("The ordinal address to receive the purchased item"),
278
+ description: z
279
+ .string()
280
+ .optional()
281
+ .describe("Optional description for the transaction"),
282
+ });
283
+
284
+ export type PurchaseListingArgs = z.infer<typeof purchaseListingArgsSchema>;
@@ -0,0 +1,112 @@
1
+ import { P2PKH } from "@bsv/sdk";
2
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
+ import type { RequestHandlerExtra } from "@modelcontextprotocol/sdk/shared/protocol.js";
4
+ import type { z } from "zod";
5
+ import { sendToAddressArgsSchema } from "./schemas";
6
+ import type { Wallet } from "./wallet";
7
+
8
+ /**
9
+ * Fetch the current BSV price from whatsonchain API
10
+ * @returns The BSV price in USD
11
+ */
12
+ async function getBsvPrice(): Promise<number> {
13
+ try {
14
+ const res = await fetch(
15
+ "https://api.whatsonchain.com/v1/bsv/main/exchangerate",
16
+ );
17
+ if (!res.ok) throw new Error("Failed to fetch BSV price");
18
+
19
+ // Parse the response with proper type casting
20
+ const data = (await res.json()) as {
21
+ rate: string;
22
+ currency: string;
23
+ time: number;
24
+ };
25
+ const price = Number(data.rate);
26
+
27
+ if (Number.isNaN(price) || price <= 0) throw new Error("Invalid BSV price");
28
+ return price;
29
+ } catch (error) {
30
+ console.error("BSV price fetch error:", error);
31
+ throw error;
32
+ }
33
+ }
34
+
35
+ // Use the schema imported from schemas.ts
36
+ export type SendToAddressArgs = z.infer<typeof sendToAddressArgsSchema>;
37
+
38
+ /**
39
+ * Register the sendToAddress tool
40
+ */
41
+ export function registerSendToAddressTool(server: McpServer, wallet: Wallet) {
42
+ server.tool(
43
+ "wallet_sendToAddress",
44
+ {
45
+ args: sendToAddressArgsSchema,
46
+ },
47
+ async (
48
+ { args }: { args: SendToAddressArgs },
49
+ extra: RequestHandlerExtra,
50
+ ) => {
51
+ try {
52
+ const {
53
+ address,
54
+ amount,
55
+ currency = "BSV",
56
+ description = "Send to address",
57
+ } = args;
58
+
59
+ // Convert USD to satoshis if needed
60
+ let satoshis = amount;
61
+ if (currency === "USD") {
62
+ // Get current BSV price
63
+ const bsvPriceUsd = await getBsvPrice();
64
+
65
+ // Convert USD to BSV, then to satoshis
66
+ satoshis = Math.floor((amount / bsvPriceUsd) * 100000000);
67
+ } else {
68
+ // Convert BSV to satoshis
69
+ satoshis = Math.floor(amount * 100000000);
70
+ }
71
+
72
+ // Create P2PKH script from address
73
+ const lockingScript = new P2PKH().lock(address);
74
+
75
+ // Create the transaction
76
+ const tx = await wallet.createAction({
77
+ description,
78
+ outputs: [
79
+ {
80
+ lockingScript: lockingScript.toHex(),
81
+ satoshis,
82
+ outputDescription: `Payment to ${address}`,
83
+ },
84
+ ],
85
+ });
86
+
87
+ return {
88
+ content: [
89
+ {
90
+ type: "text",
91
+ text: JSON.stringify({
92
+ status: "success",
93
+ txid: tx.txid,
94
+ satoshis,
95
+ }),
96
+ },
97
+ ],
98
+ };
99
+ } catch (error) {
100
+ return {
101
+ content: [
102
+ {
103
+ type: "text",
104
+ text: error instanceof Error ? error.message : String(error),
105
+ },
106
+ ],
107
+ isError: true,
108
+ };
109
+ }
110
+ },
111
+ );
112
+ }
@@ -0,0 +1,76 @@
1
+ import { expect, test } from "bun:test";
2
+ import { PrivateKey } from "@bsv/sdk";
3
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
4
+ import type { RequestHandlerExtra } from "@modelcontextprotocol/sdk/shared/protocol.js";
5
+ import { getPublicKeyArgsSchema } from "./schemas";
6
+ import { registerWalletTools } from "./tools";
7
+ import { Wallet } from "./wallet";
8
+
9
+ // Define type for tool names to ensure they match what's in tools.ts
10
+ type WalletToolName =
11
+ | "wallet_getPublicKey"
12
+ | "wallet_createSignature"
13
+ | "wallet_verifySignature"
14
+ | "wallet_encrypt"
15
+ | "wallet_decrypt";
16
+
17
+ const toolNames: WalletToolName[] = [
18
+ "wallet_getPublicKey",
19
+ "wallet_createSignature",
20
+ "wallet_verifySignature",
21
+ "wallet_encrypt",
22
+ "wallet_decrypt",
23
+ ];
24
+
25
+ // Helper function to get dummy arguments for each tool
26
+ function getDummyArgs(tool: WalletToolName): Record<string, unknown> {
27
+ switch (tool) {
28
+ case "wallet_getPublicKey":
29
+ return getPublicKeyArgsSchema.parse({});
30
+ case "wallet_createSignature":
31
+ return { data: "test", keyType: "identity" };
32
+ case "wallet_verifySignature":
33
+ return {
34
+ data: "test",
35
+ keyType: "identity",
36
+ signature: "sig",
37
+ publicKey: "pubkey",
38
+ };
39
+ case "wallet_encrypt":
40
+ return { data: "test", publicKey: "pubkey" };
41
+ case "wallet_decrypt":
42
+ return { encryptedData: "data" };
43
+ default:
44
+ return {};
45
+ }
46
+ }
47
+
48
+ // Bun test
49
+ for (const tool of toolNames) {
50
+ test(`tool ${tool} returns not implemented error`, async () => {
51
+ const server = new McpServer({ name: "Test", version: "0.0.1" });
52
+ const wallet = new Wallet(
53
+ PrivateKey.fromWif(
54
+ "KyqU1boXYdksJKyxfsCtvBxfbt2a8XQd2aPhVZHNxMzvms9hRAvz",
55
+ ),
56
+ );
57
+ const handlers = registerWalletTools(server, wallet);
58
+
59
+ // Get the handler for this tool
60
+ const handler = handlers[tool];
61
+ if (!handler) {
62
+ throw new Error(`Tool ${tool} not registered`);
63
+ }
64
+
65
+ // Create a mock RequestHandlerExtra with required properties
66
+ const mockExtra: RequestHandlerExtra = {
67
+ signal: new AbortController().signal,
68
+ };
69
+
70
+ // Call the handler with dummy arguments
71
+ const result = await handler({ args: getDummyArgs(tool) }, mockExtra);
72
+
73
+ expect(result.isError).toBe(true);
74
+ expect(result.content?.[0]?.text ?? "").toMatch(/not implemented/i);
75
+ });
76
+ }