bsv-mcp 0.0.34 → 0.0.36

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.
@@ -1,15 +1,21 @@
1
1
  import { PrivateKey, Utils } from "@bsv/sdk";
2
+ import { Client } from "@modelcontextprotocol/sdk/client/index.js";
3
+ import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
2
4
  import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
5
  import type { RequestHandlerExtra } from "@modelcontextprotocol/sdk/shared/protocol.js";
4
6
  import type {
7
+ ClientNotification,
8
+ ClientRequest,
5
9
  ServerNotification,
6
10
  ServerRequest,
7
11
  } from "@modelcontextprotocol/sdk/types.js";
8
12
  import { createOrdinals } from "js-1sat-ord";
9
13
  import type {
10
14
  ChangeResult,
15
+ CreateOrdinalsConfig,
11
16
  Destination,
12
17
  Inscription,
18
+ LocalSigner,
13
19
  PreMAP,
14
20
  } from "js-1sat-ord";
15
21
  import { Sigma } from "sigma-protocol";
@@ -17,10 +23,16 @@ import { z } from "zod";
17
23
  import type { Wallet } from "./wallet";
18
24
  const { toArray, toBase64 } = Utils;
19
25
 
26
+ // API endpoint for the A2B Overlay service
27
+ const OVERLAY_API_URL = "https://a2b-overlay-production.up.railway.app/v1";
28
+
20
29
  // Schema for the MCP tool configuration
21
30
  export const McpConfigSchema = z.object({
22
31
  command: z.string().describe("The command to execute the tool"),
23
32
  args: z.array(z.string()).describe("Arguments to pass to the command"),
33
+ tools: z.array(z.string()).optional().describe("Available tool names"),
34
+ prompts: z.array(z.string()).optional().describe("Available prompt names"),
35
+ resources: z.array(z.string()).optional().describe("Available resource URIs"),
24
36
  env: z.record(z.string()).optional().describe("Environment variables"),
25
37
  });
26
38
 
@@ -31,6 +43,10 @@ export const a2bPublishMcpArgsSchema = z.object({
31
43
  toolName: z.string().describe("Human-friendly tool name"),
32
44
  command: z.string().describe("The command to execute the tool"),
33
45
  args: z.array(z.string()).describe("Arguments to pass to the command"),
46
+ keywords: z
47
+ .array(z.string())
48
+ .optional()
49
+ .describe("Optional keywords to improve tool discoverability"),
34
50
  env: z
35
51
  .array(
36
52
  z.object({
@@ -51,6 +67,119 @@ export const a2bPublishMcpArgsSchema = z.object({
51
67
 
52
68
  export type A2bPublishMcpArgs = z.infer<typeof a2bPublishMcpArgsSchema>;
53
69
 
70
+ /**
71
+ * Call the ingest endpoint to process a transaction
72
+ */
73
+ async function callIngestEndpoint(txid: string): Promise<boolean> {
74
+ try {
75
+ const response = await fetch(`${OVERLAY_API_URL}/ingest`, {
76
+ method: "POST",
77
+ headers: {
78
+ "Content-Type": "application/json",
79
+ },
80
+ body: JSON.stringify({ txid }),
81
+ });
82
+
83
+ if (!response.ok) {
84
+ console.warn(
85
+ `Ingest API returned status ${response.status}: ${response.statusText}`,
86
+ );
87
+ return false;
88
+ }
89
+
90
+ const result = await response.json();
91
+ // console.log('Ingest result:', result);
92
+ return true;
93
+ } catch (error) {
94
+ console.warn("Error calling ingest endpoint:", error);
95
+ return false;
96
+ }
97
+ }
98
+
99
+ /**
100
+ * Fetches MCP metadata (tools, prompts, resources) by running the command
101
+ * and connecting to it via the MCP client
102
+ */
103
+ async function fetchMcpMetadata(
104
+ command: string,
105
+ args: string[],
106
+ ): Promise<{
107
+ tools: string[];
108
+ prompts: string[];
109
+ resources: string[];
110
+ }> {
111
+ // console.log(`Fetching MCP metadata by running: ${command} ${args.join(' ')}`);
112
+
113
+ let transport: StdioClientTransport | undefined;
114
+ let client:
115
+ | Client<
116
+ ClientRequest,
117
+ ServerRequest,
118
+ ClientNotification | ServerNotification
119
+ >
120
+ | undefined;
121
+
122
+ try {
123
+ // Create a transport to the MCP server
124
+ // Pass through all current environment variables to ensure the same configuration
125
+ transport = new StdioClientTransport({
126
+ command,
127
+ args,
128
+ env: {
129
+ ...process.env, // Pass through all current environment variables
130
+ DISABLE_BROADCASTING: "true", // Prevent actual broadcasting during tool discovery
131
+ },
132
+ });
133
+
134
+ // Create and connect a client
135
+ client = new Client({
136
+ name: "metadata-fetcher",
137
+ version: "1.0.0",
138
+ });
139
+
140
+ await client.connect(transport);
141
+
142
+ // Fetch available tools, prompts, and resources
143
+ const toolsResponse = await client.listTools();
144
+ const promptsResponse = await client.listPrompts();
145
+ const resourcesResponse = await client.listResources();
146
+
147
+ // Extract the names/URIs
148
+ const tools = toolsResponse.tools.map((tool) => tool.name);
149
+ const prompts = promptsResponse.prompts.map((prompt) => prompt.name);
150
+ const resources = resourcesResponse.resources.map(
151
+ (resource) => resource.uri,
152
+ );
153
+
154
+ // Add known tools that might be missing, avoid duplicates
155
+ const allTools = [...new Set([...tools])];
156
+
157
+ return {
158
+ tools: allTools,
159
+ prompts,
160
+ resources,
161
+ };
162
+ } catch (error) {
163
+ console.error("Error fetching MCP metadata:", error);
164
+ // Return hardcoded list of known tools if fetching fails
165
+ return {
166
+ tools: [],
167
+ prompts: [],
168
+ resources: [],
169
+ };
170
+ } finally {
171
+ // Clean up resources
172
+ if (transport) {
173
+ try {
174
+ // Close the transport to shut down the child process
175
+ await transport.close();
176
+ } catch (e) {
177
+ console.error("Error closing transport:", e);
178
+ }
179
+ }
180
+ }
181
+ }
182
+
54
183
  /**
55
184
  * Registers the wallet_a2bPublishMcp for publishing an MCP tool configuration on-chain
56
185
  */
@@ -61,7 +190,7 @@ export function registerA2bPublishMcpTool(
61
190
  ) {
62
191
  server.tool(
63
192
  "wallet_a2bPublishMcp",
64
- "Publish an MCP tool configuration record on-chain via Ordinal inscription",
193
+ "Publish an MCP tool configuration record on-chain via Ordinal inscription. This creates a permanent, immutable, and discoverable tool definition that can be accessed by other MCP servers. The tool is published as a JSON inscription with metadata and optional digital signatures for authenticity verification.",
65
194
  { args: a2bPublishMcpArgsSchema },
66
195
  async (
67
196
  { args }: { args: A2bPublishMcpArgs },
@@ -91,10 +220,17 @@ export function registerA2bPublishMcpTool(
91
220
 
92
221
  const walletAddress = paymentPk.toAddress().toString();
93
222
 
223
+ // Fetch MCP metadata (tools, prompts, resources)
224
+ const metadata = await fetchMcpMetadata(args.command, args.args);
225
+ // console.log(`Discovered ${metadata.tools.length} tools, ${metadata.prompts.length} prompts, and ${metadata.resources.length} resources`);
226
+
94
227
  // Assemble tool configuration
95
228
  const toolConfig: McpConfig = {
96
229
  command: args.command,
97
230
  args: args.args,
231
+ tools: metadata.tools,
232
+ prompts: metadata.prompts,
233
+ resources: metadata.resources,
98
234
  env: args.env
99
235
  ? args.env.reduce(
100
236
  (acc, { key, description }) => {
@@ -114,7 +250,10 @@ export function registerA2bPublishMcpTool(
114
250
  mcpServers: {
115
251
  [args.toolName]: {
116
252
  description: args.description || "",
117
- type: "mcp-tool",
253
+ keywords: args.keywords || [],
254
+ tools: metadata.tools || [],
255
+ prompts: metadata.prompts || [],
256
+ resources: metadata.resources || [],
118
257
  ...toolConfig,
119
258
  },
120
259
  },
@@ -138,26 +277,32 @@ export function registerA2bPublishMcpTool(
138
277
  // Default MAP metadata: file path, content type, encoding
139
278
  const metaData: PreMAP = { app: "bsv-mcp", type: "a2b-mcp" };
140
279
 
141
- // Inscribe the ordinal on-chain via js-1sat-ord
142
- const result = await createOrdinals({
280
+ const createOrdinalsConfig = {
143
281
  utxos: paymentUtxos,
144
282
  destinations,
145
283
  paymentPk,
146
284
  changeAddress: walletAddress,
147
285
  metaData,
148
- });
286
+ } as CreateOrdinalsConfig;
287
+ if (identityPk) {
288
+ createOrdinalsConfig.signer = {
289
+ idKey: identityPk,
290
+ } as LocalSigner;
291
+ }
292
+ // Inscribe the ordinal on-chain via js-1sat-ord
293
+ const result = await createOrdinals(createOrdinalsConfig);
149
294
 
150
295
  const changeResult = result as ChangeResult;
151
296
 
152
- let finalTx = changeResult.tx;
153
- if (identityPk) {
154
- const sigma = new Sigma(result.tx);
155
- const signResponse = sigma.sign(identityPk);
156
- finalTx = signResponse.signedTx;
157
- }
158
297
  // Broadcast the transaction
159
298
  if (!config.disableBroadcasting) {
160
- await finalTx.broadcast();
299
+ await changeResult.tx.broadcast();
300
+ const txid = changeResult.tx.id("hex");
301
+
302
+ setTimeout(async () => {
303
+ // Call the ingest endpoint to process the transaction
304
+ await callIngestEndpoint(txid);
305
+ }, 1000);
161
306
 
162
307
  // Refresh UTXOs after spending
163
308
  try {
@@ -171,7 +316,7 @@ export function registerA2bPublishMcpTool(
171
316
 
172
317
  // Build a nicely formatted result
173
318
  const outpointIndex = 0; // First output with the inscription
174
- const outpoint = `${finalTx.id("hex")}_${outpointIndex}`;
319
+ const outpoint = `${txid}_${outpointIndex}`;
175
320
 
176
321
  // Tool URL for discovery is the outpoint
177
322
  const onchainUrl = `ord://${outpoint}`;
@@ -183,10 +328,13 @@ export function registerA2bPublishMcpTool(
183
328
  text: JSON.stringify(
184
329
  {
185
330
  status: "success",
186
- txid: finalTx.id("hex"),
331
+ txid,
187
332
  outpoint,
188
333
  onchainUrl,
189
334
  toolName: args.toolName,
335
+ toolCount: metadata.tools.length,
336
+ promptCount: metadata.prompts.length,
337
+ resourceCount: metadata.resources.length,
190
338
  description:
191
339
  args.description || `MCP Tool: ${args.toolName}`,
192
340
  address: targetAddress,
@@ -202,7 +350,7 @@ export function registerA2bPublishMcpTool(
202
350
  content: [
203
351
  {
204
352
  type: "text",
205
- text: finalTx.toHex(),
353
+ text: changeResult.tx.toHex(),
206
354
  },
207
355
  ],
208
356
  };
@@ -0,0 +1,130 @@
1
+ import { P2PKH, Transaction, Utils } from "@bsv/sdk";
2
+ import type { Utxo } from "js-1sat-ord";
3
+ const { toBase64 } = Utils;
4
+
5
+ /**
6
+ * Type definition for WhatsOnChain UTXO response
7
+ */
8
+ interface WhatsOnChainUtxo {
9
+ tx_hash: string;
10
+ tx_pos: number;
11
+ value: number;
12
+ height: number;
13
+ address?: string;
14
+ }
15
+
16
+ /**
17
+ * Fetches unspent transaction outputs (UTXOs) for a given address.
18
+ * Only returns confirmed unspent outputs.
19
+ *
20
+ * @param address - The address to fetch UTXOs for
21
+ * @returns Array of UTXOs or undefined if an error occurs
22
+ */
23
+ export async function fetchPaymentUtxos(
24
+ address: string,
25
+ ): Promise<Utxo[] | undefined> {
26
+ if (!address) {
27
+ console.error("fetchPaymentUtxos: No address provided");
28
+ return undefined;
29
+ }
30
+
31
+ try {
32
+ // Fetch UTXOs from WhatsOnChain API
33
+ const response = await fetch(
34
+ `https://api.whatsonchain.com/v1/bsv/main/address/${address}/unspent`,
35
+ );
36
+
37
+ if (!response.ok) {
38
+ console.error(
39
+ `WhatsOnChain API error: ${response.status} ${response.statusText}`,
40
+ );
41
+ return undefined;
42
+ }
43
+
44
+ const data = (await response.json()) as WhatsOnChainUtxo[];
45
+
46
+ // Validate response format
47
+ if (!Array.isArray(data)) {
48
+ console.error("Invalid response format from WhatsOnChain API");
49
+ return undefined;
50
+ }
51
+
52
+ // For testing purposes (FOR TESTING ONLY - REMOVE IN PRODUCTION)
53
+ // const limitUTXOs = data.slice(0, 2);
54
+
55
+ // Process each UTXO
56
+ const utxos: (Utxo | null)[] = await Promise.all(
57
+ data.map(async (utxo: WhatsOnChainUtxo) => {
58
+ // Get the transaction hex to extract the correct script
59
+ const script = await getScriptFromTransaction(
60
+ utxo.tx_hash,
61
+ utxo.tx_pos,
62
+ );
63
+
64
+ if (!script) {
65
+ console.warn(
66
+ `Could not get script for UTXO: ${utxo.tx_hash}:${utxo.tx_pos}`,
67
+ );
68
+ return null;
69
+ }
70
+
71
+ return {
72
+ txid: utxo.tx_hash,
73
+ vout: utxo.tx_pos,
74
+ satoshis: utxo.value,
75
+ script: script,
76
+ };
77
+ }),
78
+ );
79
+
80
+ // Filter out any null entries from failed processing
81
+ const validUtxos = utxos.filter((utxo) => utxo !== null) as Utxo[];
82
+
83
+ return validUtxos;
84
+ } catch (error) {
85
+ console.error("Error fetching payment UTXOs:", error);
86
+ return undefined;
87
+ }
88
+ }
89
+
90
+ /**
91
+ * Gets the script from a transaction for a specific output index
92
+ *
93
+ * @param txid - The transaction ID
94
+ * @param vout - The output index
95
+ * @returns The script as hex string or undefined if an error occurs
96
+ */
97
+ async function getScriptFromTransaction(
98
+ txid: string,
99
+ vout: number,
100
+ ): Promise<string | undefined> {
101
+ try {
102
+ const response = await fetch(
103
+ `https://api.whatsonchain.com/v1/bsv/main/tx/${txid}/hex`,
104
+ );
105
+
106
+ if (!response.ok) {
107
+ console.error(
108
+ `WhatsOnChain API error fetching tx hex: ${response.status} ${response.statusText}`,
109
+ );
110
+ return undefined;
111
+ }
112
+
113
+ const txHex = await response.text();
114
+ const tx = Transaction.fromHex(txHex);
115
+ const output = tx.outputs[vout];
116
+
117
+ if (!output) {
118
+ console.error(`Output index ${vout} not found in transaction ${txid}`);
119
+ return undefined;
120
+ }
121
+
122
+ return toBase64(output.lockingScript.toBinary());
123
+ } catch (error) {
124
+ console.error(
125
+ `Error getting script for transaction ${txid}:${vout}:`,
126
+ error,
127
+ );
128
+ return undefined;
129
+ }
130
+ }
@@ -0,0 +1,76 @@
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";
8
+ import { z } from "zod";
9
+ import type { Wallet } from "./wallet";
10
+
11
+ const refreshUtxosArgsSchema = z.object({});
12
+ type RefreshUtxosArgs = z.infer<typeof refreshUtxosArgsSchema>;
13
+
14
+ /**
15
+ * Registers the wallet_refreshUtxos tool that refreshes and returns the UTXOs for the wallet
16
+ */
17
+ export function registerRefreshUtxosTool(server: McpServer, wallet: Wallet) {
18
+ server.tool(
19
+ "wallet_refreshUtxos",
20
+ "Refreshes and returns the wallet's UTXOs. This is useful for debugging UTXO issues, ensuring the wallet has the latest transaction outputs, and for verifying available funds before making transactions.",
21
+ { args: refreshUtxosArgsSchema },
22
+ async (
23
+ { args }: { args: RefreshUtxosArgs },
24
+ _extra: RequestHandlerExtra<ServerRequest, ServerNotification>,
25
+ ) => {
26
+ try {
27
+ // Force refresh the UTXOs
28
+ await wallet.refreshUtxos();
29
+
30
+ // Get the refreshed UTXOs
31
+ const { paymentUtxos, nftUtxos } = await wallet.getUtxos();
32
+
33
+ // Calculate total satoshis in payment UTXOs
34
+ const totalSatoshis = paymentUtxos.reduce(
35
+ (sum, utxo) => sum + utxo.satoshis,
36
+ 0,
37
+ );
38
+
39
+ // Format the response
40
+ return {
41
+ content: [
42
+ {
43
+ type: "text",
44
+ text: JSON.stringify(
45
+ {
46
+ status: "success",
47
+ paymentUtxos: paymentUtxos.map((utxo) => ({
48
+ txid: utxo.txid,
49
+ vout: utxo.vout,
50
+ satoshis: utxo.satoshis,
51
+ outpoint: `${utxo.txid}_${utxo.vout}`,
52
+ })),
53
+ nftUtxos: nftUtxos.map((utxo) => ({
54
+ txid: utxo.txid,
55
+ vout: utxo.vout,
56
+ origin: utxo.origin,
57
+ outpoint: `${utxo.txid}_${utxo.vout}`,
58
+ })),
59
+ totalPaymentUtxos: paymentUtxos.length,
60
+ totalNftUtxos: nftUtxos.length,
61
+ totalSatoshis: totalSatoshis,
62
+ totalBsv: toBitcoin(totalSatoshis),
63
+ },
64
+ null,
65
+ 2,
66
+ ),
67
+ },
68
+ ],
69
+ };
70
+ } catch (err: unknown) {
71
+ const msg = err instanceof Error ? err.message : String(err);
72
+ return { content: [{ type: "text", text: msg }], isError: true };
73
+ }
74
+ },
75
+ );
76
+ }
@@ -44,6 +44,7 @@ import type { createOrdinalsArgsSchema } from "./createOrdinals";
44
44
  import { registerGetAddressTool } from "./getAddress";
45
45
  import { registerGetPublicKeyTool } from "./getPublicKey";
46
46
  import { registerPurchaseListingTool } from "./purchaseListing";
47
+ import { registerRefreshUtxosTool } from "./refreshUtxos";
47
48
  import { registerSendToAddressTool } from "./sendToAddress";
48
49
  import { registerTransferOrdTokenTool } from "./transferOrdToken";
49
50
 
@@ -83,6 +84,7 @@ type ToolArgSchemas = {
83
84
  wallet_transferOrdToken: typeof transferOrdTokenArgsSchema;
84
85
  wallet_a2bPublish: typeof a2bPublishArgsSchema;
85
86
  wallet_createOrdinals: typeof createOrdinalsArgsSchema;
87
+ wallet_refreshUtxos: typeof emptyArgsSchema;
86
88
  };
87
89
 
88
90
  // Define a type for the handler function with proper argument types
@@ -133,6 +135,9 @@ export function registerWalletTools(
133
135
  // Register the wallet_transferOrdToken tool
134
136
  registerTransferOrdTokenTool(server, wallet);
135
137
 
138
+ // Register the wallet_refreshUtxos tool
139
+ registerRefreshUtxosTool(server, wallet);
140
+
136
141
  // A2B tools have to be explicitly enabled
137
142
  if (config.enableA2bTools) {
138
143
  // Register the wallet_a2bPublishAgent tool
@@ -47,12 +47,8 @@ import type {
47
47
  WalletCertificate,
48
48
  WalletInterface,
49
49
  } from "@bsv/sdk";
50
- import {
51
- type NftUtxo,
52
- type Utxo,
53
- fetchNftUtxos,
54
- fetchPayUtxos,
55
- } from "js-1sat-ord";
50
+ import { type NftUtxo, type Utxo, fetchNftUtxos } from "js-1sat-ord";
51
+ import { fetchPaymentUtxos } from "./fetchPaymentUtxos";
56
52
 
57
53
  export class Wallet extends ProtoWallet implements WalletInterface {
58
54
  private paymentUtxos: Utxo[] = [];
@@ -77,28 +73,37 @@ export class Wallet extends ProtoWallet implements WalletInterface {
77
73
  try {
78
74
  const privateKey = this.getPrivateKey();
79
75
  if (!privateKey) {
80
- console.warn("No private key available for fetching UTXOs");
81
- return;
76
+ return; // Silent fail if no private key, keep existing UTXOs
82
77
  }
83
78
 
84
- const address = privateKey.toAddress();
79
+ const address = privateKey.toAddress().toString();
85
80
  this.lastUtxoFetch = Date.now();
86
81
 
82
+ // Payment UTXOs
83
+ let newPaymentUtxos: Utxo[] | undefined = undefined;
87
84
  try {
88
- const utxos = await fetchPayUtxos(address);
89
- this.paymentUtxos = utxos;
85
+ newPaymentUtxos = await fetchPaymentUtxos(address);
86
+ // Only update if we successfully got UTXOs
87
+ if (Array.isArray(newPaymentUtxos)) {
88
+ this.paymentUtxos = newPaymentUtxos;
89
+ }
90
90
  } catch (error) {
91
- console.error("Error fetching payment UTXOs:", error);
91
+ // Keep existing UTXOs, don't clear them on error
92
92
  }
93
+
94
+ // NFT UTXOs - keep existing if fetch fails
95
+ let newNftUtxos: NftUtxo[] = [];
93
96
  try {
94
- const nftUtxos = await fetchNftUtxos(address);
95
- this.nftUtxos = nftUtxos;
97
+ newNftUtxos = await fetchNftUtxos(address);
98
+ // Only update if we successfully got UTXOs
99
+ if (Array.isArray(newNftUtxos)) {
100
+ this.nftUtxos = newNftUtxos;
101
+ }
96
102
  } catch (error) {
97
- console.error("Error fetching NFT UTXOs:", error);
103
+ // Keep existing UTXOs, don't clear them on error
98
104
  }
99
105
  } catch (error) {
100
- console.error("Error refreshing UTXOs:", error);
101
- throw error;
106
+ // Silent global error, preserve existing UTXOs
102
107
  }
103
108
  }
104
109