bsv-mcp 0.0.34 → 0.0.35

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 CHANGED
@@ -1,5 +1,25 @@
1
1
  # BSV MCP Server Changelog
2
2
 
3
+ ## v0.0.35 - A2B Overlay Integration & Improved MCP Server Publishing
4
+
5
+ ### Features
6
+ - **A2B Overlay Integration**: Implemented a robust connection to the A2B Overlay API
7
+ - Updated `a2b_discover` tool to search for on-chain MCP servers and agents
8
+ - Enhanced search capabilities with relevance-based result ranking
9
+ - User-friendly formatted output with command suggestions
10
+ - Support for filtering by type (agent/tool/all), block range, and free text search
11
+ - **Improved MCP Server Publishing**: Enhanced the wallet_a2bPublishMcp tool
12
+ - Better identity key integration via LocalSigner
13
+ - More robust configuration for sigma signing
14
+ - Improved transaction handling and error reporting
15
+
16
+ ### Technical Improvements
17
+ - Updated API endpoint to use production overlay service
18
+ - Implemented enhanced search for better relevance scoring
19
+ - Better error handling for API responses
20
+ - Improved response formatting for readability
21
+ - Updated type definitions for consistency
22
+
3
23
  ## v0.0.34 - Transaction Broadcast Control
4
24
 
5
25
  ### Features
package/index.ts CHANGED
@@ -73,7 +73,7 @@ function initializePrivateKey(): PrivateKey | undefined {
73
73
  const privKey = initializePrivateKey();
74
74
 
75
75
  const server = new McpServer(
76
- { name: "Bitcoin SV", version: "0.0.34" },
76
+ { name: "Bitcoin SV", version: "0.0.35" },
77
77
  // {
78
78
  // // Advertise only what you actually implement
79
79
  // capabilities: {
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "bsv-mcp",
3
3
  "module": "index.ts",
4
4
  "type": "module",
5
- "version": "0.0.34",
5
+ "version": "0.0.35",
6
6
  "license": "MIT",
7
7
  "author": "satchmo",
8
8
  "description": "A collection of Bitcoin SV (BSV) tools for the Model Context Protocol (MCP) framework",
@@ -14,11 +14,12 @@
14
14
  "bitcoin",
15
15
  "bsv",
16
16
  "bitcoin-sv",
17
- "mcp",
18
- "model-context-protocol",
19
17
  "wallet",
20
18
  "ordinals",
21
- "blockchain"
19
+ "blockchain",
20
+ "1sat-ordinals",
21
+ "explorer",
22
+ "block explorer"
22
23
  ],
23
24
  "files": [
24
25
  "package.json",
@@ -0,0 +1,242 @@
1
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+
3
+ // Main JungleBus API documentation URL
4
+ const JUNGLEBUS_DOCS_URL = "https://junglebus.gorillapool.io/docs/";
5
+
6
+ /**
7
+ * Manually structured JungleBus API documentation
8
+ * Provides a clean summary of key JungleBus API endpoints and functionality
9
+ */
10
+ export function getJungleBusDocumentation(): string {
11
+ return `# JungleBus API Documentation
12
+
13
+ ## Overview
14
+
15
+ JungleBus is a transaction monitoring service for Bitcoin SV that allows applications to subscribe to transaction events and filter them based on specific criteria. The API provides both subscription and querying capabilities.
16
+
17
+ ## API Endpoints
18
+
19
+ ### Base URL
20
+ \`\`\`
21
+ https://junglebus.gorillapool.io/v1
22
+ \`\`\`
23
+
24
+ ### Transaction API
25
+
26
+ #### Get Transaction by ID
27
+ \`\`\`
28
+ GET /transaction/get/{txid}
29
+ \`\`\`
30
+
31
+ Returns detailed information about a specific transaction, including:
32
+ - Transaction data (hex format)
33
+ - Block information (hash, height, time)
34
+ - Input and output details
35
+ - Address information
36
+
37
+ #### Get Transactions by Block Hash
38
+ \`\`\`
39
+ GET /block/{blockhash}/transactions
40
+ \`\`\`
41
+
42
+ Returns all transactions within a specific block.
43
+
44
+ ### Subscription API
45
+
46
+ #### Create Subscription
47
+ \`\`\`
48
+ POST /subscribe
49
+ \`\`\`
50
+
51
+ Create a new subscription to monitor transactions based on filtering criteria.
52
+
53
+ Example request body:
54
+ \`\`\`json
55
+ {
56
+ "callback": "https://your-callback-url.com",
57
+ "fromBlock": 0,
58
+ "query": {
59
+ "find": {
60
+ "out.tape.cell.s": "BEEF"
61
+ }
62
+ }
63
+ }
64
+ \`\`\`
65
+
66
+ #### Delete Subscription
67
+ \`\`\`
68
+ DELETE /subscribe/{id}
69
+ \`\`\`
70
+
71
+ Deletes an existing subscription.
72
+
73
+ ### Network API
74
+
75
+ #### Get Network Info
76
+ \`\`\`
77
+ GET /network/info
78
+ \`\`\`
79
+
80
+ Returns current blockchain network information, including block height and other statistics.
81
+
82
+ ## Client Implementations
83
+
84
+ ### TypeScript Client
85
+
86
+ Installation:
87
+ \`\`\`bash
88
+ $ npm install @gorillapool/js-junglebus
89
+ \`\`\`
90
+
91
+ Usage:
92
+ \`\`\`javascript
93
+ import { JungleBusClient } from '@gorillapool/js-junglebus';
94
+
95
+ const server = "junglebus.gorillapool.io";
96
+ const jungleBusClient = new JungleBusClient(server, {
97
+ onConnected(ctx) {
98
+ // add your own code here
99
+ console.log(ctx);
100
+ },
101
+ onConnecting(ctx) {
102
+ // add your own code here
103
+ console.log(ctx);
104
+ },
105
+ onDisconnected(ctx) {
106
+ // add your own code here
107
+ console.log(ctx);
108
+ },
109
+ onError(ctx) {
110
+ // add your own code here
111
+ console.error(ctx);
112
+ }
113
+ });
114
+
115
+ // create subscriptions in the dashboard of the JungleBus website
116
+ const subId = "...."; // fill in the ID for the subscription
117
+ const fromBlock = 750000;
118
+
119
+ const subscription = jungleBusClient.Subscribe(
120
+ subId,
121
+ fromBlock,
122
+ onPublish(tx) => {
123
+ // add your own code here
124
+ console.log(tx);
125
+ },
126
+ onStatus(ctx) => {
127
+ // add your own code here
128
+ console.log(ctx);
129
+ },
130
+ onError(ctx) => {
131
+ // add your own code here
132
+ console.log(ctx);
133
+ },
134
+ onMempool(tx) => {
135
+ // add your own code here
136
+ console.log(tx);
137
+ }
138
+ );
139
+
140
+ // For lite mode (transaction hash and block height only)
141
+ await client.Subscribe("a5e2fa655c41753331539a2a86546bf9335ff6d9b7a512dc9acddb00ab9985c0", 1550000, onPublish, onStatus, onError, onMempool, true);
142
+ \`\`\`
143
+
144
+ ### Go Client
145
+
146
+ Installation:
147
+ \`\`\`bash
148
+ go get github.com/GorillaPool/go-junglebus
149
+ \`\`\`
150
+
151
+ Usage:
152
+ \`\`\`go
153
+ package main
154
+
155
+ import (
156
+ "context"
157
+ "log"
158
+ "sync"
159
+ "github.com/GorillaPool/go-junglebus"
160
+ "github.com/GorillaPool/go-junglebus/models"
161
+ )
162
+
163
+ func main() {
164
+ wg := &sync.WaitGroup{}
165
+
166
+ junglebusClient, err := junglebus.New(
167
+ junglebus.WithHTTP("https://junglebus.gorillapool.io"),
168
+ )
169
+ if err != nil {
170
+ log.Fatalln(err.Error())
171
+ }
172
+
173
+ subscriptionID := "..." // fill in the ID for the subscription
174
+ fromBlock := uint64(750000)
175
+
176
+ eventHandler := junglebus.EventHandler{
177
+ // do not set this function to leave out mined transactions
178
+ OnTransaction: func(tx *models.TransactionResponse) {
179
+ log.Printf("[TX]: %d: %v", tx.BlockHeight, tx.Id)
180
+ },
181
+ // do not set this function to leave out mempool transactions
182
+ OnMempool: func(tx *models.TransactionResponse) {
183
+ log.Printf("[MEMPOOL TX]: %v", tx.Id)
184
+ },
185
+ OnStatus: func(status *models.ControlResponse) {
186
+ log.Printf("[STATUS]: %v", status)
187
+ },
188
+ OnError: func(err error) {
189
+ log.Printf("[ERROR]: %v", err)
190
+ },
191
+ }
192
+
193
+ var subscription *junglebus.Subscription
194
+ if subscription, err = junglebusClient.Subscribe(context.Background(), subscriptionID, fromBlock, eventHandler); err != nil {
195
+ log.Printf("ERROR: failed getting subscription %s", err.Error())
196
+ }
197
+
198
+ // For lite mode
199
+ if subscription, err := junglebusClient.SubscribeWithQueue(context.Background(), subscriptionID, fromBlock, 0, eventHandler, &junglebus.SubscribeOptions{
200
+ QueueSize: 100000,
201
+ LiteMode: true,
202
+ }); err != nil {
203
+ log.Printf("ERROR: failed getting subscription %s", err.Error())
204
+ }
205
+
206
+ wg.Add(1)
207
+ wg.Wait()
208
+ }
209
+ \`\`\`
210
+
211
+ ## Further Reading
212
+
213
+ For complete API documentation, visit [JungleBus Docs](${JUNGLEBUS_DOCS_URL})
214
+ `;
215
+ }
216
+
217
+ /**
218
+ * Register the JungleBus API documentation resource with the MCP server
219
+ * @param server The MCP server instance
220
+ */
221
+ export function registerJungleBusResource(server: McpServer): void {
222
+ server.resource(
223
+ "junglebus-api-docs",
224
+ JUNGLEBUS_DOCS_URL,
225
+ {
226
+ title: "JungleBus API Documentation",
227
+ description:
228
+ "API documentation for JungleBus, a transaction monitoring service for Bitcoin SV",
229
+ },
230
+ async (uri) => {
231
+ const documentationContent = getJungleBusDocumentation();
232
+ return {
233
+ contents: [
234
+ {
235
+ uri: uri.href,
236
+ text: documentationContent,
237
+ },
238
+ ],
239
+ };
240
+ },
241
+ );
242
+ }
@@ -1,6 +1,7 @@
1
1
  import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
2
  import { registerBRCsResources } from "./brcs";
3
3
  import { registerChangelogResource } from "./changelog";
4
+ import { registerJungleBusResource } from "./junglebus";
4
5
 
5
6
  /**
6
7
  * Register all resources with the MCP server
@@ -13,5 +14,8 @@ export function registerResources(server: McpServer): void {
13
14
  // Register changelog resource
14
15
  registerChangelogResource(server);
15
16
 
17
+ // Register JungleBus API documentation resource
18
+ registerJungleBusResource(server);
19
+
16
20
  // Add more resource categories here as needed
17
21
  }
@@ -6,25 +6,34 @@ import type {
6
6
  } from "@modelcontextprotocol/sdk/types.js";
7
7
  import { z } from "zod";
8
8
 
9
- type OverlayRequest = {
9
+ // API endpoint for the A2B Overlay service
10
+ const OVERLAY_API_URL = "https://a2b-overlay-production.up.railway.app/v1";
11
+
12
+ type A2BDiscoveryItem = {
13
+ txid: string;
14
+ outpoint: string;
10
15
  type: "agent" | "tool";
11
- query: string;
12
- limit: number;
13
- offset: number;
14
- fromBlock?: number;
15
- toBlock?: number;
16
+ app: string;
17
+ serverName: string;
18
+ command: string;
19
+ description: string;
20
+ keywords: string[];
21
+ args: Record<string, string>;
22
+ env: Record<string, string>;
23
+ blockHeight: number;
24
+ timestamp: string;
25
+ tools?: string[];
26
+ prompts?: string[];
27
+ resources?: string[];
16
28
  };
17
29
 
18
- type OverlayResponse = {
19
- agents: {
20
- name: string;
21
- description: string;
22
- capabilities: string[];
23
- }[];
24
- tools: {
25
- name: string;
26
- description: string;
27
- }[];
30
+ type OverlaySearchResponse = {
31
+ items: A2BDiscoveryItem[];
32
+ total: number;
33
+ limit: number;
34
+ offset: number;
35
+ queryType: "agent" | "tool" | "all";
36
+ query: string;
28
37
  };
29
38
 
30
39
  // Schema for agent discovery parameters
@@ -38,6 +47,73 @@ export const a2bDiscoverArgsSchema = z.object({
38
47
  });
39
48
  export type A2bDiscoverArgs = z.infer<typeof a2bDiscoverArgsSchema>;
40
49
 
50
+ /**
51
+ * Format the response in a user-friendly way
52
+ */
53
+ function formatSearchResults(data: unknown, queryType: string): string {
54
+ // console.log(`Received data: ${JSON.stringify(data).substring(0, 200)}...`); // Debug log
55
+
56
+ // Check if data is an object with items property
57
+ if (!data) {
58
+ return `No ${queryType} results found.`;
59
+ }
60
+
61
+ // Handle the new API format where data is an object with 'items' array
62
+ const items = Array.isArray(data)
63
+ ? data
64
+ : (data as { items?: A2BDiscoveryItem[] }).items;
65
+
66
+ // Ensure items is an array
67
+ if (!items || !Array.isArray(items) || items.length === 0) {
68
+ return `No ${queryType} results found.`;
69
+ }
70
+
71
+ let result = `Found ${items.length} ${queryType === "all" ? "items" : `${queryType}s`}:\n\n`;
72
+
73
+ items.forEach((item, index) => {
74
+ if (!item) return;
75
+
76
+ // Agent or MCP server name with description
77
+ result += `${index + 1}. **${item.serverName || "Unknown"}** - ${item.description || "No description"}\n`;
78
+
79
+ // Display command to run
80
+ if (item.command) {
81
+ const args = item.args ? Object.values(item.args).join(" ") : "";
82
+ result += ` Command: \`${item.command} ${args}\`\n`;
83
+ }
84
+
85
+ // Add tools count if available
86
+ if (item.tools && Array.isArray(item.tools)) {
87
+ result += ` Tools: ${item.tools.length} available\n`;
88
+ }
89
+
90
+ // Add keywords if available
91
+ if (
92
+ item.keywords &&
93
+ Array.isArray(item.keywords) &&
94
+ item.keywords.length > 0
95
+ ) {
96
+ result += ` Keywords: ${item.keywords.join(", ")}\n`;
97
+ }
98
+
99
+ // Add blockchain details
100
+ if (item.outpoint) {
101
+ result += ` Outpoint: ${item.outpoint}\n`;
102
+ }
103
+
104
+ if (item.blockHeight !== undefined) {
105
+ const date = item.timestamp
106
+ ? new Date(item.timestamp).toLocaleDateString()
107
+ : "Unknown date";
108
+ result += ` Block: ${item.blockHeight}, ${date}\n`;
109
+ }
110
+
111
+ result += "\n";
112
+ });
113
+
114
+ return result;
115
+ }
116
+
41
117
  /**
42
118
  * Registers the a2b_discover tool for on-chain agent discovery
43
119
  */
@@ -50,49 +126,113 @@ export function registerA2bDiscoverTool(server: McpServer) {
50
126
  { args }: { args: A2bDiscoverArgs },
51
127
  extra: RequestHandlerExtra<ServerRequest, ServerNotification>,
52
128
  ) => {
53
- if (args.queryType === "agent") {
54
- return {
55
- content: [
56
- { type: "text", text: "Agent discovery is not supported yet" },
57
- ],
58
- isError: true,
59
- };
60
- }
61
-
62
- if (args.queryType !== "tool") {
63
- return {
64
- content: [
65
- {
66
- type: "text",
67
- text: "Only tool discovery is supported currently",
68
- },
69
- ],
70
- isError: true,
71
- };
72
- }
73
-
74
129
  try {
75
130
  const params = new URLSearchParams();
131
+
132
+ // Set query type (agent, tool, or all)
76
133
  params.set("type", args.queryType);
77
- params.set("query", args.query);
78
- params.set("limit", args.limit?.toString() ?? "5");
134
+
135
+ // Use enhanced search for better relevance scoring
136
+ let searchEndpoint = "/search/enhanced";
137
+
138
+ // For empty queries, use the regular search endpoint
139
+ if (!args.query || !args.query.trim()) {
140
+ searchEndpoint = "/search";
141
+ } else {
142
+ params.set("q", args.query); // enhanced search uses 'q' parameter
143
+ }
144
+
145
+ // Add pagination parameters
146
+ params.set("limit", args.limit?.toString() ?? "10");
79
147
  params.set("offset", args.offset?.toString() ?? "0");
148
+
149
+ // Add block range if specified
80
150
  if (args.fromBlock) {
81
151
  params.set("fromBlock", args.fromBlock.toString());
82
152
  }
83
153
  if (args.toBlock) {
84
154
  params.set("toBlock", args.toBlock.toString());
85
155
  }
86
- const OVERLAY_URL = `https://overlay.a2b.network/v1/search?${params.toString()}`;
87
- const response = await fetch(OVERLAY_URL);
88
- const data = (await response.json()) as OverlayResponse;
156
+
157
+ // Construct the full URL
158
+ const searchUrl = `${OVERLAY_API_URL}${searchEndpoint}?${params.toString()}`;
159
+ //console.log(`Searching URL: ${searchUrl}`);
160
+
161
+ // Make the request to the overlay API
162
+ const response = await fetch(searchUrl, {
163
+ method: "GET",
164
+ headers: {
165
+ Accept: "application/json",
166
+ },
167
+ });
168
+
169
+ if (!response.ok) {
170
+ throw new Error(
171
+ `API returned status ${response.status}: ${response.statusText}`,
172
+ );
173
+ }
174
+
175
+ const data = (await response.json()) as OverlaySearchResponse;
176
+
177
+ // Format the results for better readability
178
+ let result = "";
179
+
180
+ if (data?.items?.length > 0) {
181
+ result = `Found ${data.items.length} ${args.queryType}(s):\n\n`;
182
+
183
+ data.items.forEach((item: A2BDiscoveryItem, index: number) => {
184
+ // Server name and description
185
+ result += `${index + 1}. **${item.serverName || "Unknown"}** - ${item.description || "No description"}\n`;
186
+
187
+ // Command to run
188
+ if (item.command) {
189
+ const cmdArgs = item.args
190
+ ? Object.values(item.args).join(" ")
191
+ : "";
192
+ result += ` Command: \`${item.command} ${cmdArgs}\`\n`;
193
+ }
194
+
195
+ // Tools available
196
+ if (item.tools?.length) {
197
+ result += ` Tools: ${item.tools.length} available\n`;
198
+ }
199
+
200
+ // Keywords
201
+ if (item.keywords?.length) {
202
+ result += ` Keywords: ${item.keywords.join(", ")}\n`;
203
+ }
204
+
205
+ // Blockchain details
206
+ if (item.outpoint) {
207
+ result += ` Outpoint: ${item.outpoint}\n`;
208
+ }
209
+
210
+ if (item.blockHeight !== undefined) {
211
+ const date = item.timestamp
212
+ ? new Date(item.timestamp).toLocaleDateString()
213
+ : "Unknown date";
214
+ result += ` Block: ${item.blockHeight}, ${date}\n`;
215
+ }
216
+
217
+ result += "\n";
218
+ });
219
+ } else {
220
+ result = `No ${args.queryType} results found.`;
221
+ }
222
+
89
223
  return {
90
- content: [{ type: "text", text: JSON.stringify(data) }],
224
+ content: [{ type: "text", text: result }],
91
225
  isError: false,
92
226
  };
93
227
  } catch (error) {
228
+ console.error("Search error:", error);
94
229
  return {
95
- content: [{ type: "text", text: `Error querying overlay: ${error}` }],
230
+ content: [
231
+ {
232
+ type: "text",
233
+ text: `Error querying A2B Overlay: ${error instanceof Error ? error.message : String(error)}`,
234
+ },
235
+ ],
96
236
  isError: true,
97
237
  };
98
238
  }
@@ -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