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 +20 -0
- package/index.ts +1 -1
- package/package.json +5 -4
- package/resources/junglebus.ts +242 -0
- package/resources/resources.ts +4 -0
- package/tools/a2b/discover.ts +184 -44
- package/tools/wallet/a2bPublishMcp.ts +163 -15
- package/tools/wallet/fetchPaymentUtxos.ts +130 -0
- package/tools/wallet/refreshUtxos.ts +76 -0
- package/tools/wallet/tools.ts +5 -0
- package/tools/wallet/wallet.ts +22 -17
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.
|
|
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.
|
|
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
|
+
}
|
package/resources/resources.ts
CHANGED
|
@@ -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
|
}
|
package/tools/a2b/discover.ts
CHANGED
|
@@ -6,25 +6,34 @@ import type {
|
|
|
6
6
|
} from "@modelcontextprotocol/sdk/types.js";
|
|
7
7
|
import { z } from "zod";
|
|
8
8
|
|
|
9
|
-
|
|
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
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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
|
-
|
|
78
|
-
|
|
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
|
-
|
|
87
|
-
|
|
88
|
-
const
|
|
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:
|
|
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: [
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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 = `${
|
|
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
|
|
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:
|
|
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
|
+
}
|
package/tools/wallet/tools.ts
CHANGED
|
@@ -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
|
package/tools/wallet/wallet.ts
CHANGED
|
@@ -47,12 +47,8 @@ import type {
|
|
|
47
47
|
WalletCertificate,
|
|
48
48
|
WalletInterface,
|
|
49
49
|
} from "@bsv/sdk";
|
|
50
|
-
import {
|
|
51
|
-
|
|
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
|
-
|
|
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
|
-
|
|
89
|
-
|
|
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
|
-
|
|
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
|
-
|
|
95
|
-
|
|
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
|
-
|
|
103
|
+
// Keep existing UTXOs, don't clear them on error
|
|
98
104
|
}
|
|
99
105
|
} catch (error) {
|
|
100
|
-
|
|
101
|
-
throw error;
|
|
106
|
+
// Silent global error, preserve existing UTXOs
|
|
102
107
|
}
|
|
103
108
|
}
|
|
104
109
|
|