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.
- package/README.md +258 -0
- package/biome.json +30 -0
- package/index.ts +34 -0
- package/package.json +31 -0
- package/tests/integration/wallet-server.test.ts +216 -0
- package/tools/bsv/decodeTransaction.ts +299 -0
- package/tools/bsv/getPrice.ts +44 -0
- package/tools/bsv/index.ts +13 -0
- package/tools/bsv/token.ts +169 -0
- package/tools/index.ts +21 -0
- package/tools/ordinals/bsv20MarketSales.ts +118 -0
- package/tools/ordinals/getBsv20ById.ts +94 -0
- package/tools/ordinals/getInscription.ts +103 -0
- package/tools/ordinals/index.ts +19 -0
- package/tools/ordinals/marketListings.ts +137 -0
- package/tools/ordinals/searchInscriptions.ts +118 -0
- package/tools/utils/conversion.ts +58 -0
- package/tools/utils/index.ts +50 -0
- package/tools/wallet/getAddress.ts +38 -0
- package/tools/wallet/purchaseListing.ts +175 -0
- package/tools/wallet/schemas.ts +284 -0
- package/tools/wallet/sendToAddress.ts +112 -0
- package/tools/wallet/tools.test.ts +76 -0
- package/tools/wallet/tools.ts +206 -0
- package/tools/wallet/wallet.ts +290 -0
- package/tsconfig.json +28 -0
|
@@ -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
|
+
}
|