#
tokens: 45232/50000 12/59 files (page 2/2)
lines: on (toggle) GitHub
raw markdown copy reset
This is page 2 of 2. Use http://codebase.md/b-open-io/bsv-mcp?lines=true&page={x} to view the full context.

# Directory Structure

```
├── .gitignore
├── biome.json
├── bun.lock
├── CHANGELOG.md
├── Dockerfile
├── docs
│   ├── a2b.md
│   └── images
│       └── mcp-config-example.png
├── index.ts
├── LICENSE
├── package.json
├── prompts
│   ├── bsvSdk
│   │   ├── auth.ts
│   │   ├── cryptography.ts
│   │   ├── index.ts
│   │   ├── overview.ts
│   │   ├── primitives.ts
│   │   ├── script.ts
│   │   ├── transaction.ts
│   │   └── wallet.ts
│   ├── index.ts
│   └── ordinals.ts
├── README.md
├── resources
│   ├── brcs.ts
│   ├── changelog.ts
│   ├── junglebus.ts
│   └── resources.ts
├── smithery.yaml
├── tools
│   ├── a2b
│   │   ├── call.ts
│   │   └── discover.ts
│   ├── bsv
│   │   ├── decodeTransaction.ts
│   │   ├── explore.ts
│   │   ├── getPrice.ts
│   │   ├── index.ts
│   │   └── token.ts
│   ├── constants.ts
│   ├── index.ts
│   ├── mnee
│   │   ├── getBalance.ts
│   │   ├── index.ts
│   │   ├── parseTx.ts
│   │   └── sendMnee.ts
│   ├── ordinals
│   │   ├── getInscription.ts
│   │   ├── getTokenByIdOrTicker.ts
│   │   ├── index.ts
│   │   ├── marketListings.ts
│   │   ├── marketSales.ts
│   │   └── searchInscriptions.ts
│   ├── utils
│   │   ├── conversion.ts
│   │   └── index.ts
│   └── wallet
│       ├── a2bPublishAgent.ts
│       ├── a2bPublishMcp.ts
│       ├── createOrdinals.ts
│       ├── fetchPaymentUtxos.ts
│       ├── getAddress.ts
│       ├── getPublicKey.ts
│       ├── purchaseListing.ts
│       ├── refreshUtxos.ts
│       ├── schemas.ts
│       ├── sendOrdinals.ts
│       ├── sendToAddress.ts
│       ├── tools.ts
│       ├── transferOrdToken.ts
│       └── wallet.ts
└── tsconfig.json
```

# Files

--------------------------------------------------------------------------------
/prompts/bsvSdk/transaction.ts:
--------------------------------------------------------------------------------

```typescript
  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 | 
  8 | /**
  9 |  * BSV SDK Transaction Prompt
 10 |  *
 11 |  * Provides detailed information about transaction building and management
 12 |  * in the BSV SDK, including input/output handling, script integration, and transaction signing.
 13 |  */
 14 | export const BSV_SDK_TRANSACTION_PROMPT = `
 15 | # BSV SDK - Transaction Module
 16 | 
 17 | The Transaction module in the BSV SDK provides comprehensive functionality for creating, manipulating, and signing Bitcoin transactions. It gives developers fine-grained control over transaction construction while abstracting many of the complexities.
 18 | 
 19 | ## Key Components
 20 | 
 21 | ### Transaction Class
 22 | 
 23 | The core \`Transaction\` class represents a Bitcoin transaction and provides methods for manipulating its components:
 24 | 
 25 | \`\`\`typescript
 26 | import { Transaction, PrivateKey, LockingScript } from "@bsv/sdk";
 27 | 
 28 | // Create a new transaction
 29 | const tx = new Transaction();
 30 | 
 31 | // Set transaction properties
 32 | tx.version = 1;
 33 | tx.lockTime = 0;
 34 | \`\`\`
 35 | 
 36 | ## Building Transactions
 37 | 
 38 | ### Adding Inputs
 39 | 
 40 | \`\`\`typescript
 41 | // Add an input by specifying the source transaction and output index
 42 | tx.addInput({
 43 |   sourceTXID: "previous_transaction_id_in_hex",
 44 |   sourceOutputIndex: 0,
 45 |   sequence: 0xffffffff // Optional, defaults to max value
 46 | });
 47 | 
 48 | // Add multiple inputs
 49 | const inputs = [
 50 |   { sourceTXID: "txid1", sourceOutputIndex: 0 },
 51 |   { sourceTXID: "txid2", sourceOutputIndex: 1 }
 52 | ];
 53 | inputs.forEach(input => tx.addInput(input));
 54 | \`\`\`
 55 | 
 56 | ### Adding Outputs
 57 | 
 58 | \`\`\`typescript
 59 | // Add an output with a locking script and amount
 60 | import { LockingScript } from "@bsv/sdk";
 61 | 
 62 | // Create from a Bitcoin address
 63 | const lockingScript = LockingScript.fromAddress("recipient_address");
 64 | 
 65 | // Add the output to the transaction
 66 | tx.addOutput({
 67 |   lockingScript,
 68 |   satoshis: 5000 // Amount in satoshis
 69 | });
 70 | 
 71 | // Add a data (OP_RETURN) output
 72 | const dataScript = LockingScript.fromData(Buffer.from("Hello, Bitcoin!"));
 73 | tx.addOutput({
 74 |   lockingScript: dataScript,
 75 |   satoshis: 0 // OP_RETURN outputs typically have 0 value
 76 | });
 77 | \`\`\`
 78 | 
 79 | ### Working with UTXOs
 80 | 
 81 | When building transactions with existing UTXOs:
 82 | 
 83 | \`\`\`typescript
 84 | import { UnlockingScript } from "@bsv/sdk";
 85 | 
 86 | // Example UTXO data
 87 | const utxos = [
 88 |   {
 89 |     txid: "previous_tx_id_in_hex",
 90 |     vout: 0,
 91 |     satoshis: 10000,
 92 |     scriptPubKey: "locking_script_hex"
 93 |   }
 94 | ];
 95 | 
 96 | // Create transaction using UTXOs
 97 | const tx = new Transaction();
 98 | 
 99 | // Add input from UTXO
100 | utxos.forEach(utxo => {
101 |   tx.addInput({
102 |     sourceTXID: utxo.txid,
103 |     sourceOutputIndex: utxo.vout
104 |   });
105 | });
106 | 
107 | // Add output with recipient address
108 | tx.addOutput({
109 |   lockingScript: LockingScript.fromAddress("recipient_address"),
110 |   satoshis: 9000 // Sending 9000 satoshis (10000 - 1000 fee)
111 | });
112 | \`\`\`
113 | 
114 | ## Signing Transactions
115 | 
116 | ### Basic Transaction Signing
117 | 
118 | \`\`\`typescript
119 | import { PrivateKey, SigningConfig, Utils } from "@bsv/sdk";
120 | 
121 | // Create a private key
122 | const privateKey = PrivateKey.fromWif("your_private_key_wif");
123 | 
124 | // Sign a specific input
125 | const inputIndex = 0;
126 | const signingConfig: SigningConfig = {
127 |   privateKey,
128 |   lockingScript: LockingScript.fromAddress(privateKey.toAddress()),
129 |   satoshis: 10000, // Original amount in the UTXO
130 |   inputIndex,
131 |   sigHashType: Utils.SIGHASH_ALL | Utils.SIGHASH_FORKID // Standard signing algorithm
132 | };
133 | 
134 | // Apply the signature to the transaction
135 | tx.sign(signingConfig);
136 | \`\`\`
137 | 
138 | ### Signing Multiple Inputs
139 | 
140 | \`\`\`typescript
141 | // Sign multiple inputs with different keys
142 | const keys = [privateKey1, privateKey2];
143 | const utxos = [utxo1, utxo2];
144 | 
145 | utxos.forEach((utxo, index) => {
146 |   const signingConfig = {
147 |     privateKey: keys[index],
148 |     lockingScript: LockingScript.fromHex(utxo.scriptPubKey),
149 |     satoshis: utxo.satoshis,
150 |     inputIndex: index,
151 |     sigHashType: Utils.SIGHASH_ALL | Utils.SIGHASH_FORKID
152 |   };
153 |   
154 |   tx.sign(signingConfig);
155 | });
156 | \`\`\`
157 | 
158 | ## Transaction Serialization
159 | 
160 | \`\`\`typescript
161 | // Convert transaction to binary format
162 | const txBinary = tx.toBinary();
163 | 
164 | // Convert to hex string
165 | const txHex = tx.toHex();
166 | 
167 | // Get transaction ID
168 | const txid = tx.hash("hex");
169 | 
170 | // Parse an existing transaction
171 | const parsedTx = Transaction.fromHex("transaction_hex_string");
172 | \`\`\`
173 | 
174 | ## Fee Calculation
175 | 
176 | \`\`\`typescript
177 | // Manual fee calculation based on transaction size
178 | const txSize = tx.toBinary().length;
179 | const feeRate = 0.5; // satoshis per byte
180 | const fee = Math.ceil(txSize * feeRate);
181 | 
182 | // Adjust output amount to include fee
183 | outputAmount = inputAmount - fee;
184 | \`\`\`
185 | 
186 | ## Advanced Transaction Features
187 | 
188 | ### Time Locks
189 | 
190 | \`\`\`typescript
191 | // Set absolute locktime (by block height)
192 | tx.lockTime = 700000; // Transaction can't be mined until block 700000
193 | 
194 | // Set relative locktime using sequence number (BIP 68)
195 | const sequenceForBlocks = (blocks) => 0xffffffff - blocks;
196 | tx.inputs[0].sequence = sequenceForBlocks(10); // Locked for 10 blocks
197 | \`\`\`
198 | 
199 | ### Custom Scripts
200 | 
201 | \`\`\`typescript
202 | import { Script, OpCodes } from "@bsv/sdk";
203 | 
204 | // Create a custom script
205 | const customScript = new Script();
206 | customScript.add(OpCodes.OP_DUP);
207 | customScript.add(OpCodes.OP_HASH160);
208 | customScript.add(Buffer.from("public_key_hash", "hex"));
209 | customScript.add(OpCodes.OP_EQUALVERIFY);
210 | customScript.add(OpCodes.OP_CHECKSIG);
211 | 
212 | // Create a locking script from the custom script
213 | const customLockingScript = LockingScript.fromScript(customScript);
214 | \`\`\`
215 | 
216 | ## Best Practices
217 | 
218 | 1. **Fee Management**: Calculate appropriate fees based on transaction size and network conditions
219 | 2. **Input/Output Management**: Properly track inputs and outputs to avoid double-spending
220 | 3. **Change Handling**: Always account for change when not spending the full UTXO amount
221 | 4. **Testing**: Test transactions on testnet before deploying to mainnet
222 | 5. **Error Handling**: Implement proper error handling for transaction building and signing
223 | 
224 | For complete API documentation and additional transaction features, refer to the official BSV SDK documentation.
225 | `;
226 | 
227 | /**
228 |  * Register the BSV SDK Transaction prompt with the MCP server
229 |  * @param server The MCP server instance
230 |  */
231 | export function registerTransactionPrompt(server: McpServer): void {
232 | 	server.prompt(
233 | 		"bitcoin_sv_sdk_transaction",
234 | 		"Detailed information about transaction building and management in the BSV SDK, including input/output handling, script integration, and transaction signing.",
235 | 		async (extra: RequestHandlerExtra<ServerRequest, ServerNotification>) => {
236 | 			return {
237 | 				messages: [
238 | 					{
239 | 						role: "assistant",
240 | 						content: {
241 | 							type: "text",
242 | 							text: BSV_SDK_TRANSACTION_PROMPT,
243 | 						},
244 | 					},
245 | 				],
246 | 			};
247 | 		},
248 | 	);
249 | }
250 | 
```

--------------------------------------------------------------------------------
/tools/a2b/discover.ts:
--------------------------------------------------------------------------------

```typescript
  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 { z } from "zod";
  8 | 
  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;
 15 | 	type: "agent" | "tool";
 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[];
 28 | };
 29 | 
 30 | type OverlaySearchResponse = {
 31 | 	items: A2BDiscoveryItem[];
 32 | 	total: number;
 33 | 	limit: number;
 34 | 	offset: number;
 35 | 	queryType: "agent" | "tool" | "all";
 36 | 	query: string;
 37 | };
 38 | 
 39 | // Schema for agent discovery parameters
 40 | export const a2bDiscoverArgsSchema = z.object({
 41 | 	queryType: z.enum(["agent", "tool"]).describe("Type of discovery to perform"),
 42 | 	query: z.string().describe("Search agent or tool names, descriptions"),
 43 | 	limit: z.number().optional().describe("Limit the number of results"),
 44 | 	offset: z.number().optional().describe("Offset the results"),
 45 | 	fromBlock: z.number().optional().describe("From block"),
 46 | 	toBlock: z.number().optional().describe("To block"),
 47 | });
 48 | export type A2bDiscoverArgs = z.infer<typeof a2bDiscoverArgsSchema>;
 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 | 
117 | /**
118 |  * Registers the a2b_discover tool for on-chain agent discovery
119 |  */
120 | export function registerA2bDiscoverTool(server: McpServer) {
121 | 	server.tool(
122 | 		"a2b_discover",
123 | 		"Search on-chain agent and MCP tool records. Use 'agent' to search for agents, 'tool' to search for MCP tools.",
124 | 		{ args: a2bDiscoverArgsSchema },
125 | 		async (
126 | 			{ args }: { args: A2bDiscoverArgs },
127 | 			extra: RequestHandlerExtra<ServerRequest, ServerNotification>,
128 | 		) => {
129 | 			try {
130 | 				const params = new URLSearchParams();
131 | 
132 | 				// Set query type (agent, tool, or all)
133 | 				params.set("type", args.queryType);
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");
147 | 				params.set("offset", args.offset?.toString() ?? "0");
148 | 
149 | 				// Add block range if specified
150 | 				if (args.fromBlock) {
151 | 					params.set("fromBlock", args.fromBlock.toString());
152 | 				}
153 | 				if (args.toBlock) {
154 | 					params.set("toBlock", args.toBlock.toString());
155 | 				}
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 | 
223 | 				return {
224 | 					content: [{ type: "text", text: result }],
225 | 					isError: false,
226 | 				};
227 | 			} catch (error) {
228 | 				console.error("Search error:", error);
229 | 				return {
230 | 					content: [
231 | 						{
232 | 							type: "text",
233 | 							text: `Error querying A2B Overlay: ${error instanceof Error ? error.message : String(error)}`,
234 | 						},
235 | 					],
236 | 					isError: true,
237 | 				};
238 | 			}
239 | 		},
240 | 	);
241 | }
242 | 
```

--------------------------------------------------------------------------------
/tools/bsv/decodeTransaction.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { Transaction, Utils } from "@bsv/sdk";
  2 | import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
  3 | import type { RequestHandlerExtra } from "@modelcontextprotocol/sdk/shared/protocol.js";
  4 | import { z } from "zod";
  5 | 
  6 | // Schema for decode transaction arguments
  7 | export const decodeTransactionArgsSchema = z.object({
  8 | 	tx: z.string().describe("Transaction data or txid"),
  9 | 	encoding: z
 10 | 		.enum(["hex", "base64"])
 11 | 		.default("hex")
 12 | 		.describe("Encoding of the input data"),
 13 | });
 14 | 
 15 | export type DecodeTransactionArgs = z.infer<typeof decodeTransactionArgsSchema>;
 16 | 
 17 | // Type for JungleBus API response
 18 | interface JungleBusTransactionResponse {
 19 | 	id: string;
 20 | 	transaction: string;
 21 | 	block_hash?: string;
 22 | 	block_height?: number;
 23 | 	block_time?: number;
 24 | 	block_index?: number;
 25 | 	addresses?: string[];
 26 | 	inputs?: string[];
 27 | 	outputs?: string[];
 28 | 	input_types?: string[];
 29 | 	output_types?: string[];
 30 | 	contexts?: string[];
 31 | 	sub_contexts?: string[];
 32 | 	data?: string[];
 33 | 	merkle_proof?: unknown;
 34 | }
 35 | 
 36 | // Network info response type
 37 | interface NetworkInfoResponse {
 38 | 	blocks: number;
 39 | 	[key: string]: unknown;
 40 | }
 41 | 
 42 | // Transaction input type
 43 | interface TransactionInputData {
 44 | 	txid: string | undefined;
 45 | 	vout: number;
 46 | 	sequence: number | undefined;
 47 | 	script: string;
 48 | 	scriptAsm: string;
 49 | 	type?: string;
 50 | 	value?: number;
 51 | }
 52 | 
 53 | // Transaction output type
 54 | interface TransactionOutputData {
 55 | 	n: number;
 56 | 	value: number | undefined;
 57 | 	scriptPubKey: {
 58 | 		hex: string;
 59 | 		asm: string;
 60 | 	};
 61 | 	type?: string;
 62 | }
 63 | 
 64 | // Transaction result type
 65 | interface TransactionResult {
 66 | 	txid: string;
 67 | 	version: number;
 68 | 	locktime: number;
 69 | 	size: number;
 70 | 	inputs: TransactionInputData[];
 71 | 	outputs: TransactionOutputData[];
 72 | 	confirmations?: number;
 73 | 	block?: {
 74 | 		hash: string;
 75 | 		height: number;
 76 | 		time: number;
 77 | 		index: number;
 78 | 	} | null;
 79 | 	addresses?: string[];
 80 | 	fee?: number | null;
 81 | 	feeRate?: number | null;
 82 | }
 83 | 
 84 | /**
 85 |  * Fetches transaction data from JungleBus
 86 |  */
 87 | async function fetchJungleBusData(
 88 | 	txid: string,
 89 | ): Promise<JungleBusTransactionResponse | null> {
 90 | 	try {
 91 | 		const response = await fetch(
 92 | 			`https://junglebus.gorillapool.io/v1/transaction/get/${txid}`,
 93 | 		);
 94 | 		if (!response.ok) {
 95 | 			console.error(
 96 | 				`JungleBus API error: ${response.status} ${response.statusText}`,
 97 | 			);
 98 | 			return null;
 99 | 		}
100 | 		return (await response.json()) as JungleBusTransactionResponse;
101 | 	} catch (error) {
102 | 		console.error("Error fetching from JungleBus:", error);
103 | 		return null;
104 | 	}
105 | }
106 | 
107 | /**
108 |  * Determines if a string is likely a txid
109 |  */
110 | function isTxid(str: string): boolean {
111 | 	// TX IDs are 64 characters in hex (32 bytes)
112 | 	return /^[0-9a-f]{64}$/i.test(str);
113 | }
114 | 
115 | /**
116 |  * Register the BSV transaction decode tool
117 |  */
118 | export function registerDecodeTransactionTool(server: McpServer): void {
119 | 	server.tool(
120 | 		"bsv_decodeTransaction",
121 | 		"Decodes and analyzes Bitcoin SV transactions to provide detailed insights. This powerful tool accepts either a transaction ID or raw transaction data and returns comprehensive information including inputs, outputs, fee calculations, script details, and blockchain context. Supports both hex and base64 encoded transactions and automatically fetches additional on-chain data when available.",
122 | 		{
123 | 			args: decodeTransactionArgsSchema,
124 | 		},
125 | 		async (
126 | 			{ args }: { args: DecodeTransactionArgs },
127 | 			extra: RequestHandlerExtra,
128 | 		) => {
129 | 			try {
130 | 				const { tx, encoding } = args;
131 | 				let transaction: Transaction;
132 | 				let rawTx: string;
133 | 				let txid: string;
134 | 				let junglebusData: JungleBusTransactionResponse | null = null;
135 | 
136 | 				// Check if input is txid or raw transaction
137 | 				if (isTxid(tx)) {
138 | 					// It's a txid, fetch from JungleBus
139 | 					txid = tx;
140 | 					junglebusData = await fetchJungleBusData(txid);
141 | 
142 | 					if (!junglebusData) {
143 | 						throw new Error(
144 | 							`Failed to fetch transaction data for txid: ${txid}`,
145 | 						);
146 | 					}
147 | 
148 | 					// JungleBus returns base64, convert if needed
149 | 					rawTx = junglebusData.transaction;
150 | 					// Check if rawTx is in base64 format (common from JungleBus)
151 | 					const isBase64 = /^[A-Za-z0-9+/=]+$/.test(rawTx);
152 | 
153 | 					if (isBase64) {
154 | 						const txBytes = Utils.toArray(rawTx, "base64");
155 | 						transaction = Transaction.fromBinary(txBytes);
156 | 					} else {
157 | 						transaction = Transaction.fromHex(rawTx);
158 | 					}
159 | 				} else {
160 | 					// It's a raw transaction
161 | 					let txBytes: number[];
162 | 
163 | 					if (encoding === "hex") {
164 | 						txBytes = Utils.toArray(tx, "hex");
165 | 					} else {
166 | 						txBytes = Utils.toArray(tx, "base64");
167 | 					}
168 | 
169 | 					transaction = Transaction.fromBinary(txBytes);
170 | 					txid = transaction.hash("hex") as string;
171 | 
172 | 					// Optionally fetch additional context from JungleBus
173 | 					junglebusData = await fetchJungleBusData(txid);
174 | 				}
175 | 
176 | 				// Basic transaction data
177 | 				const result: TransactionResult = {
178 | 					txid,
179 | 					version: transaction.version,
180 | 					locktime: transaction.lockTime,
181 | 					size: transaction.toBinary().length,
182 | 					inputs: transaction.inputs.map((input) => ({
183 | 						txid: input.sourceTXID,
184 | 						vout: input.sourceOutputIndex,
185 | 						sequence: input.sequence,
186 | 						script: input.unlockingScript ? input.unlockingScript.toHex() : "",
187 | 						scriptAsm: input.unlockingScript
188 | 							? input.unlockingScript.toASM()
189 | 							: "",
190 | 					})),
191 | 					outputs: transaction.outputs.map((output) => ({
192 | 						n: transaction.outputs.indexOf(output),
193 | 						value: output.satoshis,
194 | 						scriptPubKey: {
195 | 							hex: output.lockingScript.toHex(),
196 | 							asm: output.lockingScript.toASM(),
197 | 						},
198 | 					})),
199 | 				};
200 | 
201 | 				// Add JungleBus context if available
202 | 				if (junglebusData) {
203 | 					result.confirmations = junglebusData.block_height
204 | 						? (await getCurrentBlockHeight()) - junglebusData.block_height + 1
205 | 						: 0;
206 | 
207 | 					result.block = junglebusData.block_hash
208 | 						? {
209 | 								hash: junglebusData.block_hash,
210 | 								height: junglebusData.block_height || 0,
211 | 								time: junglebusData.block_time || 0,
212 | 								index: junglebusData.block_index || 0,
213 | 							}
214 | 						: null;
215 | 
216 | 					// Add script types
217 | 					if (
218 | 						junglebusData.input_types &&
219 | 						junglebusData.input_types.length > 0
220 | 					) {
221 | 						result.inputs = result.inputs.map((input, i) => ({
222 | 							...input,
223 | 							type: junglebusData.input_types?.[i] || "unknown",
224 | 						}));
225 | 					}
226 | 
227 | 					if (
228 | 						junglebusData.output_types &&
229 | 						junglebusData.output_types.length > 0
230 | 					) {
231 | 						result.outputs = result.outputs.map((output, i) => ({
232 | 							...output,
233 | 							type: junglebusData.output_types?.[i] || "unknown",
234 | 						}));
235 | 					}
236 | 
237 | 					// Add addresses found in transaction
238 | 					if (junglebusData.addresses && junglebusData.addresses.length > 0) {
239 | 						result.addresses = junglebusData.addresses;
240 | 					}
241 | 				}
242 | 
243 | 				// Calculate additional information
244 | 				const totalInputValue = result.inputs.reduce(
245 | 					(sum, input) => sum + (input.value || 0),
246 | 					0,
247 | 				);
248 | 				const totalOutputValue = result.outputs.reduce(
249 | 					(sum, output) => sum + (output.value || 0),
250 | 					0,
251 | 				);
252 | 
253 | 				result.fee =
254 | 					totalInputValue > 0 ? totalInputValue - totalOutputValue : null;
255 | 				result.feeRate =
256 | 					result.fee !== null
257 | 						? Math.round((result.fee / result.size) * 100000000) / 100000000
258 | 						: null;
259 | 
260 | 				return {
261 | 					content: [
262 | 						{
263 | 							type: "text",
264 | 							text: JSON.stringify(result, null, 2),
265 | 						},
266 | 					],
267 | 				};
268 | 			} catch (error) {
269 | 				return {
270 | 					content: [
271 | 						{
272 | 							type: "text",
273 | 							text: error instanceof Error ? error.message : String(error),
274 | 						},
275 | 					],
276 | 					isError: true,
277 | 				};
278 | 			}
279 | 		},
280 | 	);
281 | }
282 | 
283 | /**
284 |  * Get current block height
285 |  */
286 | async function getCurrentBlockHeight(): Promise<number> {
287 | 	try {
288 | 		const response = await fetch(
289 | 			"https://junglebus.gorillapool.io/v1/network/info",
290 | 		);
291 | 		if (!response.ok) {
292 | 			return 0;
293 | 		}
294 | 		const data = (await response.json()) as NetworkInfoResponse;
295 | 		return data.blocks || 0;
296 | 	} catch (error) {
297 | 		console.error("Error fetching current block height:", error);
298 | 		return 0;
299 | 	}
300 | }
301 | 
```

--------------------------------------------------------------------------------
/tools/wallet/wallet.ts:
--------------------------------------------------------------------------------

```typescript
  1 | /**
  2 |  * Wallet implementation scaffold.
  3 |  *
  4 |  * Implements WalletInterface from the ts-sdk and extends ProtoWallet.
  5 |  *
  6 |  * See: https://github.com/bitcoin-sv/ts-sdk/blob/main/src/wallet/Wallet.interfaces.ts
  7 |  */
  8 | import { LockingScript, PrivateKey, ProtoWallet, Transaction } from "@bsv/sdk";
  9 | import type {
 10 | 	AbortActionArgs,
 11 | 	AbortActionResult,
 12 | 	AcquireCertificateArgs,
 13 | 	AuthenticatedResult,
 14 | 	CreateActionArgs,
 15 | 	CreateActionResult,
 16 | 	DiscoverByAttributesArgs,
 17 | 	DiscoverByIdentityKeyArgs,
 18 | 	DiscoverCertificatesResult,
 19 | 	GetHeaderArgs,
 20 | 	GetHeaderResult,
 21 | 	GetHeightResult,
 22 | 	GetNetworkResult,
 23 | 	GetPublicKeyArgs,
 24 | 	GetPublicKeyResult,
 25 | 	GetVersionResult,
 26 | 	InternalizeActionArgs,
 27 | 	InternalizeActionResult,
 28 | 	ListActionsArgs,
 29 | 	ListActionsResult,
 30 | 	ListCertificatesArgs,
 31 | 	ListCertificatesResult,
 32 | 	ListOutputsArgs,
 33 | 	ListOutputsResult,
 34 | 	ProveCertificateArgs,
 35 | 	ProveCertificateResult,
 36 | 	PubKeyHex,
 37 | 	RelinquishCertificateArgs,
 38 | 	RelinquishCertificateResult,
 39 | 	RelinquishOutputArgs,
 40 | 	RelinquishOutputResult,
 41 | 	RevealCounterpartyKeyLinkageArgs,
 42 | 	RevealCounterpartyKeyLinkageResult,
 43 | 	RevealSpecificKeyLinkageArgs,
 44 | 	RevealSpecificKeyLinkageResult,
 45 | 	SignActionArgs,
 46 | 	SignActionResult,
 47 | 	WalletCertificate,
 48 | 	WalletInterface,
 49 | } from "@bsv/sdk";
 50 | import { type NftUtxo, type Utxo, fetchNftUtxos } from "js-1sat-ord";
 51 | import { fetchPaymentUtxos } from "./fetchPaymentUtxos";
 52 | 
 53 | export class Wallet extends ProtoWallet implements WalletInterface {
 54 | 	private paymentUtxos: Utxo[] = [];
 55 | 	private nftUtxos: NftUtxo[] = [];
 56 | 	private lastUtxoFetch = 0;
 57 | 	private readonly utxoRefreshIntervalMs = 5 * 60 * 1000; // 5 minutes
 58 | 	private privateKey?: PrivateKey;
 59 | 
 60 | 	constructor(privKey?: PrivateKey) {
 61 | 		super(privKey);
 62 | 		this.privateKey = privKey;
 63 | 		// Initialize UTXOs
 64 | 		this.refreshUtxos().catch((err) =>
 65 | 			console.error("Error initializing UTXOs:", err),
 66 | 		);
 67 | 	}
 68 | 
 69 | 	/**
 70 | 	 * Refresh UTXOs from the network
 71 | 	 */
 72 | 	async refreshUtxos(): Promise<void> {
 73 | 		try {
 74 | 			const privateKey = this.getPrivateKey();
 75 | 			if (!privateKey) {
 76 | 				return; // Silent fail if no private key, keep existing UTXOs
 77 | 			}
 78 | 
 79 | 			const address = privateKey.toAddress().toString();
 80 | 			this.lastUtxoFetch = Date.now();
 81 | 
 82 | 			// Payment UTXOs
 83 | 			let newPaymentUtxos: Utxo[] | undefined = undefined;
 84 | 			try {
 85 | 				newPaymentUtxos = await fetchPaymentUtxos(address);
 86 | 				// Only update if we successfully got UTXOs
 87 | 				if (Array.isArray(newPaymentUtxos)) {
 88 | 					this.paymentUtxos = newPaymentUtxos;
 89 | 				}
 90 | 			} catch (error) {
 91 | 				// Keep existing UTXOs, don't clear them on error
 92 | 			}
 93 | 
 94 | 			// NFT UTXOs - keep existing if fetch fails
 95 | 			let newNftUtxos: NftUtxo[] = [];
 96 | 			try {
 97 | 				newNftUtxos = await fetchNftUtxos(address);
 98 | 				// Only update if we successfully got UTXOs
 99 | 				if (Array.isArray(newNftUtxos)) {
100 | 					this.nftUtxos = newNftUtxos;
101 | 				}
102 | 			} catch (error) {
103 | 				// Keep existing UTXOs, don't clear them on error
104 | 			}
105 | 		} catch (error) {
106 | 			// Silent global error, preserve existing UTXOs
107 | 		}
108 | 	}
109 | 
110 | 	/**
111 | 	 * Get payment and NFT UTXOs, refreshing if needed
112 | 	 */
113 | 	async getUtxos(): Promise<{ paymentUtxos: Utxo[]; nftUtxos: NftUtxo[] }> {
114 | 		const now = Date.now();
115 | 		if (now - this.lastUtxoFetch > this.utxoRefreshIntervalMs) {
116 | 			await this.refreshUtxos();
117 | 		}
118 | 		return { paymentUtxos: this.paymentUtxos, nftUtxos: this.nftUtxos };
119 | 	}
120 | 
121 | 	/**
122 | 	 * Get the private key if available
123 | 	 */
124 | 	getPrivateKey(): PrivateKey | undefined {
125 | 		// Try to get private key from environment if not already set
126 | 		if (!this.privateKey) {
127 | 			const wif = process.env.PRIVATE_KEY_WIF;
128 | 			if (wif) {
129 | 				this.privateKey = PrivateKey.fromWif(wif);
130 | 			}
131 | 		}
132 | 		return this.privateKey;
133 | 	}
134 | 
135 | 	async getPublicKey(args: GetPublicKeyArgs): Promise<GetPublicKeyResult> {
136 | 		const privateKey = this.getPrivateKey();
137 | 		if (!privateKey) {
138 | 			throw new Error("No private key available");
139 | 		}
140 | 
141 | 		const publicKey = privateKey.toPublicKey();
142 | 		return {
143 | 			publicKey: publicKey.toDER("hex") as PubKeyHex,
144 | 		};
145 | 	}
146 | 	async revealCounterpartyKeyLinkage(
147 | 		args: RevealCounterpartyKeyLinkageArgs,
148 | 	): Promise<RevealCounterpartyKeyLinkageResult> {
149 | 		return Promise.reject(new Error("Not implemented"));
150 | 	}
151 | 	async revealSpecificKeyLinkage(
152 | 		args: RevealSpecificKeyLinkageArgs,
153 | 	): Promise<RevealSpecificKeyLinkageResult> {
154 | 		return Promise.reject(new Error("Not implemented"));
155 | 	}
156 | 	// Implemented by ProtoWallet
157 | 	// async encrypt(args: WalletEncryptArgs): Promise<WalletEncryptResult> {
158 | 	// 	return this.encrypt(args);
159 | 	// }
160 | 	// async decrypt(args: WalletDecryptArgs): Promise<WalletDecryptResult> {
161 | 	// 	return this.decrypt(args);
162 | 	// }
163 | 	// async createHmac(args: CreateHmacArgs): Promise<CreateHmacResult> {
164 | 	// 	return this.createHmac(args);
165 | 	// }
166 | 	// async verifyHmac(args: VerifyHmacArgs): Promise<VerifyHmacResult> {
167 | 	// 	return this.verifyHmac(args);
168 | 	// }
169 | 	// async createSignature(
170 | 	// 	args: CreateSignatureArgs,
171 | 	// ): Promise<CreateSignatureResult> {
172 | 	// 	return Promise.reject(new Error("Not implemented"));
173 | 	// }
174 | 	// async verifySignature(
175 | 	// 	args: VerifySignatureArgs,
176 | 	// ): Promise<VerifySignatureResult> {
177 | 	// 	return Promise.reject(new Error("Not implemented"));
178 | 	// }
179 | 	async createAction(args: CreateActionArgs): Promise<CreateActionResult> {
180 | 		const tx = new Transaction();
181 | 
182 | 		// Add outputs
183 | 		if (args.outputs) {
184 | 			for (const output of args.outputs) {
185 | 				const lockingScript = LockingScript.fromHex(output.lockingScript);
186 | 				tx.addOutput({
187 | 					lockingScript,
188 | 					satoshis: output.satoshis,
189 | 				});
190 | 			}
191 | 		}
192 | 
193 | 		// Add inputs (if provided)
194 | 		if (args.inputs) {
195 | 			for (const input of args.inputs) {
196 | 				const [txid, outputIndexStr] = input.outpoint.split(".");
197 | 				tx.addInput({
198 | 					sourceTXID: txid,
199 | 					sourceOutputIndex: Number.parseInt(outputIndexStr || "0", 10),
200 | 				});
201 | 			}
202 | 		}
203 | 
204 | 		// Set lockTime and version if provided
205 | 		if (args.lockTime !== undefined) tx.lockTime = args.lockTime;
206 | 		if (args.version !== undefined) tx.version = args.version;
207 | 
208 | 		// Serialize the transaction using Utils
209 | 		const txid = tx.hash("hex") as string;
210 | 		const txArray = tx.toBinary();
211 | 
212 | 		return {
213 | 			txid,
214 | 			tx: txArray,
215 | 			signableTransaction: undefined,
216 | 		};
217 | 	}
218 | 	async signAction(args: SignActionArgs): Promise<SignActionResult> {
219 | 		return Promise.reject(new Error("Not implemented"));
220 | 	}
221 | 	async abortAction(args: AbortActionArgs): Promise<AbortActionResult> {
222 | 		return Promise.reject(new Error("Not implemented"));
223 | 	}
224 | 	async listActions(args: ListActionsArgs): Promise<ListActionsResult> {
225 | 		return Promise.reject(new Error("Not implemented"));
226 | 	}
227 | 	async internalizeAction(
228 | 		args: InternalizeActionArgs,
229 | 	): Promise<InternalizeActionResult> {
230 | 		return Promise.reject(new Error("Not implemented"));
231 | 	}
232 | 	async listOutputs(args: ListOutputsArgs): Promise<ListOutputsResult> {
233 | 		return Promise.reject(new Error("Not implemented"));
234 | 	}
235 | 	async relinquishOutput(
236 | 		args: RelinquishOutputArgs,
237 | 	): Promise<RelinquishOutputResult> {
238 | 		return Promise.reject(new Error("Not implemented"));
239 | 	}
240 | 	async acquireCertificate(
241 | 		args: AcquireCertificateArgs,
242 | 	): Promise<WalletCertificate> {
243 | 		return Promise.reject(new Error("Not implemented"));
244 | 	}
245 | 	async listCertificates(
246 | 		args: ListCertificatesArgs,
247 | 	): Promise<ListCertificatesResult> {
248 | 		return Promise.reject(new Error("Not implemented"));
249 | 	}
250 | 	async proveCertificate(
251 | 		args: ProveCertificateArgs,
252 | 	): Promise<ProveCertificateResult> {
253 | 		return Promise.reject(new Error("Not implemented"));
254 | 	}
255 | 	async relinquishCertificate(
256 | 		args: RelinquishCertificateArgs,
257 | 	): Promise<RelinquishCertificateResult> {
258 | 		return Promise.reject(new Error("Not implemented"));
259 | 	}
260 | 	async discoverByIdentityKey(
261 | 		args: DiscoverByIdentityKeyArgs,
262 | 	): Promise<DiscoverCertificatesResult> {
263 | 		return Promise.reject(new Error("Not implemented"));
264 | 	}
265 | 	async discoverByAttributes(
266 | 		args: DiscoverByAttributesArgs,
267 | 	): Promise<DiscoverCertificatesResult> {
268 | 		return Promise.reject(new Error("Not implemented"));
269 | 	}
270 | 	async isAuthenticated(args: object): Promise<AuthenticatedResult> {
271 | 		return Promise.reject(new Error("Not implemented"));
272 | 	}
273 | 	async waitForAuthentication(args: object): Promise<AuthenticatedResult> {
274 | 		return Promise.reject(new Error("Not implemented"));
275 | 	}
276 | 	async getHeight(args: object): Promise<GetHeightResult> {
277 | 		return Promise.reject(new Error("Not implemented"));
278 | 	}
279 | 	async getHeaderForHeight(args: GetHeaderArgs): Promise<GetHeaderResult> {
280 | 		return Promise.reject(new Error("Not implemented"));
281 | 	}
282 | 	async getNetwork(args: object): Promise<GetNetworkResult> {
283 | 		return Promise.reject(new Error("Not implemented"));
284 | 	}
285 | 	async getVersion(args: object): Promise<GetVersionResult> {
286 | 		return Promise.reject(new Error("Not implemented"));
287 | 	}
288 | }
289 | 
290 | export default Wallet;
291 | 
```

--------------------------------------------------------------------------------
/tools/wallet/a2bPublishAgent.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { PrivateKey, Utils } from "@bsv/sdk";
  2 | import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
  3 | import type { RequestHandlerExtra } from "@modelcontextprotocol/sdk/shared/protocol.js";
  4 | import type {
  5 | 	ServerNotification,
  6 | 	ServerRequest,
  7 | } from "@modelcontextprotocol/sdk/types.js";
  8 | import { createOrdinals } from "js-1sat-ord";
  9 | import type {
 10 | 	ChangeResult,
 11 | 	CreateOrdinalsConfig,
 12 | 	Destination,
 13 | 	Inscription,
 14 | 	LocalSigner,
 15 | 	PreMAP,
 16 | } from "js-1sat-ord";
 17 | import { Sigma } from "sigma-protocol";
 18 | import { z } from "zod";
 19 | import type { Wallet } from "./wallet";
 20 | const { toArray, toBase64 } = Utils;
 21 | 
 22 | // https://raw.githubusercontent.com/google/A2A/refs/heads/main/specification/json/a2a.json
 23 | 
 24 | // A2A AgentCard schema (per A2A spec)
 25 | const AgentCapabilitiesSchema = z.object({
 26 | 	streaming: z.boolean().default(false),
 27 | 	pushNotifications: z.boolean().default(false),
 28 | 	stateTransitionHistory: z.boolean().default(false),
 29 | });
 30 | const AgentSkillSchema = z.object({
 31 | 	id: z.string(),
 32 | 	name: z.string(),
 33 | 	description: z.string().nullable(),
 34 | 	tags: z.array(z.string()).nullable(),
 35 | 	examples: z.array(z.string()).nullable(),
 36 | 	inputModes: z.array(z.string()).nullable(),
 37 | 	outputModes: z.array(z.string()).nullable(),
 38 | });
 39 | 
 40 | // Provider per A2A spec
 41 | const AgentProviderSchema = z
 42 | 	.object({
 43 | 		organization: z.string(),
 44 | 		url: z.string().url().nullable().default(null),
 45 | 	})
 46 | 	.nullable()
 47 | 	.default(null);
 48 | 
 49 | // Authentication per A2A spec
 50 | const AgentAuthenticationSchema = z
 51 | 	.object({
 52 | 		schemes: z.array(z.string()),
 53 | 		credentials: z.string().nullable().default(null),
 54 | 	})
 55 | 	.nullable()
 56 | 	.default(null);
 57 | 
 58 | // Pricing plan schema
 59 | export const PricingSchema = z.object({
 60 | 	id: z.string(),
 61 | 	description: z.string().nullable().optional(),
 62 | 	currency: z.string(),
 63 | 	amount: z
 64 | 		.number()
 65 | 		.describe(
 66 | 			"Cost in standard units of currency (e.g., BSV, USD, not satoshis)",
 67 | 		),
 68 | 	address: z.string(),
 69 | 	acceptedCurrencies: z.array(z.string()).optional(),
 70 | 	skills: z.array(z.string()).optional(),
 71 | 	interval: z.enum(["day", "week", "month", "year"]).nullable().optional(),
 72 | 	includedCalls: z.record(z.number()).optional(),
 73 | });
 74 | 
 75 | export type PricingConfig = z.infer<typeof PricingSchema>;
 76 | 
 77 | export const AgentCardSchema = z.object({
 78 | 	name: z.string(),
 79 | 	description: z.string().nullable().default(null),
 80 | 	url: z.string().url(),
 81 | 	provider: AgentProviderSchema,
 82 | 	version: z.string(),
 83 | 	documentationUrl: z.string().url().nullable().default(null),
 84 | 	capabilities: AgentCapabilitiesSchema,
 85 | 	authentication: AgentAuthenticationSchema,
 86 | 	defaultInputModes: z.array(z.string()).default(["text"]),
 87 | 	defaultOutputModes: z.array(z.string()).default(["text"]),
 88 | 	skills: z.array(AgentSkillSchema),
 89 | 	"x-payment": z.array(PricingSchema).optional().default([]),
 90 | });
 91 | 
 92 | // Schema for on-chain agent publish parameters
 93 | export const a2bPublishArgsSchema = z.object({
 94 | 	agentUrl: z
 95 | 		.string()
 96 | 		.url()
 97 | 		.describe("Agent base URL (e.g. https://example.com)"),
 98 | 	agentName: z.string().describe("Human-friendly agent name"),
 99 | 	description: z
100 | 		.string()
101 | 		.nullable()
102 | 		.optional()
103 | 		.describe("Optional agent description"),
104 | 	providerOrganization: z
105 | 		.string()
106 | 		.optional()
107 | 		.describe("Optional provider organization name"),
108 | 	providerUrl: z.string().url().optional().describe("Optional provider URL"),
109 | 	version: z.string().optional().describe("Optional agent version"),
110 | 	documentationUrl: z
111 | 		.string()
112 | 		.url()
113 | 		.nullable()
114 | 		.optional()
115 | 		.describe("Optional documentation URL"),
116 | 	streaming: z
117 | 		.boolean()
118 | 		.default(false)
119 | 		.describe("Supports SSE (tasks/sendSubscribe)"),
120 | 	pushNotifications: z
121 | 		.boolean()
122 | 		.default(false)
123 | 		.describe("Supports push notifications"),
124 | 	stateTransitionHistory: z
125 | 		.boolean()
126 | 		.default(false)
127 | 		.describe("Supports state transition history"),
128 | 	defaultInputModes: z
129 | 		.array(z.string())
130 | 		.default(["text"])
131 | 		.describe("Default input modes"),
132 | 	defaultOutputModes: z
133 | 		.array(z.string())
134 | 		.default(["text"])
135 | 		.describe("Default output modes"),
136 | 	skills: z
137 | 		.array(AgentSkillSchema)
138 | 		.optional()
139 | 		.default([])
140 | 		.describe("List of agent skills"),
141 | 	destinationAddress: z
142 | 		.string()
143 | 		.optional()
144 | 		.describe("Optional target address for inscription"),
145 | });
146 | export type A2bPublishArgs = z.infer<typeof a2bPublishArgsSchema>;
147 | 
148 | /**
149 |  * Registers the wallet_a2bPublish tool for publishing an agent record on-chain
150 |  */
151 | export function registerA2bPublishAgentTool(server: McpServer, wallet: Wallet) {
152 | 	server.tool(
153 | 		"wallet_a2bPublish",
154 | 		"Publish an agent.json record on-chain via Ordinal inscription",
155 | 		{ args: a2bPublishArgsSchema },
156 | 		async (
157 | 			{ args }: { args: A2bPublishArgs },
158 | 			extra: RequestHandlerExtra<ServerRequest, ServerNotification>,
159 | 		) => {
160 | 			try {
161 | 				const paymentPk = wallet.getPrivateKey();
162 | 				if (!paymentPk) throw new Error("No private key available");
163 | 				const { paymentUtxos } = await wallet.getUtxos();
164 | 				if (!paymentUtxos?.length)
165 | 					throw new Error("No payment UTXOs available to fund inscription");
166 | 
167 | 				// TODO: Get the skills from actually running the MCP instead of trusting the agent args for args.skills
168 | 				const walletAddress = paymentPk.toAddress().toString();
169 | 
170 | 				// Create pricing plans using the new schema
171 | 				const pricingConfig: PricingConfig[] = [
172 | 					// {
173 | 					//   id: "subscription-premium",
174 | 					//   description: "Premium subscription with all features",
175 | 					//   currency: "USD",
176 | 					//   amount: 10,
177 | 					//   address: walletAddress,
178 | 					//   interval: "month",
179 | 					//   skills: ["wallet_a2bPublish", "wallet_a2bCall"],
180 | 					// },
181 | 					// {
182 | 					//   id: "subscription-free",
183 | 					//   description: "Free tier with limited features",
184 | 					//   currency: "USD",
185 | 					//   amount: 0,
186 | 					//   address: walletAddress,
187 | 					//   interval: "month",
188 | 					//   skills: ["wallet_a2bCall"],
189 | 					// },
190 | 					{
191 | 						id: "pay-per-call-default",
192 | 						description: "Pay-per-call for publish operations",
193 | 						currency: "USD",
194 | 						amount: 1,
195 | 						address: walletAddress,
196 | 						skills: ["wallet_a2bPublish"],
197 | 						interval: null,
198 | 					},
199 | 					// {
200 | 					//   id: "pay-per-call-royalty",
201 | 					//   description: "Royalty payment for agent",
202 | 					//   currency: "USD",
203 | 					//   amount: 1,
204 | 					//   address: "1JOExxxxxxxxxxxx",
205 | 					//   skills: ["wallet_a2bPublish"],
206 | 					//   interval: null,
207 | 					// }
208 | 				];
209 | 
210 | 				// Assemble AgentCard with defaults and user overrides
211 | 				const agentCard = {
212 | 					name: args.agentName,
213 | 					description: args.description ?? null,
214 | 					url: args.agentUrl,
215 | 					provider:
216 | 						args.providerOrganization && args.providerUrl
217 | 							? {
218 | 									organization: args.providerOrganization,
219 | 									url: args.providerUrl,
220 | 								}
221 | 							: null,
222 | 					version: args.version ?? "1.0.0",
223 | 					documentationUrl: args.documentationUrl ?? null,
224 | 					capabilities: {
225 | 						streaming: args.streaming,
226 | 						pushNotifications: args.pushNotifications,
227 | 						stateTransitionHistory: args.stateTransitionHistory,
228 | 					},
229 | 					authentication: null,
230 | 					defaultInputModes: args.defaultInputModes,
231 | 					defaultOutputModes: args.defaultOutputModes,
232 | 					skills: args.skills,
233 | 					"x-payment-config": pricingConfig,
234 | 				};
235 | 				// Validate compliance
236 | 				AgentCardSchema.parse(agentCard);
237 | 				const fileContent = JSON.stringify(agentCard, null, 2);
238 | 				// Base64 payload for inscription
239 | 				const dataB64 = toBase64(toArray(fileContent));
240 | 				const inscription: Inscription = {
241 | 					dataB64,
242 | 					contentType: "application/json",
243 | 				};
244 | 				// Destination for the ordinal
245 | 				const targetAddress = args.destinationAddress ?? walletAddress;
246 | 				const destinations: Destination[] = [
247 | 					{ address: targetAddress, inscription },
248 | 				];
249 | 				// Default MAP metadata: file path, content type, encoding
250 | 				const metaData: PreMAP = { app: "bsv-mcp", type: "a2b" };
251 | 
252 | 				const createOrdinalsConfig: CreateOrdinalsConfig = {
253 | 					utxos: paymentUtxos,
254 | 					destinations,
255 | 					paymentPk,
256 | 					changeAddress: walletAddress,
257 | 					metaData,
258 | 				};
259 | 
260 | 				const identityPk = process.env.IDENTITY_KEY_WIF
261 | 					? PrivateKey.fromWif(process.env.IDENTITY_KEY_WIF)
262 | 					: undefined;
263 | 				if (identityPk) {
264 | 					createOrdinalsConfig.signer = {
265 | 						idKey: identityPk,
266 | 					} as LocalSigner;
267 | 				}
268 | 
269 | 				// Inscribe the ordinal on-chain via js-1sat-ord
270 | 				const result = await createOrdinals(createOrdinalsConfig);
271 | 				const changeResult = result as ChangeResult;
272 | 
273 | 				const disableBroadcasting = process.env.DISABLE_BROADCASTING === "true";
274 | 				if (!disableBroadcasting) {
275 | 					await changeResult.tx.broadcast();
276 | 
277 | 					// Refresh UTXOs
278 | 					try {
279 | 						await wallet.refreshUtxos();
280 | 					} catch {}
281 | 					// Return transaction details
282 | 					return {
283 | 						content: [
284 | 							{
285 | 								type: "text",
286 | 								text: JSON.stringify({
287 | 									txid: changeResult.tx.id("hex"),
288 | 									spentOutpoints: changeResult.spentOutpoints,
289 | 									payChange: changeResult.payChange,
290 | 									inscriptionAddress: targetAddress,
291 | 									agentCard,
292 | 								}),
293 | 							},
294 | 						],
295 | 					};
296 | 				}
297 | 
298 | 				return {
299 | 					content: [
300 | 						{
301 | 							type: "text",
302 | 							text: changeResult.tx.toHex(),
303 | 						},
304 | 					],
305 | 				};
306 | 			} catch (err: unknown) {
307 | 				const msg = err instanceof Error ? err.message : String(err);
308 | 				return { content: [{ type: "text", text: msg }], isError: true };
309 | 			}
310 | 		},
311 | 	);
312 | }
313 | 
```

--------------------------------------------------------------------------------
/tools/wallet/schemas.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import type { SecurityLevel } from "@bsv/sdk";
  2 | import { z } from "zod";
  3 | 
  4 | // Define SecurityLevel to match the BSV SDK exactly
  5 | const SecurityLevelEnum = z.union([z.literal(0), z.literal(1), z.literal(2)]);
  6 | 
  7 | // Create a custom validator without tuples
  8 | export const walletProtocolSchema = z.custom<[SecurityLevel, string]>((val) => {
  9 | 	return (
 10 | 		Array.isArray(val) &&
 11 | 		val.length === 2 &&
 12 | 		(val[0] === 0 || val[0] === 1 || val[0] === 2) &&
 13 | 		typeof val[1] === "string"
 14 | 	);
 15 | });
 16 | 
 17 | // Empty args schema for functions that don't take arguments
 18 | export const emptyArgsSchema = z.object({});
 19 | 
 20 | // Get public key arguments
 21 | export const getPublicKeyArgsSchema = z.object({});
 22 | 
 23 | // Create signature arguments
 24 | export const createSignatureArgsSchema = z.object({
 25 | 	data: z.array(z.number()).optional(),
 26 | 	hashToDirectlySign: z.array(z.number()).optional(),
 27 | 	protocolID: walletProtocolSchema,
 28 | 	keyID: z.string(),
 29 | 	privilegedReason: z.string().optional(),
 30 | 	counterparty: z
 31 | 		.union([z.string(), z.literal("self"), z.literal("anyone")])
 32 | 		.optional(),
 33 | 	privileged: z.boolean().optional(),
 34 | });
 35 | 
 36 | // Verify signature arguments
 37 | export const verifySignatureArgsSchema = z.object({
 38 | 	data: z.array(z.number()).optional(),
 39 | 	hashToDirectlyVerify: z.array(z.number()).optional(),
 40 | 	signature: z.array(z.number()),
 41 | 	protocolID: walletProtocolSchema,
 42 | 	keyID: z.string(),
 43 | 	privilegedReason: z.string().optional(),
 44 | 	counterparty: z
 45 | 		.union([z.string(), z.literal("self"), z.literal("anyone")])
 46 | 		.optional(),
 47 | 	forSelf: z.boolean().optional(),
 48 | 	privileged: z.boolean().optional(),
 49 | });
 50 | 
 51 | // Wallet encryption args
 52 | export const walletEncryptArgsSchema = z.object({
 53 | 	plaintext: z.array(z.number()),
 54 | 	protocolID: walletProtocolSchema,
 55 | 	keyID: z.string(),
 56 | 	privilegedReason: z.string().optional(),
 57 | 	counterparty: z
 58 | 		.union([z.string(), z.literal("self"), z.literal("anyone")])
 59 | 		.optional(),
 60 | 	privileged: z.boolean().optional(),
 61 | });
 62 | 
 63 | // Wallet decryption args
 64 | export const walletDecryptArgsSchema = z.object({
 65 | 	ciphertext: z.array(z.number()),
 66 | 	protocolID: walletProtocolSchema,
 67 | 	keyID: z.string(),
 68 | 	privilegedReason: z.string().optional(),
 69 | 	counterparty: z
 70 | 		.union([z.string(), z.literal("self"), z.literal("anyone")])
 71 | 		.optional(),
 72 | 	privileged: z.boolean().optional(),
 73 | });
 74 | 
 75 | // Combined wallet encryption/decryption args
 76 | export const walletEncryptionArgsSchema = z
 77 | 	.object({
 78 | 		mode: z
 79 | 			.enum(["encrypt", "decrypt"])
 80 | 			.describe(
 81 | 				"Operation mode: 'encrypt' to encrypt plaintext or 'decrypt' to decrypt data",
 82 | 			),
 83 | 		data: z
 84 | 			.union([
 85 | 				z.string().describe("Text data to encrypt or decrypt"),
 86 | 				z.array(z.number()).describe("Binary data to encrypt or decrypt"),
 87 | 			])
 88 | 			.describe("Data to process: text/data for encryption or decryption"),
 89 | 		encoding: z
 90 | 			.enum(["utf8", "hex", "base64"])
 91 | 			.optional()
 92 | 			.default("utf8")
 93 | 			.describe("Encoding of text data (default: utf8)"),
 94 | 	})
 95 | 	.describe("Schema for encryption and decryption operations");
 96 | 
 97 | // Create HMAC arguments
 98 | export const createHmacArgsSchema = z.object({
 99 | 	message: z.string(),
100 | 	encoding: z.enum(["utf8", "hex", "base64"]).optional(),
101 | });
102 | 
103 | // Verify HMAC arguments
104 | export const verifyHmacArgsSchema = z.object({
105 | 	message: z.string(),
106 | 	hmac: z.string(),
107 | 	publicKey: z.string(),
108 | 	encoding: z.enum(["utf8", "hex", "base64"]).optional(),
109 | });
110 | 
111 | // Transaction input schema
112 | export const transactionInputSchema = z.object({
113 | 	outpoint: z.string(),
114 | 	inputDescription: z.string(), // Required field
115 | 	sequence: z.number().optional(),
116 | });
117 | 
118 | // Transaction output schema
119 | export const transactionOutputSchema = z.object({
120 | 	lockingScript: z.string(),
121 | 	satoshis: z.number(),
122 | 	outputDescription: z.string(), // Required field
123 | 	change: z.boolean().optional(),
124 | });
125 | 
126 | // Create action args
127 | export const createActionArgsSchema = z.object({
128 | 	description: z.string(),
129 | 	labels: z.array(z.string()).optional(),
130 | 	lockTime: z.number().optional(),
131 | 	version: z.number().optional(),
132 | 	inputBEEF: z.array(z.number()).optional(),
133 | 	inputs: z.array(transactionInputSchema).optional(),
134 | 	outputs: z.array(transactionOutputSchema).optional(),
135 | 	options: z.record(z.union([z.string(), z.number(), z.boolean()])).optional(),
136 | });
137 | 
138 | // Sign action args
139 | export const signActionArgsSchema = z.object({
140 | 	reference: z.string(),
141 | 	spends: z.record(z.any()).optional(),
142 | 	options: z.record(z.union([z.string(), z.number(), z.boolean()])).optional(),
143 | });
144 | 
145 | // List actions args
146 | export const listActionsArgsSchema = z.object({
147 | 	labels: z.array(z.string()),
148 | 	labelQueryMode: z.enum(["any", "all"]).optional(),
149 | 	limit: z.number().optional(),
150 | 	offset: z.number().optional(),
151 | 	includeInputs: z.boolean().optional(),
152 | 	includeOutputs: z.boolean().optional(),
153 | 	includeLabels: z.boolean().optional(),
154 | 	includeInputSourceLockingScripts: z.boolean().optional(),
155 | 	includeInputUnlockingScripts: z.boolean().optional(),
156 | 	seekPermission: z.boolean().optional(),
157 | });
158 | 
159 | // List outputs args
160 | export const listOutputsArgsSchema = z.object({
161 | 	basket: z.string(),
162 | 	tags: z.array(z.string()).optional(),
163 | 	tagQueryMode: z.enum(["all", "any"]).optional(),
164 | 	limit: z.number().optional(),
165 | 	offset: z.number().optional(),
166 | 	include: z.enum(["locking scripts", "entire transactions"]).optional(),
167 | 	includeLabels: z.boolean().optional(),
168 | 	includeTags: z.boolean().optional(),
169 | 	includeCustomInstructions: z.boolean().optional(),
170 | 	seekPermission: z.boolean().optional(),
171 | });
172 | 
173 | // Reveal counterparty key linkage args
174 | export const revealCounterpartyKeyLinkageArgsSchema = z.object({
175 | 	counterparty: z.string(),
176 | 	verifier: z.string(),
177 | 	privileged: z.boolean().optional(),
178 | 	privilegedReason: z.string().optional(),
179 | });
180 | 
181 | // Reveal specific key linkage args
182 | export const revealSpecificKeyLinkageArgsSchema = z.object({
183 | 	keyID: z.number(),
184 | 	verifier: z.string(),
185 | 	privileged: z.boolean().optional(),
186 | 	privilegedReason: z.string().optional(),
187 | });
188 | 
189 | // Abort action args
190 | export const abortActionArgsSchema = z.object({
191 | 	reference: z.string(),
192 | });
193 | 
194 | // Internalize action args
195 | export const internalizeActionArgsSchema = z.object({
196 | 	tx: z.array(z.number()),
197 | 	outputs: z.array(
198 | 		z.object({
199 | 			outputIndex: z.number(),
200 | 			protocol: z.enum(["wallet payment", "basket insertion"]),
201 | 			lockingScript: z.string(),
202 | 			satoshis: z.number(),
203 | 		}),
204 | 	),
205 | 	description: z.string(),
206 | 	labels: z.array(z.string()).optional(),
207 | 	seekPermission: z.boolean().optional(),
208 | });
209 | 
210 | // Relinquish output args
211 | export const relinquishOutputArgsSchema = z.object({
212 | 	basket: z.string(),
213 | 	output: z.string(),
214 | });
215 | 
216 | // Acquire certificate args
217 | export const acquireCertificateArgsSchema = z.object({
218 | 	type: z.string(),
219 | 	certifier: z.string(),
220 | 	acquisitionProtocol: z.enum(["direct", "issuance"]),
221 | 	fields: z.record(z.string()),
222 | 	certifierUrl: z.string().optional(),
223 | 	serialNumber: z.string().optional(),
224 | 	signature: z.string().optional(),
225 | 	revocationOutpoint: z.string().optional(),
226 | 	keyringForSubject: z.record(z.string()).optional(),
227 | 	keyringRevealer: z.string().optional(),
228 | 	privileged: z.boolean().optional(),
229 | 	privilegedReason: z.string().optional(),
230 | });
231 | 
232 | // List certificates args
233 | export const listCertificatesArgsSchema = z.object({
234 | 	certifiers: z.array(z.string()),
235 | 	types: z.array(z.string()),
236 | 	limit: z.number().optional(),
237 | 	offset: z.number().optional(),
238 | 	privileged: z.boolean().optional(),
239 | 	privilegedReason: z.string().optional(),
240 | });
241 | 
242 | // Prove certificate args
243 | export const proveCertificateArgsSchema = z.object({
244 | 	certificate: z.object({}),
245 | 	fieldsToReveal: z.array(z.string()),
246 | 	verifier: z.string(),
247 | 	privileged: z.boolean().optional(),
248 | 	privilegedReason: z.string().optional(),
249 | });
250 | 
251 | // Relinquish certificate args
252 | export const relinquishCertificateArgsSchema = z.object({
253 | 	type: z.string(),
254 | 	serialNumber: z.string(),
255 | 	certifier: z.string(),
256 | });
257 | 
258 | // Discover by identity key args
259 | export const discoverByIdentityKeyArgsSchema = z.object({
260 | 	identityKey: z.string(),
261 | 	limit: z.number().optional(),
262 | 	offset: z.number().optional(),
263 | 	seekPermission: z.boolean().optional(),
264 | });
265 | 
266 | // Discover by attributes args
267 | export const discoverByAttributesArgsSchema = z.object({
268 | 	attributes: z.record(z.string()),
269 | 	limit: z.number().optional(),
270 | 	offset: z.number().optional(),
271 | 	seekPermission: z.boolean().optional(),
272 | });
273 | 
274 | // Get header for height args
275 | export const getHeaderArgsSchema = z.object({
276 | 	height: z.number(),
277 | });
278 | 
279 | // Get address args
280 | export const getAddressArgsSchema = z.object({});
281 | 
282 | // Send to address args
283 | export const sendToAddressArgsSchema = z.object({
284 | 	address: z.string(),
285 | 	amount: z.number(),
286 | 	currency: z.enum(["BSV", "USD"]).optional().default("BSV"),
287 | 	description: z.string().optional(),
288 | });
289 | 
290 | /**
291 |  * Schema for purchase listing arguments
292 |  */
293 | export const purchaseListingArgsSchema = z
294 | 	.object({
295 | 		listingOutpoint: z
296 | 			.string()
297 | 			.describe("The outpoint of the listing to purchase (txid_vout format)"),
298 | 		ordAddress: z
299 | 			.string()
300 | 			.describe("The ordinal address to receive the purchased item"),
301 | 		listingType: z
302 | 			.enum(["nft", "token"])
303 | 			.default("nft")
304 | 			.describe(
305 | 				"Type of listing: 'nft' for ordinal NFTs, 'token' for BSV-20 tokens",
306 | 			),
307 | 		tokenProtocol: z
308 | 			.enum(["bsv-20", "bsv-21"])
309 | 			.optional()
310 | 			.default("bsv-21")
311 | 			.describe(
312 | 				"Token protocol for token listings (required when listingType is 'token')",
313 | 			),
314 | 		tokenID: z
315 | 			.string()
316 | 			.optional()
317 | 			.describe(
318 | 				"Token ID for BSV-21 tokens or ticker for BSV-20 tokens (required when listingType is 'token')",
319 | 			),
320 | 		description: z
321 | 			.string()
322 | 			.optional()
323 | 			.describe("Optional description for the transaction"),
324 | 	})
325 | 	.describe(
326 | 		"Schema for the wallet_purchaseListing tool arguments (purchase NFTs or tokens), with detailed field descriptions.",
327 | 	);
328 | 
329 | // Export types
330 | export type SendToAddressArgs = z.infer<typeof sendToAddressArgsSchema>;
331 | export type PurchaseListingArgs = z.infer<typeof purchaseListingArgsSchema>;
332 | export type WalletEncryptionArgs = z.infer<typeof walletEncryptionArgsSchema>;
333 | 
```

--------------------------------------------------------------------------------
/tools/wallet/tools.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
  2 | import type { ToolCallback } from "@modelcontextprotocol/sdk/server/mcp.js";
  3 | import type { RequestHandlerExtra } from "@modelcontextprotocol/sdk/shared/protocol.js";
  4 | import type {
  5 | 	CallToolResult,
  6 | 	ServerNotification,
  7 | 	ServerRequest,
  8 | } from "@modelcontextprotocol/sdk/types.js";
  9 | import type { z } from "zod";
 10 | import type {
 11 | 	abortActionArgsSchema,
 12 | 	acquireCertificateArgsSchema,
 13 | 	createHmacArgsSchema,
 14 | 	discoverByAttributesArgsSchema,
 15 | 	discoverByIdentityKeyArgsSchema,
 16 | 	getHeaderArgsSchema,
 17 | 	internalizeActionArgsSchema,
 18 | 	listActionsArgsSchema,
 19 | 	listCertificatesArgsSchema,
 20 | 	listOutputsArgsSchema,
 21 | 	proveCertificateArgsSchema,
 22 | 	relinquishCertificateArgsSchema,
 23 | 	relinquishOutputArgsSchema,
 24 | 	revealCounterpartyKeyLinkageArgsSchema,
 25 | 	revealSpecificKeyLinkageArgsSchema,
 26 | 	verifyHmacArgsSchema,
 27 | } from "./schemas";
 28 | import type { Wallet } from "./wallet";
 29 | 
 30 | import {
 31 | 	createSignatureArgsSchema,
 32 | 	type emptyArgsSchema,
 33 | 	type getAddressArgsSchema,
 34 | 	type getPublicKeyArgsSchema,
 35 | 	type purchaseListingArgsSchema,
 36 | 	type sendToAddressArgsSchema,
 37 | 	verifySignatureArgsSchema,
 38 | 	walletEncryptionArgsSchema,
 39 | } from "./schemas";
 40 | 
 41 | import { Utils, type WalletProtocol } from "@bsv/sdk";
 42 | import { registerCreateOrdinalsTool } from "./createOrdinals";
 43 | import type { createOrdinalsArgsSchema } from "./createOrdinals";
 44 | import { registerGetAddressTool } from "./getAddress";
 45 | import { registerGetPublicKeyTool } from "./getPublicKey";
 46 | import { registerPurchaseListingTool } from "./purchaseListing";
 47 | import { registerRefreshUtxosTool } from "./refreshUtxos";
 48 | import { registerSendToAddressTool } from "./sendToAddress";
 49 | import { registerTransferOrdTokenTool } from "./transferOrdToken";
 50 | 
 51 | import type { a2bPublishArgsSchema } from "./a2bPublishAgent";
 52 | import { registerA2bPublishMcpTool } from "./a2bPublishMcp";
 53 | import type { transferOrdTokenArgsSchema } from "./transferOrdToken";
 54 | 
 55 | // Define mapping from tool names to argument schemas
 56 | type ToolArgSchemas = {
 57 | 	wallet_getPublicKey: typeof getPublicKeyArgsSchema;
 58 | 	wallet_createSignature: typeof createSignatureArgsSchema;
 59 | 	wallet_verifySignature: typeof verifySignatureArgsSchema;
 60 | 	wallet_encryption: typeof walletEncryptionArgsSchema;
 61 | 	wallet_listActions: typeof listActionsArgsSchema;
 62 | 	wallet_listOutputs: typeof listOutputsArgsSchema;
 63 | 	wallet_getNetwork: typeof emptyArgsSchema;
 64 | 	wallet_getVersion: typeof emptyArgsSchema;
 65 | 	wallet_revealCounterpartyKeyLinkage: typeof revealCounterpartyKeyLinkageArgsSchema;
 66 | 	wallet_revealSpecificKeyLinkage: typeof revealSpecificKeyLinkageArgsSchema;
 67 | 	wallet_createHmac: typeof createHmacArgsSchema;
 68 | 	wallet_verifyHmac: typeof verifyHmacArgsSchema;
 69 | 	wallet_abortAction: typeof abortActionArgsSchema;
 70 | 	wallet_internalizeAction: typeof internalizeActionArgsSchema;
 71 | 	wallet_relinquishOutput: typeof relinquishOutputArgsSchema;
 72 | 	wallet_acquireCertificate: typeof acquireCertificateArgsSchema;
 73 | 	wallet_listCertificates: typeof listCertificatesArgsSchema;
 74 | 	wallet_proveCertificate: typeof proveCertificateArgsSchema;
 75 | 	wallet_relinquishCertificate: typeof relinquishCertificateArgsSchema;
 76 | 	wallet_discoverByIdentityKey: typeof discoverByIdentityKeyArgsSchema;
 77 | 	wallet_discoverByAttributes: typeof discoverByAttributesArgsSchema;
 78 | 	wallet_isAuthenticated: typeof emptyArgsSchema;
 79 | 	wallet_waitForAuthentication: typeof emptyArgsSchema;
 80 | 	wallet_getHeaderForHeight: typeof getHeaderArgsSchema;
 81 | 	wallet_getAddress: typeof getAddressArgsSchema;
 82 | 	wallet_sendToAddress: typeof sendToAddressArgsSchema;
 83 | 	wallet_purchaseListing: typeof purchaseListingArgsSchema;
 84 | 	wallet_transferOrdToken: typeof transferOrdTokenArgsSchema;
 85 | 	wallet_a2bPublish: typeof a2bPublishArgsSchema;
 86 | 	wallet_createOrdinals: typeof createOrdinalsArgsSchema;
 87 | 	wallet_refreshUtxos: typeof emptyArgsSchema;
 88 | };
 89 | 
 90 | // Define a type for the handler function with proper argument types
 91 | type ToolHandler = (
 92 | 	params: { args: unknown },
 93 | 	extra: RequestHandlerExtra<ServerRequest, ServerNotification>,
 94 | ) => Promise<CallToolResult>;
 95 | 
 96 | // Define a map type for tool name to handler functions
 97 | type ToolHandlerMap = {
 98 | 	[K in keyof ToolArgSchemas]: ToolHandler;
 99 | };
100 | 
101 | export function registerWalletTools(
102 | 	server: McpServer,
103 | 	wallet: Wallet,
104 | 	config: {
105 | 		disableBroadcasting: boolean;
106 | 		enableA2bTools: boolean;
107 | 	},
108 | ): ToolHandlerMap {
109 | 	const handlers = {} as ToolHandlerMap;
110 | 
111 | 	// Handle tools registration with properly typed parameters
112 | 	function registerTool<T extends z.ZodType>(
113 | 		name: keyof ToolArgSchemas,
114 | 		description: string,
115 | 		schema: { args: T },
116 | 		handler: ToolCallback<{ args: T }>,
117 | 	): void {
118 | 		// Register all tools normally
119 | 		server.tool(name, description, schema, handler);
120 | 		handlers[name] = handler as ToolHandler;
121 | 	}
122 | 
123 | 	// Register the wallet_sendToAddress tool
124 | 	registerSendToAddressTool(server, wallet);
125 | 
126 | 	// Register the wallet_getAddress tool
127 | 	registerGetAddressTool(server);
128 | 
129 | 	// Register the wallet_getPublicKey tool
130 | 	registerGetPublicKeyTool(server, wallet);
131 | 
132 | 	// Register the wallet_purchaseListing tool
133 | 	registerPurchaseListingTool(server, wallet);
134 | 
135 | 	// Register the wallet_transferOrdToken tool
136 | 	registerTransferOrdTokenTool(server, wallet);
137 | 
138 | 	// Register the wallet_refreshUtxos tool
139 | 	registerRefreshUtxosTool(server, wallet);
140 | 
141 | 	// A2B tools have to be explicitly enabled
142 | 	if (config.enableA2bTools) {
143 | 		// Register the wallet_a2bPublishAgent tool
144 | 		// registerA2bPublishAgentTool(server, wallet);
145 | 
146 | 		// Register the wallet_a2bPublishMcp tool
147 | 		registerA2bPublishMcpTool(server, wallet, {
148 | 			disableBroadcasting: config.disableBroadcasting,
149 | 		});
150 | 	}
151 | 
152 | 	// Register only the minimal public-facing tools
153 | 	// wallet_createAction, wallet_signAction and wallet_getHeight have been removed
154 | 
155 | 	// Register wallet_createSignature
156 | 	registerTool(
157 | 		"wallet_createSignature",
158 | 		"Creates a cryptographic signature using the wallet's private key. This tool enables secure message signing and transaction authorization, supporting various signature protocols.",
159 | 		{ args: createSignatureArgsSchema },
160 | 		async (
161 | 			{ args }: { args: z.infer<typeof createSignatureArgsSchema> },
162 | 			extra: RequestHandlerExtra<ServerRequest, ServerNotification>,
163 | 		) => {
164 | 			try {
165 | 				const result = await wallet.createSignature(args);
166 | 				return { content: [{ type: "text", text: JSON.stringify(result) }] };
167 | 			} catch (err: unknown) {
168 | 				const msg = err instanceof Error ? err.message : String(err);
169 | 				return { content: [{ type: "text", text: msg }], isError: true };
170 | 			}
171 | 		},
172 | 	);
173 | 
174 | 	// Register wallet_verifySignature
175 | 	registerTool(
176 | 		"wallet_verifySignature",
177 | 		"Verifies a cryptographic signature against a message or data. This tool supports various verification protocols and can validate signatures from both the wallet's own keys and external public keys.",
178 | 		{ args: verifySignatureArgsSchema },
179 | 		async (
180 | 			{ args }: { args: z.infer<typeof verifySignatureArgsSchema> },
181 | 			extra: RequestHandlerExtra<ServerRequest, ServerNotification>,
182 | 		) => {
183 | 			try {
184 | 				const result = await wallet.verifySignature(args);
185 | 				return { content: [{ type: "text", text: JSON.stringify(result) }] };
186 | 			} catch (err: unknown) {
187 | 				const msg = err instanceof Error ? err.message : String(err);
188 | 				return { content: [{ type: "text", text: msg }], isError: true };
189 | 			}
190 | 		},
191 | 	);
192 | 
193 | 	// Register combined wallet_encryption tool
194 | 	registerTool(
195 | 		"wallet_encryption",
196 | 		"Combined tool for encrypting and decrypting data using the wallet's cryptographic keys.\n\n" +
197 | 			"PARAMETERS:\n" +
198 | 			'- mode: (required) Either "encrypt" to encrypt plaintext or "decrypt" to decrypt ciphertext\n' +
199 | 			"- data: (required) Text string or array of numbers to process\n" +
200 | 			"- encoding: (optional) For text input, the encoding format (utf8, hex, base64) - default is utf8\n\n" +
201 | 			"EXAMPLES:\n" +
202 | 			"1. Encrypt text data:\n" +
203 | 			"   {\n" +
204 | 			'     "mode": "encrypt",\n' +
205 | 			'     "data": "Hello World"\n' +
206 | 			"   }\n\n" +
207 | 			"2. Decrypt previously encrypted data:\n" +
208 | 			"   {\n" +
209 | 			'     "mode": "decrypt",\n' +
210 | 			'     "data": [encrypted bytes from previous response]\n' +
211 | 			"   }",
212 | 		{ args: walletEncryptionArgsSchema },
213 | 		async (
214 | 			{ args }: { args: z.infer<typeof walletEncryptionArgsSchema> },
215 | 			extra: RequestHandlerExtra<ServerRequest, ServerNotification>,
216 | 		) => {
217 | 			try {
218 | 				const { mode, data, encoding } = args;
219 | 
220 | 				// Set default values for required parameters
221 | 				const protocolID: WalletProtocol = [1, "aes256"];
222 | 				const keyID = "default";
223 | 
224 | 				// Convert string data to binary if needed
225 | 				let binaryData: number[];
226 | 				if (Array.isArray(data)) {
227 | 					binaryData = data;
228 | 				} else {
229 | 					// String data with encoding
230 | 					const { toArray } = Utils;
231 | 					binaryData = toArray(data, encoding || "utf8");
232 | 				}
233 | 
234 | 				let result: { ciphertext?: number[]; plaintext?: number[] | string } =
235 | 					{};
236 | 				if (mode === "encrypt") {
237 | 					result = await wallet.encrypt({
238 | 						plaintext: binaryData,
239 | 						protocolID,
240 | 						keyID,
241 | 					});
242 | 				} else if (mode === "decrypt") {
243 | 					result = await wallet.decrypt({
244 | 						ciphertext: binaryData,
245 | 						protocolID,
246 | 						keyID,
247 | 					});
248 | 
249 | 					// For decryption, convert plaintext back to string if it's likely UTF-8 text
250 | 					if (result.plaintext as number[]) {
251 | 						try {
252 | 							const { toUTF8 } = Utils;
253 | 							const textResult = toUTF8(result.plaintext as number[]);
254 | 							// If conversion succeeds and seems like valid text, return as string
255 | 							if (textResult && textResult.length > 0) {
256 | 								return {
257 | 									content: [
258 | 										{
259 | 											type: "text",
260 | 											text: JSON.stringify({ plaintext: textResult }),
261 | 										},
262 | 									],
263 | 								};
264 | 							}
265 | 						} catch (e) {
266 | 							// If UTF-8 conversion fails, continue with binary result
267 | 						}
268 | 					}
269 | 				}
270 | 
271 | 				return { content: [{ type: "text", text: JSON.stringify(result) }] };
272 | 			} catch (error) {
273 | 				const errorMessage =
274 | 					error instanceof Error ? error.message : String(error);
275 | 				return {
276 | 					content: [
277 | 						{
278 | 							type: "text",
279 | 							text: `Error during ${args.mode}: ${errorMessage}`,
280 | 						},
281 | 					],
282 | 					isError: true,
283 | 				};
284 | 			}
285 | 		},
286 | 	);
287 | 
288 | 	// Register createOrdinals tool
289 | 	registerCreateOrdinalsTool(server, wallet);
290 | 
291 | 	return handlers;
292 | }
293 | 
```

--------------------------------------------------------------------------------
/tools/wallet/purchaseListing.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { PrivateKey } from "@bsv/sdk";
  2 | import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
  3 | import type { RequestHandlerExtra } from "@modelcontextprotocol/sdk/shared/protocol.js";
  4 | import type {
  5 | 	ServerNotification,
  6 | 	ServerRequest,
  7 | } from "@modelcontextprotocol/sdk/types.js";
  8 | import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
  9 | import {
 10 | 	type ChangeResult,
 11 | 	type ExistingListing,
 12 | 	type LocalSigner,
 13 | 	type Payment,
 14 | 	type PurchaseOrdListingConfig,
 15 | 	type Royalty,
 16 | 	TokenType,
 17 | 	type TokenUtxo,
 18 | 	type Utxo,
 19 | 	oneSatBroadcaster,
 20 | 	purchaseOrdListing,
 21 | 	purchaseOrdTokenListing,
 22 | } from "js-1sat-ord";
 23 | import { Sigma } from "sigma-protocol";
 24 | import type { z } from "zod";
 25 | import {
 26 | 	MARKET_FEE_PERCENTAGE,
 27 | 	MARKET_WALLET_ADDRESS,
 28 | 	MINIMUM_MARKET_FEE_SATOSHIS,
 29 | } from "../constants";
 30 | import { purchaseListingArgsSchema } from "./schemas";
 31 | import type { Wallet } from "./wallet";
 32 | 
 33 | // Define types for 1Sat API response
 34 | interface OrdUtxo {
 35 | 	txid: string;
 36 | 	vout: number;
 37 | 	satoshis: number;
 38 | 	script: string;
 39 | 	origin?: {
 40 | 		outpoint: string;
 41 | 		data?: {
 42 | 			map?: {
 43 | 				royalties?: string;
 44 | 				[key: string]: string | number | boolean | null | undefined;
 45 | 			};
 46 | 			insc?: {
 47 | 				text?: string;
 48 | 				file?: {
 49 | 					type?: string;
 50 | 					size?: number;
 51 | 				};
 52 | 			};
 53 | 		};
 54 | 	};
 55 | 	data?: {
 56 | 		list?: {
 57 | 			price: number;
 58 | 			payout: string;
 59 | 		};
 60 | 		bsv20?: {
 61 | 			amt?: string;
 62 | 			tick?: string;
 63 | 			id?: string;
 64 | 		};
 65 | 	};
 66 | }
 67 | 
 68 | /**
 69 |  * Register the purchaseListing tool
 70 |  *
 71 |  * This tool enables purchasing listed ordinals (NFTs or tokens) from the marketplace:
 72 |  * 1. Parses the listing outpoint to get the txid and vout
 73 |  * 2. Fetches the listing UTXO from the ordinals API
 74 |  * 3. Gets the wallet's payment UTXOs (using the wallet's internal UTXO management)
 75 |  * 4. Uses purchaseOrdListing or purchaseOrdTokenListing based on the listing type
 76 |  * 5. For NFTs, automatically detects and processes royalty payments to original creators
 77 |  * 6. Broadcasts the transaction
 78 |  * 7. Returns the transaction details including success status and txid
 79 |  *
 80 |  * The tool supports both NFT and token listings with appropriate type-specific handling.
 81 |  * Royalty payments are supported for NFT purchases only (based on creator-defined metadata).
 82 |  */
 83 | export function registerPurchaseListingTool(server: McpServer, wallet: Wallet) {
 84 | 	// Store a reference to check if wallet is persistent
 85 | 
 86 | 	server.tool(
 87 | 		"wallet_purchaseListing",
 88 | 		"Purchases a listing from the Bitcoin SV ordinals marketplace. Supports both NFT purchases (with royalty payments to original creators) and BSV-20/BSV-21 token purchases. The tool handles all aspects of the transaction - from fetching listing details, calculating fees, creating and broadcasting the transaction.",
 89 | 		{ args: purchaseListingArgsSchema },
 90 | 		async (
 91 | 			{ args }: { args: z.infer<typeof purchaseListingArgsSchema> },
 92 | 			extra: RequestHandlerExtra<ServerRequest, ServerNotification>,
 93 | 		): Promise<CallToolResult> => {
 94 | 			try {
 95 | 				// Load optional identity key for sigma signing
 96 | 				const identityKeyWif = process.env.IDENTITY_KEY_WIF;
 97 | 				let identityPk: PrivateKey | undefined;
 98 | 				if (identityKeyWif) {
 99 | 					try {
100 | 						identityPk = PrivateKey.fromWif(identityKeyWif);
101 | 					} catch (e) {
102 | 						console.warn(
103 | 							"Warning: Invalid IDENTITY_KEY_WIF environment variable; sigma signing disabled",
104 | 							e,
105 | 						);
106 | 					}
107 | 				}
108 | 
109 | 				// Fetch the listing info directly from the API
110 | 				const response = await fetch(
111 | 					`https://ordinals.gorillapool.io/api/txos/${args.listingOutpoint}?script=true`,
112 | 				);
113 | 				if (!response.ok) {
114 | 					throw new Error(
115 | 						`Failed to fetch listing data: ${response.statusText}`,
116 | 					);
117 | 				}
118 | 
119 | 				const listingData = (await response.json()) as OrdUtxo;
120 | 
121 | 				// Check if the listing is valid and has a price
122 | 				if (!listingData.data?.list?.price) {
123 | 					throw new Error("Listing is either not for sale or invalid");
124 | 				}
125 | 
126 | 				// Check if payout is available
127 | 				if (!listingData.data.list.payout) {
128 | 					throw new Error("Listing doesn't have payout information");
129 | 				}
130 | 
131 | 				// Calculate the market fee (3% of listing price)
132 | 				const listingPrice = listingData.data.list.price;
133 | 				let marketFee = Math.round(listingPrice * MARKET_FEE_PERCENTAGE);
134 | 
135 | 				// Ensure minimum fee
136 | 				if (marketFee < MINIMUM_MARKET_FEE_SATOSHIS) {
137 | 					marketFee = MINIMUM_MARKET_FEE_SATOSHIS;
138 | 				}
139 | 
140 | 				// Parse the listing outpoint to get txid and vout
141 | 				const [txid, voutStr] = args.listingOutpoint.split("_");
142 | 				if (!txid) {
143 | 					throw new Error("Invalid outpoint format. Expected txid_vout");
144 | 				}
145 | 				const vout = Number.parseInt(voutStr || "0", 10);
146 | 
147 | 				// Get private key from the wallet
148 | 				const paymentPk = wallet.getPrivateKey();
149 | 				if (!paymentPk) {
150 | 					throw new Error("No private key available in wallet");
151 | 				}
152 | 
153 | 				// Get payment address
154 | 				const paymentAddress = paymentPk.toAddress().toString();
155 | 
156 | 				// Get payment UTXOs from the wallet's managed UTXOs
157 | 				const { paymentUtxos } = await wallet.getUtxos();
158 | 				if (!paymentUtxos || paymentUtxos.length === 0) {
159 | 					// Provide more helpful error message with instructions
160 | 					throw new Error(
161 | 						`No payment UTXOs available for address ${paymentAddress}. 
162 | Please fund this wallet address with enough BSV to cover the purchase price 
163 | (${listingData.data.list.price} satoshis) plus market fee (${marketFee} satoshis) and transaction fees.`,
164 | 					);
165 | 				}
166 | 
167 | 				// Define market fee payment
168 | 				const additionalPayments: Payment[] = [
169 | 					{
170 | 						to: MARKET_WALLET_ADDRESS,
171 | 						amount: marketFee,
172 | 					},
173 | 				];
174 | 
175 | 				// Define metadata for the transaction
176 | 				const metaData = {
177 | 					app: "bsv-mcp",
178 | 					type: "ord",
179 | 					op: "purchase",
180 | 				};
181 | 
182 | 				// Create the purchase transaction based on listing type
183 | 				let transaction: ChangeResult;
184 | 
185 | 				if (args.listingType === "token") {
186 | 					if (!args.tokenProtocol) {
187 | 						throw new Error("tokenProtocol is required for token listings");
188 | 					}
189 | 
190 | 					if (!args.tokenID) {
191 | 						throw new Error("tokenID is required for token listings");
192 | 					}
193 | 
194 | 					// Validate token data from the listing
195 | 					if (!listingData.data.bsv20) {
196 | 						throw new Error("This is not a valid BSV-20 token listing");
197 | 					}
198 | 
199 | 					// For BSV-20, the amount should be included in the listing data
200 | 					if (!listingData.data.bsv20.amt) {
201 | 						throw new Error("Token listing doesn't have an amount specified");
202 | 					}
203 | 
204 | 					// Convert the token protocol to the enum type expected by js-1sat-ord
205 | 					const protocol =
206 | 						args.tokenProtocol === "bsv-20" ? TokenType.BSV20 : TokenType.BSV21;
207 | 
208 | 					// Create a TokenUtxo with the required fields
209 | 					const listingUtxo: TokenUtxo = {
210 | 						txid,
211 | 						vout,
212 | 						script: listingData.script,
213 | 						satoshis: 1, // TokenUtxo's satoshis must be exactly 1
214 | 						amt: listingData.data.bsv20.amt,
215 | 						id: args.tokenID,
216 | 						payout: listingData.data.list.payout,
217 | 					};
218 | 
219 | 					transaction = await purchaseOrdTokenListing({
220 | 						protocol,
221 | 						tokenID: args.tokenID,
222 | 						utxos: paymentUtxos,
223 | 						paymentPk,
224 | 						listingUtxo,
225 | 						ordAddress: args.ordAddress,
226 | 						additionalPayments,
227 | 						metaData,
228 | 					});
229 | 				} else {
230 | 					// Create a regular Utxo for NFT listing
231 | 					const listingUtxo: Utxo = {
232 | 						txid,
233 | 						vout,
234 | 						script: listingData.script,
235 | 						satoshis: listingData.satoshis,
236 | 					};
237 | 
238 | 					// Create the ExistingListing object for NFT listings
239 | 					const listing: ExistingListing = {
240 | 						payout: listingData.data.list.payout,
241 | 						listingUtxo,
242 | 					};
243 | 
244 | 					// Check for royalties in the NFT origin data
245 | 					// Royalties are only supported for NFTs, not for tokens
246 | 					// The royalties are defined by the original creator as a JSON string
247 | 					// in the NFT's metadata and parsed into a Royalty[] array
248 | 					let royalties: Royalty[] = [];
249 | 					if (listingData.origin?.data?.map?.royalties) {
250 | 						try {
251 | 							royalties = JSON.parse(listingData.origin.data.map.royalties);
252 | 						} catch (error) {
253 | 							// Remove console.warn
254 | 						}
255 | 					}
256 | 
257 | 					const purchaseOrdListingConfig: PurchaseOrdListingConfig = {
258 | 						utxos: paymentUtxos,
259 | 						paymentPk,
260 | 						ordAddress: args.ordAddress,
261 | 						listing,
262 | 						additionalPayments,
263 | 						metaData,
264 | 						royalties,
265 | 					};
266 | 
267 | 					transaction = await purchaseOrdListing(purchaseOrdListingConfig);
268 | 				}
269 | 
270 | 				// After successful transaction creation, refresh the wallet's UTXOs
271 | 				// This ensures the wallet doesn't try to reuse spent UTXOs
272 | 				try {
273 | 					await wallet.refreshUtxos();
274 | 				} catch (refreshError) {
275 | 					// Remove console.warn
276 | 				}
277 | 
278 | 				// Optionally sign with identity key then broadcast the transaction
279 | 
280 | 				const disableBroadcasting = process.env.DISABLE_BROADCASTING === "true";
281 | 				if (!disableBroadcasting) {
282 | 					const broadcastResult = await transaction.tx.broadcast(
283 | 						oneSatBroadcaster(),
284 | 					);
285 | 					// Handle broadcast response
286 | 					const resultStatus =
287 | 						typeof broadcastResult === "object" && "status" in broadcastResult
288 | 							? broadcastResult.status
289 | 							: "unknown";
290 | 
291 | 					const resultMessage =
292 | 						typeof broadcastResult === "object" && "error" in broadcastResult
293 | 							? broadcastResult.error
294 | 							: "Transaction broadcast successful";
295 | 
296 | 					return {
297 | 						content: [
298 | 							{
299 | 								type: "text",
300 | 								text: JSON.stringify({
301 | 									status: resultStatus,
302 | 									message: resultMessage,
303 | 									txid: transaction.tx.id("hex"),
304 | 									listingOutpoint: args.listingOutpoint,
305 | 									destinationAddress: args.ordAddress,
306 | 									listingType: args.listingType,
307 | 									tokenProtocol: args.tokenID ? args.tokenProtocol : undefined,
308 | 									tokenID: args.tokenID,
309 | 									price: listingData.data.list.price,
310 | 									marketFee,
311 | 									marketFeeAddress: MARKET_WALLET_ADDRESS,
312 | 									royaltiesPaid:
313 | 										args.listingType === "nft" &&
314 | 										listingData.origin?.data?.map?.royalties
315 | 											? JSON.parse(listingData.origin.data.map.royalties)
316 | 											: undefined,
317 | 								}),
318 | 							},
319 | 						],
320 | 					};
321 | 				}
322 | 				return {
323 | 					content: [
324 | 						{
325 | 							type: "text",
326 | 							text: transaction.tx.toHex(),
327 | 						},
328 | 					],
329 | 				};
330 | 			} catch (err: unknown) {
331 | 				const msg = err instanceof Error ? err.message : String(err);
332 | 				return { content: [{ type: "text", text: msg }], isError: true };
333 | 			}
334 | 		},
335 | 	);
336 | }
337 | 
```

--------------------------------------------------------------------------------
/tools/wallet/a2bPublishMcp.ts:
--------------------------------------------------------------------------------

```typescript
  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";
  4 | import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
  5 | import type { RequestHandlerExtra } from "@modelcontextprotocol/sdk/shared/protocol.js";
  6 | import type {
  7 | 	ClientNotification,
  8 | 	ClientRequest,
  9 | 	ServerNotification,
 10 | 	ServerRequest,
 11 | } from "@modelcontextprotocol/sdk/types.js";
 12 | import { createOrdinals } from "js-1sat-ord";
 13 | import type {
 14 | 	ChangeResult,
 15 | 	CreateOrdinalsConfig,
 16 | 	Destination,
 17 | 	Inscription,
 18 | 	LocalSigner,
 19 | 	PreMAP,
 20 | } from "js-1sat-ord";
 21 | import { Sigma } from "sigma-protocol";
 22 | import { z } from "zod";
 23 | import type { Wallet } from "./wallet";
 24 | const { toArray, toBase64 } = Utils;
 25 | 
 26 | // API endpoint for the A2B Overlay service
 27 | const OVERLAY_API_URL = "https://a2b-overlay-production.up.railway.app/v1";
 28 | 
 29 | // Schema for the MCP tool configuration
 30 | export const McpConfigSchema = z.object({
 31 | 	command: z.string().describe("The command to execute the tool"),
 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"),
 36 | 	env: z.record(z.string()).optional().describe("Environment variables"),
 37 | });
 38 | 
 39 | export type McpConfig = z.infer<typeof McpConfigSchema>;
 40 | 
 41 | // Schema for on-chain tool publish parameters
 42 | export const a2bPublishMcpArgsSchema = z.object({
 43 | 	toolName: z.string().describe("Human-friendly tool name"),
 44 | 	command: z.string().describe("The command to execute the tool"),
 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"),
 50 | 	env: z
 51 | 		.array(
 52 | 			z.object({
 53 | 				key: z.string().describe("Environment variable name"),
 54 | 				description: z
 55 | 					.string()
 56 | 					.describe("Description of the environment variable"),
 57 | 			}),
 58 | 		)
 59 | 		.optional()
 60 | 		.describe("Optional environment variables with descriptions"),
 61 | 	description: z.string().optional().describe("Optional tool description"),
 62 | 	destinationAddress: z
 63 | 		.string()
 64 | 		.optional()
 65 | 		.describe("Optional target address for inscription"),
 66 | });
 67 | 
 68 | export type A2bPublishMcpArgs = z.infer<typeof a2bPublishMcpArgsSchema>;
 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 | 
183 | /**
184 |  * Registers the wallet_a2bPublishMcp for publishing an MCP tool configuration on-chain
185 |  */
186 | export function registerA2bPublishMcpTool(
187 | 	server: McpServer,
188 | 	wallet: Wallet,
189 | 	config: { disableBroadcasting: boolean },
190 | ) {
191 | 	server.tool(
192 | 		"wallet_a2bPublishMcp",
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.",
194 | 		{ args: a2bPublishMcpArgsSchema },
195 | 		async (
196 | 			{ args }: { args: A2bPublishMcpArgs },
197 | 			extra: RequestHandlerExtra<ServerRequest, ServerNotification>,
198 | 		) => {
199 | 			try {
200 | 				// Load optional identity key for sigma signing
201 | 				const identityKeyWif = process.env.IDENTITY_KEY_WIF;
202 | 				let identityPk: PrivateKey | undefined;
203 | 				if (identityKeyWif) {
204 | 					try {
205 | 						identityPk = PrivateKey.fromWif(identityKeyWif);
206 | 					} catch (e) {
207 | 						console.warn(
208 | 							"Warning: Invalid IDENTITY_KEY_WIF environment variable; sigma signing disabled",
209 | 							e,
210 | 						);
211 | 					}
212 | 				}
213 | 
214 | 				const paymentPk = wallet.getPrivateKey();
215 | 				if (!paymentPk) throw new Error("No private key available");
216 | 
217 | 				const { paymentUtxos } = await wallet.getUtxos();
218 | 				if (!paymentUtxos?.length)
219 | 					throw new Error("No payment UTXOs available to fund inscription");
220 | 
221 | 				const walletAddress = paymentPk.toAddress().toString();
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 | 
227 | 				// Assemble tool configuration
228 | 				const toolConfig: McpConfig = {
229 | 					command: args.command,
230 | 					args: args.args,
231 | 					tools: metadata.tools,
232 | 					prompts: metadata.prompts,
233 | 					resources: metadata.resources,
234 | 					env: args.env
235 | 						? args.env.reduce(
236 | 								(acc, { key, description }) => {
237 | 									acc[key] = description;
238 | 									return acc;
239 | 								},
240 | 								{} as Record<string, string>,
241 | 							)
242 | 						: undefined,
243 | 				};
244 | 
245 | 				// Validate compliance
246 | 				McpConfigSchema.parse(toolConfig);
247 | 
248 | 				// Prepare the full configuration with metadata
249 | 				const fullConfig = {
250 | 					mcpServers: {
251 | 						[args.toolName]: {
252 | 							description: args.description || "",
253 | 							keywords: args.keywords || [],
254 | 							tools: metadata.tools || [],
255 | 							prompts: metadata.prompts || [],
256 | 							resources: metadata.resources || [],
257 | 							...toolConfig,
258 | 						},
259 | 					},
260 | 				};
261 | 
262 | 				const fileContent = JSON.stringify(fullConfig, null, 2);
263 | 
264 | 				// Base64 payload for inscription
265 | 				const dataB64 = toBase64(toArray(fileContent));
266 | 				const inscription: Inscription = {
267 | 					dataB64,
268 | 					contentType: "application/json",
269 | 				};
270 | 
271 | 				// Destination for the ordinal
272 | 				const targetAddress = args.destinationAddress ?? walletAddress;
273 | 				const destinations: Destination[] = [
274 | 					{ address: targetAddress, inscription },
275 | 				];
276 | 
277 | 				// Default MAP metadata: file path, content type, encoding
278 | 				const metaData: PreMAP = { app: "bsv-mcp", type: "a2b-mcp" };
279 | 
280 | 				const createOrdinalsConfig = {
281 | 					utxos: paymentUtxos,
282 | 					destinations,
283 | 					paymentPk,
284 | 					changeAddress: walletAddress,
285 | 					metaData,
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);
294 | 
295 | 				const changeResult = result as ChangeResult;
296 | 
297 | 				// Broadcast the transaction
298 | 				if (!config.disableBroadcasting) {
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);
306 | 
307 | 					// Refresh UTXOs after spending
308 | 					try {
309 | 						await wallet.refreshUtxos();
310 | 					} catch (refreshError) {
311 | 						console.warn(
312 | 							"Failed to refresh UTXOs after transaction:",
313 | 							refreshError,
314 | 						);
315 | 					}
316 | 
317 | 					// Build a nicely formatted result
318 | 					const outpointIndex = 0; // First output with the inscription
319 | 					const outpoint = `${txid}_${outpointIndex}`;
320 | 
321 | 					// Tool URL for discovery is the outpoint
322 | 					const onchainUrl = `ord://${outpoint}`;
323 | 
324 | 					return {
325 | 						content: [
326 | 							{
327 | 								type: "text",
328 | 								text: JSON.stringify(
329 | 									{
330 | 										status: "success",
331 | 										txid,
332 | 										outpoint,
333 | 										onchainUrl,
334 | 										toolName: args.toolName,
335 | 										toolCount: metadata.tools.length,
336 | 										promptCount: metadata.prompts.length,
337 | 										resourceCount: metadata.resources.length,
338 | 										description:
339 | 											args.description || `MCP Tool: ${args.toolName}`,
340 | 										address: targetAddress,
341 | 									},
342 | 									null,
343 | 									2,
344 | 								),
345 | 							},
346 | 						],
347 | 					};
348 | 				}
349 | 				return {
350 | 					content: [
351 | 						{
352 | 							type: "text",
353 | 							text: changeResult.tx.toHex(),
354 | 						},
355 | 					],
356 | 				};
357 | 			} catch (err: unknown) {
358 | 				const msg = err instanceof Error ? err.message : String(err);
359 | 				return { content: [{ type: "text", text: msg }], isError: true };
360 | 			}
361 | 		},
362 | 	);
363 | }
364 | 
```

--------------------------------------------------------------------------------
/tools/bsv/explore.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
  2 | import { z } from "zod";
  3 | 
  4 | // Base URL for WhatsOnChain API
  5 | const WOC_API_BASE_URL = "https://api.whatsonchain.com/v1/bsv";
  6 | 
  7 | /**
  8 |  * WhatsOnChain API endpoints available for exploration
  9 |  */
 10 | enum ExploreEndpoint {
 11 | 	// Chain endpoints
 12 | 	CHAIN_INFO = "chain_info",
 13 | 	CHAIN_TIPS = "chain_tips",
 14 | 	CIRCULATING_SUPPLY = "circulating_supply",
 15 | 	PEER_INFO = "peer_info",
 16 | 
 17 | 	// Block endpoints
 18 | 	BLOCK_BY_HASH = "block_by_hash",
 19 | 	BLOCK_BY_HEIGHT = "block_by_height",
 20 | 	BLOCK_HEADERS = "block_headers",
 21 | 	BLOCK_PAGES = "block_pages",
 22 | 
 23 | 	// Stats endpoints
 24 | 	TAG_COUNT_BY_HEIGHT = "tag_count_by_height",
 25 | 	BLOCK_STATS_BY_HEIGHT = "block_stats_by_height",
 26 | 	BLOCK_MINER_STATS = "block_miner_stats",
 27 | 	MINER_SUMMARY_STATS = "miner_summary_stats",
 28 | 
 29 | 	// Transaction endpoints
 30 | 	TX_BY_HASH = "tx_by_hash",
 31 | 	TX_RAW = "tx_raw",
 32 | 	TX_RECEIPT = "tx_receipt",
 33 | 	BULK_TX_DETAILS = "bulk_tx_details",
 34 | 	ADDRESS_HISTORY = "address_history",
 35 | 	ADDRESS_UTXOS = "address_utxos",
 36 | 
 37 | 	// Health endpoint
 38 | 	HEALTH = "health",
 39 | }
 40 | 
 41 | enum Network {
 42 | 	MAIN = "main",
 43 | 	TEST = "test",
 44 | }
 45 | 
 46 | // Schema for the bsv_explore tool arguments
 47 | const exploreArgsSchema = z.object({
 48 | 	endpoint: z
 49 | 		.nativeEnum(ExploreEndpoint)
 50 | 		.describe("WhatsOnChain API endpoint to call"),
 51 | 	network: z
 52 | 		.nativeEnum(Network)
 53 | 		.default(Network.MAIN)
 54 | 		.describe("Network to use (main or test)"),
 55 | 
 56 | 	// Parameters for specific endpoints
 57 | 	blockHash: z
 58 | 		.string()
 59 | 		.optional()
 60 | 		.describe("Block hash (required for block_by_hash endpoint)"),
 61 | 	blockHeight: z
 62 | 		.number()
 63 | 		.optional()
 64 | 		.describe(
 65 | 			"Block height (required for block_by_height and block_stats_by_height endpoints)",
 66 | 		),
 67 | 	txHash: z
 68 | 		.string()
 69 | 		.optional()
 70 | 		.describe(
 71 | 			"Transaction hash (required for tx_by_hash, tx_raw, and tx_receipt endpoints)",
 72 | 		),
 73 | 	txids: z
 74 | 		.array(z.string())
 75 | 		.optional()
 76 | 		.describe("Array of transaction IDs for bulk_tx_details endpoint"),
 77 | 	address: z
 78 | 		.string()
 79 | 		.optional()
 80 | 		.describe(
 81 | 			"Bitcoin address (required for address_history and address_utxos endpoints)",
 82 | 		),
 83 | 	limit: z
 84 | 		.number()
 85 | 		.optional()
 86 | 		.describe("Limit for paginated results (optional for address_history)"),
 87 | 	pageNumber: z
 88 | 		.number()
 89 | 		.optional()
 90 | 		.describe("Page number for block_pages endpoint (defaults to 1)"),
 91 | 	days: z
 92 | 		.number()
 93 | 		.optional()
 94 | 		.describe("Number of days for miner stats endpoints (defaults to 7)"),
 95 | });
 96 | 
 97 | // Type for the tool arguments
 98 | type ExploreArgs = z.infer<typeof exploreArgsSchema>;
 99 | 
100 | /**
101 |  * Register the bsv_explore tool with the MCP server
102 |  * @param server The MCP server instance
103 |  */
104 | export function registerExploreTool(server: McpServer): void {
105 | 	server.tool(
106 | 		"bsv_explore",
107 | 		"Explore Bitcoin SV blockchain data using the WhatsOnChain API. Access multiple data types:\n\n" +
108 | 			"CHAIN DATA:\n" +
109 | 			"- chain_info: Network stats, difficulty, and chain work\n" +
110 | 			"- chain_tips: Current chain tips including heights and states\n" +
111 | 			"- circulating_supply: Current BSV circulating supply\n" +
112 | 			"- peer_info: Connected peer statistics\n\n" +
113 | 			"BLOCK DATA:\n" +
114 | 			"- block_by_hash: Complete block data via hash (requires blockHash parameter)\n" +
115 | 			"- block_by_height: Complete block data via height (requires blockHeight parameter)\n" +
116 | 			"- tag_count_by_height: Stats on tag count for a specific block via height (requires blockHeight parameter)\n" +
117 | 			"- block_headers: Retrieves the last 10 block headers\n" +
118 | 			"- block_pages: Retrieves pages of transaction IDs for large blocks (requires blockHash and optional pageNumber)\n\n" +
119 | 			"STATS DATA:\n" +
120 | 			"- block_stats_by_height: Block statistics for a specific height (requires blockHeight parameter)\n" +
121 | 			"- block_miner_stats: Block mining statistics for a time period (optional days parameter, default 7)\n" +
122 | 			"- miner_summary_stats: Summary of mining statistics (optional days parameter, default 7)\n\n" +
123 | 			"TRANSACTION DATA:\n" +
124 | 			"- tx_by_hash: Detailed transaction data (requires txHash parameter)\n" +
125 | 			"- tx_raw: Raw transaction hex data (requires txHash parameter)\n" +
126 | 			"- tx_receipt: Transaction receipt (requires txHash parameter)\n" +
127 | 			"- bulk_tx_details: Bulk transaction details (requires txids parameter as array of transaction hashes)\n\n" +
128 | 			"ADDRESS DATA:\n" +
129 | 			"- address_history: Transaction history for address (requires address parameter, optional limit)\n" +
130 | 			"- address_utxos: Unspent outputs for address (requires address parameter)\n\n" +
131 | 			"NETWORK:\n" +
132 | 			"- health: API health check\n\n" +
133 | 			"Use the appropriate parameters for each endpoint type and specify 'main' or 'test' network.",
134 | 		{ args: exploreArgsSchema },
135 | 		async ({ args }) => {
136 | 			try {
137 | 				const params = exploreArgsSchema.parse(args);
138 | 
139 | 				// Validate required parameters for specific endpoints
140 | 				if (
141 | 					params.endpoint === ExploreEndpoint.BLOCK_BY_HASH &&
142 | 					!params.blockHash
143 | 				) {
144 | 					throw new Error("blockHash is required for block_by_hash endpoint");
145 | 				}
146 | 
147 | 				if (
148 | 					(params.endpoint === ExploreEndpoint.BLOCK_BY_HEIGHT ||
149 | 						params.endpoint === ExploreEndpoint.TAG_COUNT_BY_HEIGHT) &&
150 | 					params.blockHeight === undefined
151 | 				) {
152 | 					throw new Error(
153 | 						"blockHeight is required for block_by_height and tag_count_by_height endpoints",
154 | 					);
155 | 				}
156 | 
157 | 				if (
158 | 					[
159 | 						ExploreEndpoint.TX_BY_HASH,
160 | 						ExploreEndpoint.TX_RAW,
161 | 						ExploreEndpoint.TX_RECEIPT,
162 | 					].includes(params.endpoint) &&
163 | 					!params.txHash
164 | 				) {
165 | 					throw new Error("txHash is required for transaction endpoints");
166 | 				}
167 | 
168 | 				if (
169 | 					[
170 | 						ExploreEndpoint.ADDRESS_HISTORY,
171 | 						ExploreEndpoint.ADDRESS_UTXOS,
172 | 					].includes(params.endpoint) &&
173 | 					!params.address
174 | 				) {
175 | 					throw new Error("address is required for address endpoints");
176 | 				}
177 | 
178 | 				if (
179 | 					params.endpoint === ExploreEndpoint.BLOCK_PAGES &&
180 | 					!params.blockHash
181 | 				) {
182 | 					throw new Error("blockHash is required for block_pages endpoint");
183 | 				}
184 | 
185 | 				if (
186 | 					params.endpoint === ExploreEndpoint.BLOCK_STATS_BY_HEIGHT &&
187 | 					params.blockHeight === undefined
188 | 				) {
189 | 					throw new Error(
190 | 						"blockHeight is required for block_stats_by_height endpoint",
191 | 					);
192 | 				}
193 | 
194 | 				if (
195 | 					params.endpoint === ExploreEndpoint.BULK_TX_DETAILS &&
196 | 					(!params.txids || params.txids.length === 0)
197 | 				) {
198 | 					throw new Error(
199 | 						"txids array is required for bulk_tx_details endpoint",
200 | 					);
201 | 				}
202 | 
203 | 				// Build API URL based on the selected endpoint
204 | 				let apiUrl = `${WOC_API_BASE_URL}/${params.network}`;
205 | 
206 | 				switch (params.endpoint) {
207 | 					case ExploreEndpoint.CHAIN_INFO:
208 | 						apiUrl += "/chain/info";
209 | 						break;
210 | 					case ExploreEndpoint.CHAIN_TIPS:
211 | 						apiUrl += "/chain/tips";
212 | 						break;
213 | 					case ExploreEndpoint.CIRCULATING_SUPPLY:
214 | 						apiUrl += "/circulatingsupply";
215 | 						break;
216 | 					case ExploreEndpoint.PEER_INFO:
217 | 						apiUrl += "/peer/info";
218 | 						break;
219 | 					case ExploreEndpoint.BLOCK_BY_HASH:
220 | 						apiUrl += `/block/hash/${params.blockHash}`;
221 | 						break;
222 | 					case ExploreEndpoint.BLOCK_BY_HEIGHT:
223 | 						apiUrl += `/block/height/${params.blockHeight}`;
224 | 						break;
225 | 					case ExploreEndpoint.TAG_COUNT_BY_HEIGHT:
226 | 						apiUrl += `/block/tagcount/height/${params.blockHeight}/stats`;
227 | 						break;
228 | 					case ExploreEndpoint.BLOCK_HEADERS:
229 | 						apiUrl += "/block/headers";
230 | 						break;
231 | 					case ExploreEndpoint.BLOCK_PAGES: {
232 | 						const pageNumber = params.pageNumber || 1;
233 | 						apiUrl += `/block/hash/${params.blockHash}/page/${pageNumber}`;
234 | 						break;
235 | 					}
236 | 					case ExploreEndpoint.BLOCK_STATS_BY_HEIGHT:
237 | 						apiUrl += `/block/height/${params.blockHeight}/stats`;
238 | 						break;
239 | 					case ExploreEndpoint.BLOCK_MINER_STATS: {
240 | 						const days = params.days !== undefined ? params.days : 7;
241 | 						apiUrl += `/miner/blocks/stats?days=${days}`;
242 | 						break;
243 | 					}
244 | 					case ExploreEndpoint.MINER_SUMMARY_STATS: {
245 | 						const days = params.days !== undefined ? params.days : 7;
246 | 						apiUrl += `/miner/summary/stats?days=${days}`;
247 | 						break;
248 | 					}
249 | 					case ExploreEndpoint.TX_BY_HASH:
250 | 						apiUrl += `/tx/hash/${params.txHash}`;
251 | 						break;
252 | 					case ExploreEndpoint.TX_RAW:
253 | 						apiUrl += `/tx/${params.txHash}/hex`;
254 | 						break;
255 | 					case ExploreEndpoint.TX_RECEIPT:
256 | 						apiUrl += `/tx/${params.txHash}/receipt`;
257 | 						break;
258 | 					case ExploreEndpoint.BULK_TX_DETAILS:
259 | 						apiUrl += "/txs";
260 | 						break;
261 | 					case ExploreEndpoint.ADDRESS_HISTORY:
262 | 						apiUrl += `/address/${params.address}/history`;
263 | 						if (params.limit !== undefined) {
264 | 							apiUrl += `?limit=${params.limit}`;
265 | 						}
266 | 						break;
267 | 					case ExploreEndpoint.ADDRESS_UTXOS:
268 | 						apiUrl += `/address/${params.address}/unspent`;
269 | 						break;
270 | 					case ExploreEndpoint.HEALTH:
271 | 						apiUrl += "/woc";
272 | 						break;
273 | 					default:
274 | 						throw new Error(`Unsupported endpoint: ${params.endpoint}`);
275 | 				}
276 | 
277 | 				// Special handling for bulk_tx_details which requires a POST request
278 | 				if (params.endpoint === ExploreEndpoint.BULK_TX_DETAILS) {
279 | 					const response = await fetch(apiUrl, {
280 | 						method: "POST",
281 | 						headers: {
282 | 							"Content-Type": "application/json",
283 | 						},
284 | 						body: JSON.stringify({ txids: params.txids }),
285 | 					});
286 | 
287 | 					if (!response.ok) {
288 | 						let errorMsg = `API error: ${response.status} ${response.statusText}`;
289 | 						if (response.status === 404) {
290 | 							errorMsg += " - One or more transaction IDs not found";
291 | 						}
292 | 						throw new Error(errorMsg);
293 | 					}
294 | 
295 | 					const result = await response.json();
296 | 
297 | 					return {
298 | 						content: [
299 | 							{
300 | 								type: "text",
301 | 								text: JSON.stringify(result, null, 2),
302 | 							},
303 | 						],
304 | 					};
305 | 				}
306 | 
307 | 				// For all other endpoints, use GET request
308 | 				const response = await fetch(apiUrl);
309 | 				if (!response.ok) {
310 | 					let errorMsg = `API error: ${response.status} ${response.statusText}`;
311 | 					// Provide helpful tips for specific error codes
312 | 					if (response.status === 404) {
313 | 						switch (params.endpoint) {
314 | 							case ExploreEndpoint.BLOCK_BY_HASH:
315 | 								errorMsg += ` - Block hash "${params.blockHash}" not found`;
316 | 								break;
317 | 							case ExploreEndpoint.BLOCK_BY_HEIGHT:
318 | 								errorMsg += ` - Block at height ${params.blockHeight} not found`;
319 | 								break;
320 | 							case ExploreEndpoint.BLOCK_PAGES:
321 | 								errorMsg += ` - Block hash "${params.blockHash}" not found or page ${params.pageNumber || 1} does not exist`;
322 | 								break;
323 | 							case ExploreEndpoint.TX_BY_HASH:
324 | 							case ExploreEndpoint.TX_RAW:
325 | 							case ExploreEndpoint.TX_RECEIPT:
326 | 								errorMsg += ` - Transaction hash "${params.txHash}" not found`;
327 | 								break;
328 | 							case ExploreEndpoint.ADDRESS_HISTORY:
329 | 							case ExploreEndpoint.ADDRESS_UTXOS:
330 | 								errorMsg += ` - No data found for address "${params.address}"`;
331 | 								break;
332 | 						}
333 | 					}
334 | 					throw new Error(errorMsg);
335 | 				}
336 | 
337 | 				const result = await response.json();
338 | 
339 | 				return {
340 | 					content: [
341 | 						{
342 | 							type: "text",
343 | 							text: JSON.stringify(result, null, 2),
344 | 						},
345 | 					],
346 | 				};
347 | 			} catch (error) {
348 | 				return {
349 | 					content: [
350 | 						{
351 | 							type: "text",
352 | 							text: `Error: ${error instanceof Error ? error.message : String(error)}`,
353 | 						},
354 | 					],
355 | 					isError: true,
356 | 				};
357 | 			}
358 | 		},
359 | 	);
360 | }
361 | 
```

--------------------------------------------------------------------------------
/resources/brcs.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import {
  2 | 	type McpServer,
  3 | 	ResourceTemplate,
  4 | } from "@modelcontextprotocol/sdk/server/mcp.js";
  5 | 
  6 | /**
  7 |  * BRC Categories for organizing the Bitcoin Request for Comments specifications
  8 |  */
  9 | export enum BRCCategory {
 10 | 	Wallet = "wallet",
 11 | 	Transactions = "transactions",
 12 | 	Scripts = "scripts",
 13 | 	Tokens = "tokens",
 14 | 	Overlays = "overlays",
 15 | 	Payments = "payments",
 16 | 	PeerToPeer = "peer-to-peer",
 17 | 	KeyDerivation = "key-derivation",
 18 | 	Outpoints = "outpoints",
 19 | 	Opinions = "opinions",
 20 | 	StateMachines = "state-machines",
 21 | 	Apps = "apps",
 22 | }
 23 | 
 24 | /**
 25 |  * Interface defining a BRC document
 26 |  */
 27 | interface BRCDocument {
 28 | 	number: string;
 29 | 	title: string;
 30 | 	category: BRCCategory;
 31 | }
 32 | 
 33 | /**
 34 |  * Register all BRC-related resources with the MCP server
 35 |  * @param server The MCP server instance
 36 |  */
 37 | export function registerBRCsResources(server: McpServer): void {
 38 | 	// Register BRCs repository main README
 39 | 	server.resource(
 40 | 		"brcs_readme",
 41 | 		"https://raw.githubusercontent.com/bitcoin-sv/BRCs/master/README.md",
 42 | 		{
 43 | 			title: "Bitcoin SV BRCs Overview",
 44 | 			description:
 45 | 				"Overview of all Bitcoin SV protocol specifications in the BRCs repository",
 46 | 		},
 47 | 		async (uri) => {
 48 | 			const resp = await fetch(uri.href);
 49 | 			const text = await resp.text();
 50 | 			return {
 51 | 				contents: [
 52 | 					{
 53 | 						uri: uri.href,
 54 | 						text,
 55 | 					},
 56 | 				],
 57 | 			};
 58 | 		},
 59 | 	);
 60 | 
 61 | 	// Register SUMMARY file which has the ToC
 62 | 	server.resource(
 63 | 		"brcs_summary",
 64 | 		"https://raw.githubusercontent.com/bitcoin-sv/BRCs/master/SUMMARY.md",
 65 | 		{
 66 | 			title: "Bitcoin SV BRCs Summary",
 67 | 			description: "Table of contents for all Bitcoin SV BRCs",
 68 | 		},
 69 | 		async (uri) => {
 70 | 			const resp = await fetch(uri.href);
 71 | 			const text = await resp.text();
 72 | 			return {
 73 | 				contents: [
 74 | 					{
 75 | 						uri: uri.href,
 76 | 						text,
 77 | 					},
 78 | 				],
 79 | 			};
 80 | 		},
 81 | 	);
 82 | 
 83 | 	// Add a dynamic BRC specification resource for any BRC by path
 84 | 	server.resource(
 85 | 		"brc_spec",
 86 | 		new ResourceTemplate("brc://{category}/{brcNumber}", { list: undefined }),
 87 | 		{
 88 | 			title: "Bitcoin SV BRC Specification",
 89 | 			description: "Access specific BRC specifications by category and number",
 90 | 		},
 91 | 		async (uri, { category, brcNumber }) => {
 92 | 			const path = `${category}/${brcNumber}.md`;
 93 | 			const resp = await fetch(
 94 | 				`https://raw.githubusercontent.com/bitcoin-sv/BRCs/master/${path}`,
 95 | 			);
 96 | 
 97 | 			if (!resp.ok) {
 98 | 				throw new Error(`BRC specification not found: ${path}`);
 99 | 			}
100 | 
101 | 			const text = await resp.text();
102 | 			return {
103 | 				contents: [
104 | 					{
105 | 						uri: uri.href,
106 | 						text,
107 | 					},
108 | 				],
109 | 			};
110 | 		},
111 | 	);
112 | 
113 | 	// Register individual BRCs
114 | 	registerAllBRCs(server);
115 | }
116 | 
117 | /**
118 |  * Register all BRC specifications from the master list
119 |  */
120 | function registerAllBRCs(server: McpServer): void {
121 | 	// Complete list of BRCs from the README.md
122 | 	const brcs: BRCDocument[] = [
123 | 		{
124 | 			number: "0",
125 | 			title: "Banana-Powered Bitcoin Wallet Control Protocol",
126 | 			category: BRCCategory.Wallet,
127 | 		},
128 | 		{
129 | 			number: "1",
130 | 			title: "Transaction Creation",
131 | 			category: BRCCategory.Wallet,
132 | 		},
133 | 		{
134 | 			number: "2",
135 | 			title: "Data Encryption and Decryption",
136 | 			category: BRCCategory.Wallet,
137 | 		},
138 | 		{
139 | 			number: "3",
140 | 			title: "Digital Signature Creation and Verification",
141 | 			category: BRCCategory.Wallet,
142 | 		},
143 | 		{ number: "4", title: "Input Redemption", category: BRCCategory.Wallet },
144 | 		{
145 | 			number: "5",
146 | 			title: "HTTP Wallet Communications Substrate",
147 | 			category: BRCCategory.Wallet,
148 | 		},
149 | 		{
150 | 			number: "6",
151 | 			title: "XDM Wallet Communications Substrate",
152 | 			category: BRCCategory.Wallet,
153 | 		},
154 | 		{
155 | 			number: "7",
156 | 			title: "Window Wallet Communication Substrate",
157 | 			category: BRCCategory.Wallet,
158 | 		},
159 | 		{
160 | 			number: "8",
161 | 			title: "Everett-style Transaction Envelopes",
162 | 			category: BRCCategory.Transactions,
163 | 		},
164 | 		{
165 | 			number: "9",
166 | 			title: "Simplified Payment Verification",
167 | 			category: BRCCategory.Transactions,
168 | 		},
169 | 		{
170 | 			number: "10",
171 | 			title: "Merkle proof standardised format",
172 | 			category: BRCCategory.Transactions,
173 | 		},
174 | 		{
175 | 			number: "11",
176 | 			title: "TSC Proof Format with Heights",
177 | 			category: BRCCategory.Transactions,
178 | 		},
179 | 		{
180 | 			number: "12",
181 | 			title: "Raw Transaction Format",
182 | 			category: BRCCategory.Transactions,
183 | 		},
184 | 		{
185 | 			number: "13",
186 | 			title: "TXO Transaction Object Format",
187 | 			category: BRCCategory.Transactions,
188 | 		},
189 | 		{
190 | 			number: "14",
191 | 			title: "Bitcoin Script Binary, Hex and ASM Formats",
192 | 			category: BRCCategory.Scripts,
193 | 		},
194 | 		{
195 | 			number: "15",
196 | 			title: "Bitcoin Script Assembly Language",
197 | 			category: BRCCategory.Scripts,
198 | 		},
199 | 		{
200 | 			number: "16",
201 | 			title: "Pay to Public Key Hash",
202 | 			category: BRCCategory.Scripts,
203 | 		},
204 | 		{
205 | 			number: "17",
206 | 			title: "Pay to R Puzzle Hash",
207 | 			category: BRCCategory.Scripts,
208 | 		},
209 | 		{
210 | 			number: "18",
211 | 			title: "Pay to False Return",
212 | 			category: BRCCategory.Scripts,
213 | 		},
214 | 		{
215 | 			number: "19",
216 | 			title: "Pay to True Return",
217 | 			category: BRCCategory.Scripts,
218 | 		},
219 | 		{ number: "20", title: "There is no BRC-20", category: BRCCategory.Tokens },
220 | 		{ number: "21", title: "Push TX", category: BRCCategory.Scripts },
221 | 		{
222 | 			number: "22",
223 | 			title: "Overlay Network Data Synchronization",
224 | 			category: BRCCategory.Overlays,
225 | 		},
226 | 		{
227 | 			number: "23",
228 | 			title: "Confederacy Host Interconnect Protocol (CHIP)",
229 | 			category: BRCCategory.Overlays,
230 | 		},
231 | 		{
232 | 			number: "24",
233 | 			title: "Overlay Network Lookup Services",
234 | 			category: BRCCategory.Overlays,
235 | 		},
236 | 		{
237 | 			number: "25",
238 | 			title: "Confederacy Lookup Availability Protocol (CLAP)",
239 | 			category: BRCCategory.Overlays,
240 | 		},
241 | 		{
242 | 			number: "26",
243 | 			title: "Universal Hash Resolution Protocol",
244 | 			category: BRCCategory.Overlays,
245 | 		},
246 | 		{
247 | 			number: "27",
248 | 			title: "Direct Payment Protocol (DPP)",
249 | 			category: BRCCategory.Payments,
250 | 		},
251 | 		{
252 | 			number: "28",
253 | 			title: "Paymail Payment Destinations",
254 | 			category: BRCCategory.Payments,
255 | 		},
256 | 		{
257 | 			number: "29",
258 | 			title: "Simple Authenticated BSV P2PKH Payment Protocol",
259 | 			category: BRCCategory.Payments,
260 | 		},
261 | 		{
262 | 			number: "30",
263 | 			title: "Transaction Extended Format (EF)",
264 | 			category: BRCCategory.Transactions,
265 | 		},
266 | 		{
267 | 			number: "31",
268 | 			title: "Authrite Mutual Authentication",
269 | 			category: BRCCategory.PeerToPeer,
270 | 		},
271 | 		{
272 | 			number: "32",
273 | 			title: "BIP32 Key Derivation Scheme",
274 | 			category: BRCCategory.KeyDerivation,
275 | 		},
276 | 		{
277 | 			number: "33",
278 | 			title: "PeerServ Message Relay Interface",
279 | 			category: BRCCategory.PeerToPeer,
280 | 		},
281 | 		{
282 | 			number: "34",
283 | 			title: "PeerServ Host Interconnect Protocol",
284 | 			category: BRCCategory.PeerToPeer,
285 | 		},
286 | 		// #35 is unused
287 | 		{
288 | 			number: "36",
289 | 			title: "Format for Bitcoin Outpoints",
290 | 			category: BRCCategory.Outpoints,
291 | 		},
292 | 		{
293 | 			number: "37",
294 | 			title: "Spending Instructions Extension for UTXO Storage Format",
295 | 			category: BRCCategory.Outpoints,
296 | 		},
297 | 		// 38, 39, 40 are placeholders in the README
298 | 		{
299 | 			number: "41",
300 | 			title: "PacketPay HTTP Payment Mechanism",
301 | 			category: BRCCategory.Payments,
302 | 		},
303 | 		{
304 | 			number: "42",
305 | 			title: "BSV Key Derivation Scheme (BKDS)",
306 | 			category: BRCCategory.KeyDerivation,
307 | 		},
308 | 		{
309 | 			number: "43",
310 | 			title: "Security Levels, Protocol IDs, Key IDs and Counterparties",
311 | 			category: BRCCategory.KeyDerivation,
312 | 		},
313 | 		{
314 | 			number: "44",
315 | 			title: "Admin-reserved and Prohibited Key Derivation Protocols",
316 | 			category: BRCCategory.KeyDerivation,
317 | 		},
318 | 		{
319 | 			number: "45",
320 | 			title: "Definition of UTXOs as Bitcoin Tokens",
321 | 			category: BRCCategory.Tokens,
322 | 		},
323 | 		{
324 | 			number: "46",
325 | 			title: "Wallet Transaction Output Tracking (Output Baskets)",
326 | 			category: BRCCategory.Wallet,
327 | 		},
328 | 		{
329 | 			number: "47",
330 | 			title: "Bare Multi-Signature",
331 | 			category: BRCCategory.Scripts,
332 | 		},
333 | 		{ number: "48", title: "Pay to Push Drop", category: BRCCategory.Scripts },
334 | 		{
335 | 			number: "49",
336 | 			title: "Users should never see an address",
337 | 			category: BRCCategory.Opinions,
338 | 		},
339 | 		{
340 | 			number: "50",
341 | 			title: "Submitting Received Payments to a Wallet",
342 | 			category: BRCCategory.Wallet,
343 | 		},
344 | 		{
345 | 			number: "51",
346 | 			title: "List of user experiences",
347 | 			category: BRCCategory.Opinions,
348 | 		},
349 | 		{
350 | 			number: "52",
351 | 			title: "Identity Certificates",
352 | 			category: BRCCategory.PeerToPeer,
353 | 		},
354 | 		{
355 | 			number: "53",
356 | 			title: "Certificate Creation and Revelation",
357 | 			category: BRCCategory.Wallet,
358 | 		},
359 | 		{
360 | 			number: "54",
361 | 			title: "Hybrid Payment Mode for DPP",
362 | 			category: BRCCategory.Payments,
363 | 		},
364 | 		{
365 | 			number: "55",
366 | 			title: "HTTPS Transport Mechanism for DPP",
367 | 			category: BRCCategory.Payments,
368 | 		},
369 | 		{
370 | 			number: "56",
371 | 			title: "Unified Abstract Wallet-to-Application Messaging Layer",
372 | 			category: BRCCategory.Wallet,
373 | 		},
374 | 		{
375 | 			number: "57",
376 | 			title: "Legitimate Uses for mAPI",
377 | 			category: BRCCategory.Opinions,
378 | 		},
379 | 		{
380 | 			number: "58",
381 | 			title: "Merkle Path JSON format",
382 | 			category: BRCCategory.Transactions,
383 | 		},
384 | 		{
385 | 			number: "59",
386 | 			title: "Security and Scalability Benefits of UTXO-based Overlay Networks",
387 | 			category: BRCCategory.Opinions,
388 | 		},
389 | 		{
390 | 			number: "60",
391 | 			title: "Simplifying State Machine Event Chains in Bitcoin",
392 | 			category: BRCCategory.StateMachines,
393 | 		},
394 | 		{
395 | 			number: "61",
396 | 			title: "Compound Merkle Path Format",
397 | 			category: BRCCategory.Transactions,
398 | 		},
399 | 		{
400 | 			number: "62",
401 | 			title: "Background Evaluation Extended Format (BEEF) Transactions",
402 | 			category: BRCCategory.Transactions,
403 | 		},
404 | 		{
405 | 			number: "63",
406 | 			title: "Genealogical Identity Protocol",
407 | 			category: BRCCategory.PeerToPeer,
408 | 		},
409 | 		{
410 | 			number: "64",
411 | 			title: "Overlay Network Transaction History Tracking",
412 | 			category: BRCCategory.Overlays,
413 | 		},
414 | 		{
415 | 			number: "65",
416 | 			title: "Transaction Labels and List Actions",
417 | 			category: BRCCategory.Wallet,
418 | 		},
419 | 		{
420 | 			number: "66",
421 | 			title: "Output Basket Removal and Certificate Deletion",
422 | 			category: BRCCategory.Wallet,
423 | 		},
424 | 		{
425 | 			number: "67",
426 | 			title: "Simplified Payment Verification",
427 | 			category: BRCCategory.Transactions,
428 | 		},
429 | 		{
430 | 			number: "68",
431 | 			title: "Publishing Trust Anchor Details at an Internet Domain",
432 | 			category: BRCCategory.PeerToPeer,
433 | 		},
434 | 		{
435 | 			number: "69",
436 | 			title: "Revealing Key Linkages",
437 | 			category: BRCCategory.KeyDerivation,
438 | 		},
439 | 		{
440 | 			number: "70",
441 | 			title: "Paymail BEEF Transaction",
442 | 			category: BRCCategory.Payments,
443 | 		},
444 | 		{
445 | 			number: "71",
446 | 			title: "Merkle Path Binary Format",
447 | 			category: BRCCategory.Transactions,
448 | 		},
449 | 		{
450 | 			number: "72",
451 | 			title: "Protecting BRC-69 Key Linkage Information in Transit",
452 | 			category: BRCCategory.KeyDerivation,
453 | 		},
454 | 		{
455 | 			number: "73",
456 | 			title: "Group Permissions for App Access",
457 | 			category: BRCCategory.Wallet,
458 | 		},
459 | 		{
460 | 			number: "74",
461 | 			title: "BSV Unified Merkle Path (BUMP) Format",
462 | 			category: BRCCategory.Transactions,
463 | 		},
464 | 		{
465 | 			number: "75",
466 | 			title: "Mnemonic For Master Private Key",
467 | 			category: BRCCategory.KeyDerivation,
468 | 		},
469 | 		{
470 | 			number: "76",
471 | 			title: "Graph Aware Sync Protocol",
472 | 			category: BRCCategory.Transactions,
473 | 		},
474 | 		{
475 | 			number: "77",
476 | 			title: "Message Signature Creation and Verification",
477 | 			category: BRCCategory.PeerToPeer,
478 | 		},
479 | 		{
480 | 			number: "78",
481 | 			title: "Serialization Format for Portable Encrypted Messages",
482 | 			category: BRCCategory.PeerToPeer,
483 | 		},
484 | 		{
485 | 			number: "79",
486 | 			title: "Token Exchange Protocol for UTXO-based Overlay Networks",
487 | 			category: BRCCategory.Tokens,
488 | 		},
489 | 		{
490 | 			number: "80",
491 | 			title: "Improving on MLD for BSV Multicast Services",
492 | 			category: BRCCategory.Opinions,
493 | 		},
494 | 		{
495 | 			number: "81",
496 | 			title: "Private Overlays with P2PKH Transactions",
497 | 			category: BRCCategory.Overlays,
498 | 		},
499 | 		{
500 | 			number: "82",
501 | 			title:
502 | 				"Defining a Scalable IPv6 Multicast Protocol for Blockchain Transaction Broadcast",
503 | 			category: BRCCategory.PeerToPeer,
504 | 		},
505 | 		{
506 | 			number: "83",
507 | 			title: "Scalable Transaction Processing in the BSV Network",
508 | 			category: BRCCategory.Transactions,
509 | 		},
510 | 		{
511 | 			number: "84",
512 | 			title: "Linked Key Derivation Scheme",
513 | 			category: BRCCategory.KeyDerivation,
514 | 		},
515 | 		{
516 | 			number: "85",
517 | 			title: "Proven Identity Key Exchange (PIKE)",
518 | 			category: BRCCategory.PeerToPeer,
519 | 		},
520 | 		{
521 | 			number: "86",
522 | 			title:
523 | 				"Bidirectionally Authenticated Derivation of Privacy Restricted Type 42 Keys",
524 | 			category: BRCCategory.KeyDerivation,
525 | 		},
526 | 		{
527 | 			number: "87",
528 | 			title:
529 | 				"Standardized Naming Conventions for BRC-22 Topic Managers and BRC-24 Lookup Services",
530 | 			category: BRCCategory.Overlays,
531 | 		},
532 | 		{
533 | 			number: "88",
534 | 			title: "Overlay Services Synchronization Architecture",
535 | 			category: BRCCategory.Overlays,
536 | 		},
537 | 		{
538 | 			number: "89",
539 | 			title: "Web 3.0 Standard (at a high level)",
540 | 			category: BRCCategory.Opinions,
541 | 		},
542 | 		{
543 | 			number: "90",
544 | 			title: "Thoughts on the Mandala Network",
545 | 			category: BRCCategory.Opinions,
546 | 		},
547 | 		{
548 | 			number: "91",
549 | 			title: "Outputs, Overlays, and Scripts in the Mandala Network",
550 | 			category: BRCCategory.Opinions,
551 | 		},
552 | 		{
553 | 			number: "92",
554 | 			title: "Mandala Token Protocol",
555 | 			category: BRCCategory.Tokens,
556 | 		},
557 | 		{
558 | 			number: "93",
559 | 			title: "Limitations of BRC-69 Key Linkage Revelation",
560 | 			category: BRCCategory.KeyDerivation,
561 | 		},
562 | 		{
563 | 			number: "94",
564 | 			title: "Verifiable Revelation of Shared Secrets Using Schnorr Protocol",
565 | 			category: BRCCategory.KeyDerivation,
566 | 		},
567 | 		{
568 | 			number: "95",
569 | 			title: "Atomic BEEF Transactions",
570 | 			category: BRCCategory.Transactions,
571 | 		},
572 | 		{
573 | 			number: "96",
574 | 			title: "BEEF V2 Txid Only Extension",
575 | 			category: BRCCategory.Transactions,
576 | 		},
577 | 		{
578 | 			number: "97",
579 | 			title: "Extensible Proof-Type Format for Specific Key Linkage Claims",
580 | 			category: BRCCategory.Wallet,
581 | 		},
582 | 		{
583 | 			number: "98",
584 | 			title: "P Protocols: Allowing future wallet protocol permission schemes",
585 | 			category: BRCCategory.Wallet,
586 | 		},
587 | 		{
588 | 			number: "99",
589 | 			title:
590 | 				"P Baskets: Allowing Future Wallet Basket and Digital Asset Permission Schemes",
591 | 			category: BRCCategory.Wallet,
592 | 		},
593 | 		{
594 | 			number: "100",
595 | 			title:
596 | 				"Unified, Vendor-Neutral, Unchanging, and Open BSV Blockchain Standard Wallet-to-Application Interface",
597 | 			category: BRCCategory.Wallet,
598 | 		},
599 | 		{
600 | 			number: "101",
601 | 			title:
602 | 				"Diverse Facilitators and URL Protocols for SHIP and SLAP Overlay Advertisements",
603 | 			category: BRCCategory.Overlays,
604 | 		},
605 | 		{
606 | 			number: "102",
607 | 			title: "The deployment-info.json Specification",
608 | 			category: BRCCategory.Apps,
609 | 		},
610 | 		{
611 | 			number: "103",
612 | 			title:
613 | 				"Peer-to-Peer Mutual Authentication and Certificate Exchange Protocol",
614 | 			category: BRCCategory.PeerToPeer,
615 | 		},
616 | 		{
617 | 			number: "104",
618 | 			title: "HTTP Transport for BRC-103 Mutual Authentication",
619 | 			category: BRCCategory.PeerToPeer,
620 | 		},
621 | 		{
622 | 			number: "105",
623 | 			title: "HTTP Service Monetization Framework",
624 | 			category: BRCCategory.Payments,
625 | 		},
626 | 	];
627 | 
628 | 	// Register each BRC
629 | 	for (const brc of brcs) {
630 | 		// Format the BRC number with leading zeros for consistency in resource IDs
631 | 		const paddedNumber = brc.number.padStart(4, "0");
632 | 
633 | 		// Use a shorter resource ID that includes a relevant key from the title
634 | 		const titleKeywords = brc.title.replace(/[^a-zA-Z0-9 ]/g, "").split(" ");
635 | 		const keyword =
636 | 			titleKeywords.find(
637 | 				(word) =>
638 | 					word.length > 3 &&
639 | 					![
640 | 						"with",
641 | 						"from",
642 | 						"that",
643 | 						"this",
644 | 						"your",
645 | 						"when",
646 | 						"then",
647 | 						"them",
648 | 						"they",
649 | 						"bitcoin",
650 | 					].includes(word.toLowerCase()),
651 | 			) ||
652 | 			titleKeywords[0] ||
653 | 			"brc";
654 | 
655 | 		// Create a descriptive resource ID
656 | 		const resourceId = `brc_${paddedNumber}_${keyword.toLowerCase()}`;
657 | 
658 | 		// Build the URL to the BRC file
659 | 		const url = `https://raw.githubusercontent.com/bitcoin-sv/BRCs/master/${brc.category}/${brc.number.padStart(4, "0")}.md`;
660 | 
661 | 		server.resource(
662 | 			resourceId,
663 | 			url,
664 | 			{
665 | 				title: `BRC-${brc.number}: ${brc.title}`,
666 | 				description: `Bitcoin SV BRC-${brc.number}: ${brc.title}`,
667 | 			},
668 | 			async (uri) => {
669 | 				const resp = await fetch(uri.href);
670 | 				if (!resp.ok) {
671 | 					throw new Error(`BRC-${brc.number} not found at ${uri.href}`);
672 | 				}
673 | 				const text = await resp.text();
674 | 				return {
675 | 					contents: [
676 | 						{
677 | 							uri: uri.href,
678 | 							text,
679 | 						},
680 | 					],
681 | 				};
682 | 			},
683 | 		);
684 | 	}
685 | }
686 | 
```

--------------------------------------------------------------------------------
/docs/a2b.md:
--------------------------------------------------------------------------------

```markdown
  1 | # A2B (Agent‑to‑Bitcoin)  
  2 | *A payment + discovery extension for Google A2A*
  3 | 
  4 | ---
  5 | 
  6 | ## Table of Contents
  7 | 1. [Introduction](#introduction)  
  8 | 2. [Motivation](#motivation)  
  9 | 3. [End‑to‑End Flow (Diagram)](#end-to-end-flow-diagram)  
 10 | 4. [Data Structures](#data-structures)  
 11 |    * 4.1 [Pricing Configuration (`x-payment-config`)](#pricing-configuration)  
 12 |    * 4.2 [Payment Claim (`x-payment` DataPart)](#payment-claim)  
 13 |    * 4.3 [Agent Card Examples](#agent-card-examples)  
 14 | 5. [Payment Flow](#payment-flow)
 15 |     * 5.1 [Two‑Stage Deposit Model](#two-stage-deposit-model)
 16 |     * 5.2 [FX Conversion & Price Feeds](#fx-conversion--price-feeds)
 17 |     * 5.3 [Error Codes](#error-codes)
 18 |     * 5.4 [Task State Machine](#task-state-machine)
 19 |     * 5.5 [Subscription Renewal Flow](#subscription-renewal-flow)
 20 | 6. [On‑Chain Registry](#on-chain-registry)
 21 |    * 6.1 [1Sat Ordinal + MAP Format](#1sat-ordinal--map-format)
 22 |    * 6.2 [Updating via Re‑inscription](#updating-via-re-inscription)
 23 |    * 6.3 [Overlay Search UX](#overlay-search-ux)
 24 |    * 6.4 [Cross‑Chain Registry Compatibility](#cross-chain-registry-compatibility)
 25 | 7. [Protocol Guidelines](#protocol-guidelines)
 26 | 8. [Security Considerations](#security-considerations)
 27 | 9. [Payment Verification Algorithm](#payment-verification-algorithm)
 28 | 10. [Implementation Guide](#implementation-guide)
 29 |     * 10.1 [Client](#client)  
 30 |     * 10.2 [Server](#server)  
 31 |     * 10.3 [Publisher](#publisher)
 32 | 
 33 | ---
 34 | 
 35 | ## 1  Introduction<a id="introduction"></a>
 36 | 
 37 | A2B (Agent‑to‑Bitcoin) is a modular extension to Google's Agent‑to‑Agent (A2A) protocol, introducing **native payment** and **decentralized discovery** capabilities. It comprises two interoperable components:
 38 | 
 39 | * **Payment Protocol** – Extends A2A's JSON‑RPC schema by embedding **signed cryptocurrency transactions** directly within task requests, enabling agents to require and process payments seamlessly.
 40 | * **Registry Protocol** – Utilizes **1Sat Ordinals** to inscribe agent metadata onto the blockchain, creating an immutable, decentralized registry that supports ownership transfer and updates.
 41 | 
 42 | These components can function **independently or in tandem**, providing flexibility for diverse use‑cases ranging from one‑off micro‑payments to long‑term subscription services.
 43 | 
 44 | ---
 45 | 
 46 | ## 2  Motivation<a id="motivation"></a>
 47 | 
 48 | AI agents are increasingly ubiquitous, yet two core requirements remain underserved:
 49 | 
 50 | 1. **Discovery** – Today, agent discovery relies on centralized registries or ad‑hoc web crawling. This both limits reach for smaller publishers and introduces single points of failure.
 51 | 2. **Monetization** – Payment flows typically depend on proprietary API keys and external billing systems, resulting in fragmented, less secure implementations.
 52 | 
 53 | A2B addresses these challenges by:
 54 | 
 55 | * **Democratizing Discovery** – By inscribing AgentCards as 1Sat Ordinals, any participant can run an indexer and surface agents without heavy infrastructure.
 56 | * **Streamlining Monetization** – Payment configurations (`x‑payment‑config`) live in the same AgentCard that advertises an agent's skills, while signed payment claims (`x‑payment`) travel with each request – eliminating external billing glue.
 57 | * **Enabling Ownership & Transferability** – The inscription model confers on‑chain ownership. Registry entries can be transferred or sold, allowing a marketplace of agent reputations and services.
 58 | 
 59 | Together, these features pave the way for a more **robust, decentralized, and monetizable** agent ecosystem that scales with the evolving needs of AI‑driven interactions.
 60 | 
 61 | ---
 62 | 
 63 | ## 3  End‑to‑End Flow (Diagram)<a id="end-to-end-flow-diagram"></a>
 64 | 
 65 | ```mermaid
 66 | sequenceDiagram
 67 |     autonumber
 68 |     participant C as A2B Client
 69 |     participant O as A2B Overlay
 70 |     participant B as Blockchain
 71 |     participant S as A2B Agent (A2A Server)
 72 |     participant T as MCP Server
 73 | 
 74 |     Note over O,B: Overlay indexes Blockchain (1Sat Ordinal + MAP) and exposes search
 75 | 
 76 |     C->>O: search("watchtower penalty")
 77 |     O->>B: query inscriptions
 78 |     B-->>O: matching entries
 79 |     O-->>C: results list (AgentCard hash + summary)
 80 | 
 81 |     C->>S: GET /.well-known/agent.json
 82 |     S-->>C: AgentCard JSON
 83 |     C->>C: verify hash == inscription
 84 | 
 85 |     C->>C: sign rawTx (deposit/full)
 86 |     C->>S: tasks/send + x‑payment
 87 | 
 88 |     
 89 |     S->>B: Validate payment claim
 90 | 
 91 |     alt Agent needs external compute
 92 |         S-->>T: mcp.tool.call(params)
 93 |         T-->>S: result / partial stream
 94 |     end
 95 | 
 96 |     S-->>C: TaskStatus / data stream
 97 | 
 98 |     S->>B: broadcast rawTx (deposit or full)
 99 | 
100 | ```
101 | 
102 | ---
103 | 
104 | ## 4  Data Structures<a id="data-structures"></a>
105 | 
106 | ### Type Definitions
107 | 
108 | ```typescript
109 | /** On‑chain pricing entry */
110 | export interface PricingConfig {
111 |   id: string;
112 |   name: string;
113 |   currency: string;            // anchor ticker
114 |   amount: number;              // price in anchor units
115 |   address: string;             // pay‑to address for anchor currency
116 |   acceptedCurrencies: string[]; // other tickers accepted
117 |   skillIds: string[];          // skills this price unlocks
118 |   interval?: 'day'|'week'|'month'|'year'|string|null;
119 |   description?: string|null;
120 |   depositPct?: number;         // 0‑1 for two‑stage payments
121 |   priceFeedUrl?: string;       // optional FX oracle
122 | }
123 | 
124 | /** DataPart claim sent with tasks/send */
125 | export interface PaymentClaim {
126 |   configId: string;
127 |   stage: 'deposit' | 'final' | 'full';
128 |   rawTx: string;               // hex‑encoded unsigned or signed tx
129 |   currency: string;            // ticker of UTXO value
130 |   refundAddress?: string;      // optional alt‑chain refund
131 | }
132 | ```
133 | 
134 | ### 4.1  Pricing Configuration<a id="pricing-configuration"></a>
135 | 
136 | **Minimal**
137 | 
138 | ```jsonc
139 | {
140 |   "id": "wt-basic",
141 |   "name": "Basic Pay‑Per‑Call",
142 |   "currency": "BSV",
143 |   "amount": 0.0005,
144 |   "address": "1WatchtowerAddr",
145 |   "acceptedCurrencies": ["BSV"],
146 |   "skillIds": ["watchChannels"]
147 | }
148 | ```
149 | 
150 | <details>
151 | <summary>Extensive</summary>
152 | 
153 | ```jsonc
154 | {
155 |   "id": "watchtower-18m",
156 |   "name": "18‑Month Enterprise Watchtower",
157 |   "currency": "BSV",
158 |   "amount": 0.030,
159 |   "address": "1WatchtowerAddr",
160 |   "acceptedCurrencies": ["BSV","BTC","USD"],
161 |   "depositPct": 0.20,
162 |   "priceFeedUrl": "https://oracle.example/spot",
163 |   "interval": "P18M",
164 |   "skillIds": ["watchChannels"],
165 |   "description": "Long‑term SLA — 20 % up‑front, 80 % on success."
166 | }
167 | ```
168 | </details>
169 | 
170 | ### 4.2  Payment Claim (`x-payment` DataPart)<a id="payment-claim"></a>
171 | 
172 | **Minimal**
173 | 
174 | ```jsonc
175 | {
176 |   "type": "data",
177 |   "data": {
178 |     "x-payment": {
179 |       "configId": "wt-basic",
180 |       "stage": "full",
181 |       "rawTx": "<hex>",
182 |       "currency": "BSV"
183 |     }
184 |   }
185 | }
186 | ```
187 | 
188 | <details>
189 | <summary>Extensive</summary>
190 | 
191 | ```jsonc
192 | {
193 |   "type": "data",
194 |   "data": {
195 |     "x-payment": {
196 |       "configId": "dex-chart-sub-month",
197 |       "stage": "deposit",
198 |       "rawTx": "<signed‑hex>",
199 |       "currency": "SOL",
200 |       "refundAddress": "solRefundPubKey"
201 |     }
202 |   }
203 | }
204 | ```
205 | </details>
206 | 
207 | ### 4.3  Agent Card Examples<a id="agent-card-examples"></a>
208 | 
209 | #### Watchtower Agent — Minimal
210 | 
211 | ```jsonc
212 | {
213 |   "name": "Tower‑Guard (Minimal)",
214 |   "url": "https://watchtower.example",
215 |   "version": "1.0.0",
216 |   "capabilities": {},
217 |   "skills": [
218 |     { "id": "watchChannels", "name": "Watch Lightning Channels" }
219 |   ],
220 |   "x-payment-config": [
221 |     { "id": "wt-basic", "name": "Basic Pay‑Per‑Call", "currency": "BSV", "amount": 0.0005, "address": "1WatchtowerAddr","acceptedCurrencies": ["BSV"],"skillIds": ["watchChannels"] }
222 |   ]
223 | }
224 | ```
225 | 
226 | <details>
227 | <summary>Watchtower Agent — Extensive</summary>
228 | 
229 | ```jsonc
230 | {
231 |   "name": "Tower‑Guard Watch Services",
232 |   "url": "https://watchtower.example",
233 |   "version": "2.1.0",
234 |   "capabilities": {
235 |     "streaming": true,
236 |     "pushNotifications": true,
237 |     "stateTransitionHistory": true
238 |   },
239 |   "skills": [
240 |     {
241 |       "id": "watchChannels",
242 |       "name": "Lightning Watchtower",
243 |       "description": "Monitors LN channels and broadcasts penalty transactions.",
244 |       "tags": ["lightning","security","fraud-prevention"],
245 |       "examples": [
246 |         "watch channel 0234abcd… for 30 days",
247 |         "monitor my node for revoked states"
248 |       ],
249 |       "inputModes": ["data"],
250 |       "outputModes": ["stream"]
251 |     }
252 |   ],
253 |   "x-payment-config": [
254 |     {
255 |       "id": "watchtower-month",
256 |       "name": "30‑Day Watchtower",
257 |       "currency": "BSV",
258 |       "amount": 0.002,
259 |       "address": "1WatchtowerAddr",
260 |       "acceptedCurrencies": ["BSV","BTC","USD"],
261 |       "interval": "month",
262 |       "skillIds": ["watchChannels"],
263 |       "description": "Penalty‑tx monitoring for 30 days."
264 |     },
265 |     {
266 |       "id": "watchtower-18m",
267 |       "name": "18‑Month Enterprise Watchtower",
268 |       "currency": "BSV",
269 |       "amount": 0.030,
270 |       "address": "1WatchtowerAddr",
271 |       "acceptedCurrencies": ["BSV","BTC","USD"],
272 |       "interval": "P18M",
273 |       "depositPct": 0.20,
274 |       "priceFeedUrl": "https://oracle.example/spot",
275 |       "skillIds": ["watchChannels"],
276 |       "description": "Long‑term SLA; 20 % deposit, 80 % on completion."
277 |     }
278 |   ]
279 | }
280 | ```
281 | </details>
282 | 
283 | #### DEX Chart Agent — Minimal
284 | 
285 | ```jsonc
286 | {
287 |   "name": "DEX Chart API (Minimal)",
288 |   "url": "https://dexcharts.example",
289 |   "version": "1.0.0",
290 |   "capabilities": {},
291 |   "skills": [
292 |     { "id": "getDexChart", "name": "DEX Chart JSON" }
293 |   ],
294 |   "x-payment-config": [
295 |     {
296 |       "id": "dex-chart-call",
297 |       "name": "Single OHLCV Snapshot",
298 |       "currency": "USD",
299 |       "amount": 0.05,
300 |       "address": "1DexDataAddr",
301 |       "acceptedCurrencies": ["USD"],
302 |       "skillIds": ["getDexChart"]
303 |     }
304 |   ]
305 | }
306 | ```
307 | 
308 | <details>
309 | <summary>DEX Chart Agent — Extensive</summary>
310 | 
311 | ```jsonc
312 | {
313 |   "name": "On‑Chain DEX Chart API",
314 |   "url": "https://dexcharts.example",
315 |   "version": "1.0.0",
316 |   "capabilities": { "streaming": false },
317 |   "skills": [
318 |     {
319 |       "id": "getDexChart",
320 |       "name": "DEX Chart JSON",
321 |       "description": "Returns OHLCV data for any on‑chain DEX pair.",
322 |       "tags": ["markets","dex","charts"],
323 |       "examples": ["dex chart BSV/USDC 1h 500"],
324 |       "inputModes": ["text"],
325 |       "outputModes": ["data"]
326 |     }
327 |   ],
328 |   "x-payment-config": [
329 |     {
330 |       "id": "dex-chart-call",
331 |       "name": "Single OHLCV Snapshot",
332 |       "currency": "USD",
333 |       "amount": 0.05,
334 |       "address": "1DexDataAddr",
335 |       "acceptedCurrencies": ["USD","BSV","SOL"],
336 |       "skillIds": ["getDexChart"],
337 |       "description": "Returns 500‑candle OHLCV JSON."
338 |     },
339 |     {
340 |       "id": "dex-chart-sub-month",
341 |       "name": "Unlimited Charts · 30 Days",
342 |       "currency": "USD",
343 |       "amount": 20,
344 |       "address": "1DexDataAddr",
345 |       "acceptedCurrencies": ["USD","BSV","SOL"],
346 |       "interval": "month",
347 |       "skillIds": ["getDexChart"],
348 |       "description": "Unlimited OHLCV queries for one month."
349 |     }
350 |   ]
351 | }
352 | ```
353 | </details>
354 | 
355 | ---
356 | 
357 | ## 5  Payment Flow<a id="payment-flow"></a>
358 | 
359 | A2B's payment logic has three variants — *full*, *deposit + final* and *subscription*. The client always signs a raw transaction but **only the server broadcasts** it once the task reaches the corresponding stage.
360 | 
361 | <details>
362 | <summary>Mermaid · Deposit vs Full Path</summary>
363 | 
364 | ```mermaid
365 | flowchart TD
366 |     A[Client signs rawTx] --> B{depositPct set?}
367 |     B -- No --> F[stage full]
368 |     F --> G[validate & broadcast]
369 |     G --> H[run task]
370 |     H --> I[done]
371 | 
372 |     B -- Yes --> C[stage deposit]
373 |     C --> D[validate]
374 |     D --> E[run task]
375 |     C --> D[broadcast]
376 |     E --> J{needs final?}
377 |     J -- Yes --> K[402 AmountInsufficient]
378 |     K --> L[Client signs final rawTx]
379 |     L --> M[stage final]
380 |     M --> N[validate & broadcast]
381 |     N --> I
382 | ```
383 | </details>
384 | 
385 | ### 5.1  Two‑Stage Deposit Model<a id="two-stage-deposit-model"></a>
386 | 
387 | | Stage   | RawTx ≥ (after FX) | Sender | Broadcast |
388 | |---------|--------------------|--------|-----------|
389 | | deposit | `amount × depositPct` | Client | Server |
390 | | final   | `amount − deposit`    | Client | Server |
391 | | full    | `amount`              | Client | Server |
392 | 
393 | ---
394 | 
395 | ### 5.2  FX Conversion & Price Feeds<a id="fx-conversion--price-feeds"></a>
396 | 
397 | <details>
398 | <summary>Mermaid · FX Math & Slippage</summary>
399 | 
400 | ```mermaid
401 | flowchart LR
402 |     A[anchor amount] --> B[spot rate]
403 |     B --> C[required = anchor×rate]
404 |     C --> D[apply slippage]
405 |     D --> E{UTXO ≥ required?}
406 |     E -- Yes --> F[valid]
407 |     E -- No  --> G[402 AmountInsufficient]
408 | ```
409 | </details>
410 | 
411 | * Oracle JSON example: `{ "rates": { "USD": 123.45, "BTC": 0.000014 } }`
412 | * Default slippage tolerance: **±1 %** (override per pricing config).
413 | 
414 | ### 5.3  Error Codes<a id="error-codes"></a>
415 | 
416 | | HTTP | JSON‑RPC code | Meaning                       |
417 | |------|---------------|-------------------------------|
418 | | 402  | `-32030`      | PaymentMissing                |
419 | | 402  | `-32031`      | PaymentInvalid (rawTx)        |
420 | | 402  | `-32032`      | StageMismatch                 |
421 | | 402  | `-32033`      | AmountInsufficient            |
422 | | 402  | `-32034`      | CurrencyUnsupported / AddressMismatch |
423 | 
424 | ---
425 | 
426 | ### 5.4  Task State Machine
427 | 
428 | <details>
429 | <summary>Mermaid · Task States</summary>
430 | 
431 | ```mermaid
432 | stateDiagram-v2
433 |     [*] --> pending
434 |     pending --> running : deposit/full valid
435 |     running --> needsFinal : 402
436 |     needsFinal --> running : final valid
437 |     running --> completed : success
438 |     running --> failed : error
439 |     needsFinal --> failed : timeout
440 | ```
441 | </details>
442 | 
443 | ---
444 | 
445 | ### 5.5  Subscription Renewal Flow
446 | 
447 | <details>
448 | <summary>Mermaid · Monthly Subscription Sequence</summary>
449 | 
450 | ```mermaid
451 | sequenceDiagram
452 |     autonumber
453 |     participant C as Client
454 |     participant S as Agent
455 |     participant B as Blockchain
456 | 
457 |     C->>S: tasks/send (+ x‑payment month 1)
458 |     S->>B: broadcast tx₁
459 |     S-->>C: data stream (month 1)
460 |     Note over C,S: 30 days pass
461 |     C->>S: tasks/send (+ x‑payment month 2)
462 |     S->>B: broadcast tx₂
463 |     S-->>C: data stream (month 2)
464 | ```
465 | </details>
466 | 
467 | ---
468 | 
469 | ## 6  On‑Chain Registry<a id="on-chain-registry"></a>
470 | 
471 | ### 6.1  Agent Card + A2B Metadata<a id="1sat-ordinal--map-format"></a>
472 | 
473 | ```
474 | Output 0 (1 sat):
475 |   <P2PKH>
476 |   OP_FALSE OP_IF
477 |     "ord"
478 |     OP_1 "application/json"
479 |     OP_0 <AgentCard bytes>
480 |   OP_ENDIF
481 |   OP_RETURN
482 |     1PuQa7K62MiKCtssSLKy1kh56WWU7MtUR5 SET
483 |     app your-app-name  type a2b-agent
484 | ```
485 | 
486 | ### 6.1b MCP Tool Registration
487 | 
488 | ```
489 | Output 0 (1 sat):
490 |   <P2PKH>
491 |   OP_FALSE OP_IF
492 |     "ord"
493 |     OP_1 "application/json"
494 |     OP_0 <MCP Config bytes>
495 |   OP_ENDIF
496 |   OP_RETURN
497 |     1PuQa7K62MiKCtssSLKy1kh56WWU7MtUR5 SET
498 |     app your-app-name  type a2b-mcp
499 | ```
500 | 
501 | ### 6.2  Updating via Re‑inscription<a id="updating-via-re-inscription"></a>
502 | 
503 | Spend the satoshi, attach a new envelope with updated card and identical MAP; newest wins.
504 | 
505 | ### 6.3  Overlay Search UX<a id="overlay-search-ux"></a>
506 | 
507 | * Fuzzy search over `name`, `description`, `tags`.  
508 | * Filters: `skillId`, `acceptedCurrency`, `interval`, `maxPrice`.  
509 | * Result card shows logo, summary, cheapest price, update height, rating.
510 | ### 6.4  Cross‑Chain Registry Compatibility
511 | 
512 | Full 1‑sat‑style inscriptions are effortless on **Bitcoin SV** (design target), but other ledgers impose tighter data caps or higher gas costs. The table ranks practicality for storing *the entire `agent.json`* versus a lightweight pointer.
513 | 
514 | | Chain | Native data capacity (per tx / script) | Full JSON on‑chain | Pointer variant¹ | Notes |
515 | |-------|----------------------------------------|--------------------|------------------|-------|
516 | | **Bitcoin SV** | OP_FALSE+ord ≈ 100 kB | ✅ | — | Native design. |
517 | | **Bitcoin (Taproot)** | Witness pushes ≤ 520 B, unlimited chunks (Ordinals) | ⚠️ costly | ✅ | Same inscription flow; high fee/weight. |
518 | | **Litecoin (Taproot)** | BTC rules, cheaper fees | ⚠️ | ✅ | LTC‑20 shows viability. |
519 | | **Bitcoin Cash** | OP_RETURN total 223 B | ❌ | ✅ | Store hash + URL only. |
520 | | **Dogecoin** | OP_RETURN 80 B | ❌ | ✅ | Use multi‑output "Stamps" style. |
521 | | **Ethereum / EVM** | Calldata / storage; ~16 k gas / byte | ⚠️ very expensive | ✅ | Emit LOG event with hash + IPFS; full storage = $$. |
522 | | **Solana** | Account data ≤ 10 MB | ✅ | — | Rent‑exempt account holds JSON. |
523 | | **Avalanche / Polygon / BSC** | EVM rules, cheaper gas | ⚠️ | ✅ | Pointer is practical; full JSON still big. |
524 | 
525 | **Legend**  ✅ feasible · ⚠️ feasible but high cost/complexity · ❌ impractical  
526 | ¹ *Pointer variant* = small MAP record containing a SHA‑256 hash and URI of the card.
527 | 
528 | #### Pointer Variant Specification
529 | 
530 | ```text
531 | OP_RETURN a2b <domain> <optional-32‑byte‑hex> 
532 | ```
533 | 
534 | > **Overlay behaviour** – When the indexer encounters a `hash / uri` record, it **must**:  
535 | > 1. Download the referenced `.well-known/agent.json`.  
536 | > 2. Verify `SHA‑256(body) == hash`.  
537 | > 3. Parse and ingest the JSON exactly as if it were stored on‑chain, so the agent's metadata remains fully searchable.
538 | 
539 | Using pointers lets *any* UTXO or EVM chain participate in A2B discovery with only ~75 bytes on‑chain, while still providing strong integrity via hash verification.
540 | 
541 | ---
542 | 
543 | #### Why BSV Comes First
544 | 
545 | * Supports large ord‑style envelopes in‑script, no size kludges.  
546 | * Ultra‑low fees (< $0.001 for 10 kB) enable updates & rich metadata.  
547 | * Re‑inscription model = identical to BTC/LTC Taproot flow, easing future multichain parity.
548 | 
549 | Other chains can join immediately via the pointer method, then migrate to full on‑chain JSON if/when their ecosystems adopt high‑capacity inscription tech (e.g., BTC ordinals, Ethereum blobs / proto‑danksharding  [oai_citation_attribution:9‡investopedia.com](https://www.investopedia.com/what-you-need-to-know-ahead-of-ethereum-dencun-update-wednesday-8607518?utm_source=chatgpt.com)).
550 | 
551 | ---
552 | 
553 | ## 7  Protocol Guidelines<a id="protocol-guidelines"></a>
554 | 
555 | | Scenario         | Recommendation |
556 | |------------------|----------------|
557 | | Immediate        | `stage:"full"`; server broadcasts on success. |
558 | | Streaming        | Deposit then `final`; server withholds last chunk until paid. |
559 | | Interactive      | One payment per task; switch to subscription for heavy usage. |
560 | | Long‑Running     | Deposit ≥ 50 % or milestone split. |
561 | | Subscription     | One config per billing interval (`interval`). |
562 | | MCP Payments     | Agent may embed nested A2B hop for downstream tools. |
563 | 
564 | ---
565 | 
566 | ## 8  Security Considerations<a id="security-considerations"></a>
567 | 
568 | <details>
569 | <summary>Mermaid · Threat Mitigation Flow</summary>
570 | 
571 | ```mermaid
572 | flowchart TD
573 |     A[Duplicate txid] --> B[validate]
574 |     B -->|seen| X[Reject -32031]
575 |     C[Fake oracle JSON] --> D[verify sig]
576 |     D -->|bad| X
577 |     E[Double spend] --> F[mempool check]
578 |     F -->|bad| X
579 |     H[Valid path] --> I[broadcast]
580 | ```
581 | </details>
582 | 
583 | * Grace timeout 30 min for missing `final`.  
584 | * Refund deposits on task expiry. 
585 | * Rate‑limit by satoshi/s & requests.  
586 | * Double‑spend detection before broadcast.  
587 | * Verify signed oracle feeds.
588 | 
589 | ---
590 | 
591 | ## 9  Payment Verification Algorithm<a id="payment-verification-algorithm"></a>
592 | 
593 | ```pseudo
594 | verifyAndBroadcast(rawTx, stage, cfg, payTicker):
595 |     tx   = decode(rawTx)
596 |     out  = findOutput(tx, cfg.address)
597 |     if !out           -> 32034
598 |     anchor = (stage=='deposit') ? cfg.amount*cfg.depositPct
599 |            : (stage=='final')   ? cfg.amount - cfg.amount*cfg.depositPct
600 |            : cfg.amount
601 |     rate = (payTicker==cfg.currency) ? 1
602 |          : fetchSpot(payTicker, cfg.currency, cfg.priceFeedUrl)
603 |     needed = anchor * rate
604 |     if out.value < needed*(1-slippage): -> 32033
605 |     if txidSeen(tx.id):                 -> 32031
606 |     broadcast(tx)
607 | ```
608 | 
609 | ---
610 | 
611 | ## 10  Implementation Guide<a id="implementation-guide"></a>
612 | 
613 | ### Client<a id="client"></a>
614 | 1. Search overlay; choose pricing config.  
615 | 2. Verify AgentCard hash.  
616 | 3. Sign rawTx.  
617 | 4. `tasks/send` + `x-payment`.  
618 | 5. Handle `402` (deposit model) & resend `final`.  
619 | 6. Watch mempool for server broadcast.
620 | 
621 | ### Server<a id="server"></a>
622 | 1. Inscribe AgentCard (`type=a2b`).  
623 | 2. Validate payment claim.  
624 | 3. Execute task; call MCP tools if needed.  
625 | 4. Broadcast rawTx; stream updates.  
626 | 5. Re‑inscribe satoshi for updates.
627 | 
628 | ### Publisher<a id="publisher"></a>
629 | 1. Publish AgentCard to overlay.  
630 | 2. Can be a local MCP tool like bsv-mcp
631 | 3. Either user or platform can pay the  network fee. 
632 | 4. Broadcast transactions.
633 | 
634 | ---
635 | 
636 | 
637 | *Specification version 2025‑04‑19.*
```
Page 2/2FirstPrevNextLast