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,299 @@
1
+ import { Transaction, Utils } 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 { z } from "zod";
5
+
6
+ // Schema for decode transaction arguments
7
+ export const decodeTransactionArgsSchema = z.object({
8
+ tx: z.string().describe("Transaction data or txid"),
9
+ encoding: z
10
+ .enum(["hex", "base64"])
11
+ .default("hex")
12
+ .describe("Encoding of the input data"),
13
+ });
14
+
15
+ export type DecodeTransactionArgs = z.infer<typeof decodeTransactionArgsSchema>;
16
+
17
+ // Type for JungleBus API response
18
+ interface JungleBusTransactionResponse {
19
+ id: string;
20
+ transaction: string;
21
+ block_hash?: string;
22
+ block_height?: number;
23
+ block_time?: number;
24
+ block_index?: number;
25
+ addresses?: string[];
26
+ inputs?: string[];
27
+ outputs?: string[];
28
+ input_types?: string[];
29
+ output_types?: string[];
30
+ contexts?: string[];
31
+ sub_contexts?: string[];
32
+ data?: string[];
33
+ merkle_proof?: unknown;
34
+ }
35
+
36
+ // Network info response type
37
+ interface NetworkInfoResponse {
38
+ blocks: number;
39
+ [key: string]: unknown;
40
+ }
41
+
42
+ // Transaction input type
43
+ interface TransactionInputData {
44
+ txid: string | undefined;
45
+ vout: number;
46
+ sequence: number | undefined;
47
+ script: string;
48
+ scriptAsm: string;
49
+ type?: string;
50
+ value?: number;
51
+ }
52
+
53
+ // Transaction output type
54
+ interface TransactionOutputData {
55
+ n: number;
56
+ value: number | undefined;
57
+ scriptPubKey: {
58
+ hex: string;
59
+ asm: string;
60
+ };
61
+ type?: string;
62
+ }
63
+
64
+ // Transaction result type
65
+ interface TransactionResult {
66
+ txid: string;
67
+ version: number;
68
+ locktime: number;
69
+ size: number;
70
+ inputs: TransactionInputData[];
71
+ outputs: TransactionOutputData[];
72
+ confirmations?: number;
73
+ block?: {
74
+ hash: string;
75
+ height: number;
76
+ time: number;
77
+ index: number;
78
+ } | null;
79
+ addresses?: string[];
80
+ fee?: number | null;
81
+ feeRate?: number | null;
82
+ }
83
+
84
+ /**
85
+ * Fetches transaction data from JungleBus
86
+ */
87
+ async function fetchJungleBusData(
88
+ txid: string,
89
+ ): Promise<JungleBusTransactionResponse | null> {
90
+ try {
91
+ const response = await fetch(
92
+ `https://junglebus.gorillapool.io/v1/transaction/get/${txid}`,
93
+ );
94
+ if (!response.ok) {
95
+ console.error(
96
+ `JungleBus API error: ${response.status} ${response.statusText}`,
97
+ );
98
+ return null;
99
+ }
100
+ return (await response.json()) as JungleBusTransactionResponse;
101
+ } catch (error) {
102
+ console.error("Error fetching from JungleBus:", error);
103
+ return null;
104
+ }
105
+ }
106
+
107
+ /**
108
+ * Determines if a string is likely a txid
109
+ */
110
+ function isTxid(str: string): boolean {
111
+ // TX IDs are 64 characters in hex (32 bytes)
112
+ return /^[0-9a-f]{64}$/i.test(str);
113
+ }
114
+
115
+ /**
116
+ * Register the BSV transaction decode tool
117
+ */
118
+ export function registerDecodeTransactionTool(server: McpServer): void {
119
+ server.tool(
120
+ "bsv_decodeTransaction",
121
+ {
122
+ args: decodeTransactionArgsSchema,
123
+ },
124
+ async (
125
+ { args }: { args: DecodeTransactionArgs },
126
+ extra: RequestHandlerExtra,
127
+ ) => {
128
+ try {
129
+ const { tx, encoding } = args;
130
+ let transaction: Transaction;
131
+ let rawTx: string;
132
+ let txid: string;
133
+ let junglebusData: JungleBusTransactionResponse | null = null;
134
+
135
+ // Check if input is txid or raw transaction
136
+ if (isTxid(tx)) {
137
+ // It's a txid, fetch from JungleBus
138
+ txid = tx;
139
+ junglebusData = await fetchJungleBusData(txid);
140
+
141
+ if (!junglebusData) {
142
+ throw new Error(
143
+ `Failed to fetch transaction data for txid: ${txid}`,
144
+ );
145
+ }
146
+
147
+ // JungleBus returns base64, convert if needed
148
+ rawTx = junglebusData.transaction;
149
+ // Check if rawTx is in base64 format (common from JungleBus)
150
+ const isBase64 = /^[A-Za-z0-9+/=]+$/.test(rawTx);
151
+
152
+ if (isBase64) {
153
+ const txBytes = Utils.toArray(rawTx, "base64");
154
+ transaction = Transaction.fromBinary(txBytes);
155
+ } else {
156
+ transaction = Transaction.fromHex(rawTx);
157
+ }
158
+ } else {
159
+ // It's a raw transaction
160
+ let txBytes: number[];
161
+
162
+ if (encoding === "hex") {
163
+ txBytes = Utils.toArray(tx, "hex");
164
+ } else {
165
+ txBytes = Utils.toArray(tx, "base64");
166
+ }
167
+
168
+ transaction = Transaction.fromBinary(txBytes);
169
+ txid = transaction.hash("hex") as string;
170
+
171
+ // Optionally fetch additional context from JungleBus
172
+ junglebusData = await fetchJungleBusData(txid);
173
+ }
174
+
175
+ // Basic transaction data
176
+ const result: TransactionResult = {
177
+ txid,
178
+ version: transaction.version,
179
+ locktime: transaction.lockTime,
180
+ size: transaction.toBinary().length,
181
+ inputs: transaction.inputs.map((input) => ({
182
+ txid: input.sourceTXID,
183
+ vout: input.sourceOutputIndex,
184
+ sequence: input.sequence,
185
+ script: input.unlockingScript ? input.unlockingScript.toHex() : "",
186
+ scriptAsm: input.unlockingScript
187
+ ? input.unlockingScript.toASM()
188
+ : "",
189
+ })),
190
+ outputs: transaction.outputs.map((output) => ({
191
+ n: transaction.outputs.indexOf(output),
192
+ value: output.satoshis,
193
+ scriptPubKey: {
194
+ hex: output.lockingScript.toHex(),
195
+ asm: output.lockingScript.toASM(),
196
+ },
197
+ })),
198
+ };
199
+
200
+ // Add JungleBus context if available
201
+ if (junglebusData) {
202
+ result.confirmations = junglebusData.block_height
203
+ ? (await getCurrentBlockHeight()) - junglebusData.block_height + 1
204
+ : 0;
205
+
206
+ result.block = junglebusData.block_hash
207
+ ? {
208
+ hash: junglebusData.block_hash,
209
+ height: junglebusData.block_height || 0,
210
+ time: junglebusData.block_time || 0,
211
+ index: junglebusData.block_index || 0,
212
+ }
213
+ : null;
214
+
215
+ // Add script types
216
+ if (
217
+ junglebusData.input_types &&
218
+ junglebusData.input_types.length > 0
219
+ ) {
220
+ result.inputs = result.inputs.map((input, i) => ({
221
+ ...input,
222
+ type: junglebusData.input_types?.[i] || "unknown",
223
+ }));
224
+ }
225
+
226
+ if (
227
+ junglebusData.output_types &&
228
+ junglebusData.output_types.length > 0
229
+ ) {
230
+ result.outputs = result.outputs.map((output, i) => ({
231
+ ...output,
232
+ type: junglebusData.output_types?.[i] || "unknown",
233
+ }));
234
+ }
235
+
236
+ // Add addresses found in transaction
237
+ if (junglebusData.addresses && junglebusData.addresses.length > 0) {
238
+ result.addresses = junglebusData.addresses;
239
+ }
240
+ }
241
+
242
+ // Calculate additional information
243
+ const totalInputValue = result.inputs.reduce(
244
+ (sum, input) => sum + (input.value || 0),
245
+ 0,
246
+ );
247
+ const totalOutputValue = result.outputs.reduce(
248
+ (sum, output) => sum + (output.value || 0),
249
+ 0,
250
+ );
251
+
252
+ result.fee =
253
+ totalInputValue > 0 ? totalInputValue - totalOutputValue : null;
254
+ result.feeRate =
255
+ result.fee !== null
256
+ ? Math.round((result.fee / result.size) * 100000000) / 100000000
257
+ : null;
258
+
259
+ return {
260
+ content: [
261
+ {
262
+ type: "text",
263
+ text: JSON.stringify(result, null, 2),
264
+ },
265
+ ],
266
+ };
267
+ } catch (error) {
268
+ return {
269
+ content: [
270
+ {
271
+ type: "text",
272
+ text: error instanceof Error ? error.message : String(error),
273
+ },
274
+ ],
275
+ isError: true,
276
+ };
277
+ }
278
+ },
279
+ );
280
+ }
281
+
282
+ /**
283
+ * Get current block height
284
+ */
285
+ async function getCurrentBlockHeight(): Promise<number> {
286
+ try {
287
+ const response = await fetch(
288
+ "https://junglebus.gorillapool.io/v1/network/info",
289
+ );
290
+ if (!response.ok) {
291
+ return 0;
292
+ }
293
+ const data = (await response.json()) as NetworkInfoResponse;
294
+ return data.blocks || 0;
295
+ } catch (error) {
296
+ console.error("Error fetching current block height:", error);
297
+ return 0;
298
+ }
299
+ }
@@ -0,0 +1,44 @@
1
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { z } from "zod";
3
+
4
+ /**
5
+ * Register the BSV price lookup tool
6
+ * @param server The MCP server instance
7
+ */
8
+ export function registerGetPriceTool(server: McpServer): void {
9
+ server.tool(
10
+ "bsv_getPrice",
11
+ {
12
+ args: z.object({}).optional(),
13
+ },
14
+ async () => {
15
+ try {
16
+ const res = await fetch(
17
+ "https://api.whatsonchain.com/v1/bsv/main/exchangerate",
18
+ );
19
+ if (!res.ok) throw new Error("Failed to fetch price");
20
+ const data = (await res.json()) as {
21
+ currency: string;
22
+ rate: string;
23
+ time: number;
24
+ };
25
+ const price = data.rate;
26
+ if (typeof price !== "string" && typeof price !== "number")
27
+ throw new Error("Price not found");
28
+ return {
29
+ content: [
30
+ {
31
+ type: "text",
32
+ text: `Current BSV price: $${Number(price).toFixed(2)} USD`,
33
+ },
34
+ ],
35
+ };
36
+ } catch (err) {
37
+ return {
38
+ content: [{ type: "text", text: "Error fetching BSV price." }],
39
+ isError: true,
40
+ };
41
+ }
42
+ },
43
+ );
44
+ }
@@ -0,0 +1,13 @@
1
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { registerDecodeTransactionTool } from "./decodeTransaction";
3
+ import { registerGetPriceTool } from "./getPrice";
4
+
5
+ /**
6
+ * Register all BSV tools with the MCP server
7
+ * @param server The MCP server instance
8
+ */
9
+ export function registerBsvTools(server: McpServer): void {
10
+ // Register BSV-related tools
11
+ registerGetPriceTool(server);
12
+ registerDecodeTransactionTool(server);
13
+ }
@@ -0,0 +1,169 @@
1
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import {
3
+ ReturnTypes,
4
+ toBitcoin,
5
+ toSatoshi,
6
+ toToken,
7
+ toTokenSat,
8
+ } from "satoshi-token";
9
+ import { z } from "zod";
10
+
11
+ /**
12
+ * Register token conversion tools for BSV
13
+ * @param server The MCP server instance
14
+ */
15
+ export function registerTokenTools(server: McpServer): void {
16
+ // Convert Bitcoin to Satoshis
17
+ server.tool(
18
+ "bsv_toSatoshi",
19
+ {
20
+ args: z.object({
21
+ bitcoin: z.union([z.number(), z.string()]),
22
+ returnType: z.enum(["number", "string", "bigint"]).optional(),
23
+ }),
24
+ },
25
+ async ({ args }) => {
26
+ try {
27
+ const { bitcoin, returnType } = args;
28
+ let result: number | string | bigint;
29
+
30
+ switch (returnType) {
31
+ case "bigint":
32
+ result = toSatoshi(bitcoin, ReturnTypes.BigInt);
33
+ return { content: [{ type: "text", text: result.toString() }] };
34
+ case "string":
35
+ result = toSatoshi(bitcoin, ReturnTypes.String);
36
+ return { content: [{ type: "text", text: result }] };
37
+ default:
38
+ result = toSatoshi(bitcoin);
39
+ return { content: [{ type: "text", text: result.toString() }] };
40
+ }
41
+ } catch (err: unknown) {
42
+ const msg = err instanceof Error ? err.message : String(err);
43
+ return { content: [{ type: "text", text: msg }], isError: true };
44
+ }
45
+ },
46
+ );
47
+
48
+ // Convert Satoshis to Bitcoin
49
+ server.tool(
50
+ "bsv_toBitcoin",
51
+ {
52
+ args: z.object({
53
+ satoshis: z.union([z.number(), z.string(), z.bigint()]),
54
+ returnType: z.enum(["number", "string", "bigint"]).optional(),
55
+ }),
56
+ },
57
+ async ({ args }) => {
58
+ try {
59
+ const { satoshis, returnType } = args;
60
+ let result: number | string | bigint;
61
+
62
+ switch (returnType) {
63
+ case "bigint":
64
+ try {
65
+ result = toBitcoin(satoshis, ReturnTypes.BigInt);
66
+ return { content: [{ type: "text", text: result.toString() }] };
67
+ } catch (e) {
68
+ return {
69
+ content: [
70
+ {
71
+ type: "text",
72
+ text: "Error: Cannot return Bitcoin amount as BigInt if it has decimal part",
73
+ },
74
+ ],
75
+ isError: true,
76
+ };
77
+ }
78
+ case "string":
79
+ result = toBitcoin(satoshis, ReturnTypes.String);
80
+ return { content: [{ type: "text", text: result }] };
81
+ default:
82
+ result = toBitcoin(satoshis);
83
+ return { content: [{ type: "text", text: result.toString() }] };
84
+ }
85
+ } catch (err: unknown) {
86
+ const msg = err instanceof Error ? err.message : String(err);
87
+ return { content: [{ type: "text", text: msg }], isError: true };
88
+ }
89
+ },
90
+ );
91
+
92
+ // Generic token conversion (for tokens with custom decimal places)
93
+ server.tool(
94
+ "bsv_toTokenSatoshi",
95
+ {
96
+ args: z.object({
97
+ token: z.union([z.number(), z.string(), z.bigint()]),
98
+ decimals: z.number().int().min(0),
99
+ returnType: z.enum(["number", "string", "bigint"]).optional(),
100
+ }),
101
+ },
102
+ async ({ args }) => {
103
+ try {
104
+ const { token, decimals, returnType } = args;
105
+ let result: number | string | bigint;
106
+
107
+ switch (returnType) {
108
+ case "bigint":
109
+ result = toTokenSat(token, decimals, ReturnTypes.BigInt);
110
+ return { content: [{ type: "text", text: result.toString() }] };
111
+ case "string":
112
+ result = toTokenSat(token, decimals, ReturnTypes.String);
113
+ return { content: [{ type: "text", text: result }] };
114
+ default:
115
+ result = toTokenSat(token, decimals);
116
+ return { content: [{ type: "text", text: result.toString() }] };
117
+ }
118
+ } catch (err: unknown) {
119
+ const msg = err instanceof Error ? err.message : String(err);
120
+ return { content: [{ type: "text", text: msg }], isError: true };
121
+ }
122
+ },
123
+ );
124
+
125
+ // Generic token conversion (for tokens with custom decimal places)
126
+ server.tool(
127
+ "bsv_toToken",
128
+ {
129
+ args: z.object({
130
+ tokenSatoshi: z.union([z.number(), z.string(), z.bigint()]),
131
+ decimals: z.number().int().min(0),
132
+ returnType: z.enum(["number", "string", "bigint"]).optional(),
133
+ }),
134
+ },
135
+ async ({ args }) => {
136
+ try {
137
+ const { tokenSatoshi, decimals, returnType } = args;
138
+ let result: number | string | bigint;
139
+
140
+ switch (returnType) {
141
+ case "bigint":
142
+ try {
143
+ result = toToken(tokenSatoshi, decimals, ReturnTypes.BigInt);
144
+ return { content: [{ type: "text", text: result.toString() }] };
145
+ } catch (e) {
146
+ return {
147
+ content: [
148
+ {
149
+ type: "text",
150
+ text: "Error: Cannot return token amount as BigInt if it has decimal part",
151
+ },
152
+ ],
153
+ isError: true,
154
+ };
155
+ }
156
+ case "string":
157
+ result = toToken(tokenSatoshi, decimals, ReturnTypes.String);
158
+ return { content: [{ type: "text", text: result }] };
159
+ default:
160
+ result = toToken(tokenSatoshi, decimals);
161
+ return { content: [{ type: "text", text: result.toString() }] };
162
+ }
163
+ } catch (err: unknown) {
164
+ const msg = err instanceof Error ? err.message : String(err);
165
+ return { content: [{ type: "text", text: msg }], isError: true };
166
+ }
167
+ },
168
+ );
169
+ }
package/tools/index.ts ADDED
@@ -0,0 +1,21 @@
1
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { registerBsvTools } from "./bsv";
3
+ import { registerOrdinalsTools } from "./ordinals";
4
+ import { registerUtilsTools } from "./utils";
5
+
6
+ /**
7
+ * Register all tools with the MCP server
8
+ * @param server The MCP server instance
9
+ */
10
+ export function registerAllTools(server: McpServer): void {
11
+ // Register BSV-related tools
12
+ registerBsvTools(server);
13
+
14
+ // Register Ordinals-related tools
15
+ registerOrdinalsTools(server);
16
+
17
+ // Register utility tools
18
+ registerUtilsTools(server);
19
+
20
+ // Add more tool categories as needed
21
+ }
@@ -0,0 +1,118 @@
1
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import type { RequestHandlerExtra } from "@modelcontextprotocol/sdk/shared/protocol.js";
3
+ import { z } from "zod";
4
+
5
+ // Schema for BSV20 market sales arguments
6
+ export const bsv20MarketSalesArgsSchema = z.object({
7
+ limit: z
8
+ .number()
9
+ .int()
10
+ .min(1)
11
+ .max(100)
12
+ .default(20)
13
+ .describe("Number of results (1-100, default 20)"),
14
+ offset: z.number().int().min(0).default(0).describe("Pagination offset"),
15
+ dir: z
16
+ .enum(["asc", "desc"])
17
+ .default("desc")
18
+ .describe("Sort direction (asc or desc)"),
19
+ type: z
20
+ .enum(["v1", "v2", "all"])
21
+ .default("all")
22
+ .describe("Token type (v1, v2, or all)"),
23
+ id: z.string().optional().describe("Token ID in outpoint format"),
24
+ tick: z.string().optional().describe("Token ticker symbol"),
25
+ pending: z.boolean().default(false).describe("Include pending sales"),
26
+ address: z.string().optional().describe("Bitcoin address"),
27
+ });
28
+
29
+ export type Bsv20MarketSalesArgs = z.infer<typeof bsv20MarketSalesArgsSchema>;
30
+
31
+ // Simplified BSV20 sale response type
32
+ interface Bsv20SaleResponse {
33
+ results: Array<{
34
+ outpoint: string;
35
+ data?: {
36
+ bsv20?: {
37
+ id?: string;
38
+ tick?: string;
39
+ sym?: string;
40
+ amt?: string;
41
+ op?: string;
42
+ };
43
+ list?: {
44
+ price?: number;
45
+ payout?: string;
46
+ sale?: boolean;
47
+ };
48
+ };
49
+ satoshis?: number;
50
+ height?: number;
51
+ [key: string]: unknown;
52
+ }>;
53
+ total: number;
54
+ }
55
+
56
+ /**
57
+ * Register the BSV20 market sales tool
58
+ */
59
+ export function registerBsv20MarketSalesTool(server: McpServer): void {
60
+ server.tool(
61
+ "ordinals_bsv20MarketSales",
62
+ {
63
+ args: bsv20MarketSalesArgsSchema,
64
+ },
65
+ async (
66
+ { args }: { args: Bsv20MarketSalesArgs },
67
+ extra: RequestHandlerExtra,
68
+ ) => {
69
+ try {
70
+ const { limit, offset, dir, type, id, tick, pending, address } = args;
71
+
72
+ // Build the URL with query parameters
73
+ const url = new URL(
74
+ "https://ordinals.gorillapool.io/api/bsv20/market/sales",
75
+ );
76
+ url.searchParams.append("limit", limit.toString());
77
+ url.searchParams.append("offset", offset.toString());
78
+ url.searchParams.append("dir", dir);
79
+ url.searchParams.append("type", type);
80
+ url.searchParams.append("pending", pending.toString());
81
+
82
+ if (id) url.searchParams.append("id", id);
83
+ if (tick) url.searchParams.append("tick", tick);
84
+ if (address) url.searchParams.append("address", address);
85
+
86
+ // Fetch BSV20 market sales from GorillaPool API
87
+ const response = await fetch(url.toString());
88
+
89
+ if (!response.ok) {
90
+ throw new Error(
91
+ `API error: ${response.status} ${response.statusText}`,
92
+ );
93
+ }
94
+
95
+ const data = (await response.json()) as Bsv20SaleResponse;
96
+
97
+ return {
98
+ content: [
99
+ {
100
+ type: "text",
101
+ text: JSON.stringify(data, null, 2),
102
+ },
103
+ ],
104
+ };
105
+ } catch (error) {
106
+ return {
107
+ content: [
108
+ {
109
+ type: "text",
110
+ text: error instanceof Error ? error.message : String(error),
111
+ },
112
+ ],
113
+ isError: true,
114
+ };
115
+ }
116
+ },
117
+ );
118
+ }