Skip to main content
Glama

Bitcoin SV MCP Server

by b-open-io
explore.ts11.2 kB
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { z } from "zod"; // Base URL for WhatsOnChain API const WOC_API_BASE_URL = "https://api.whatsonchain.com/v1/bsv"; /** * WhatsOnChain API endpoints available for exploration */ enum ExploreEndpoint { // Chain endpoints CHAIN_INFO = "chain_info", CHAIN_TIPS = "chain_tips", CIRCULATING_SUPPLY = "circulating_supply", PEER_INFO = "peer_info", // Block endpoints BLOCK_BY_HASH = "block_by_hash", BLOCK_BY_HEIGHT = "block_by_height", BLOCK_HEADERS = "block_headers", BLOCK_PAGES = "block_pages", // Stats endpoints TAG_COUNT_BY_HEIGHT = "tag_count_by_height", BLOCK_STATS_BY_HEIGHT = "block_stats_by_height", BLOCK_MINER_STATS = "block_miner_stats", MINER_SUMMARY_STATS = "miner_summary_stats", // Transaction endpoints TX_BY_HASH = "tx_by_hash", TX_RAW = "tx_raw", TX_RECEIPT = "tx_receipt", BULK_TX_DETAILS = "bulk_tx_details", ADDRESS_HISTORY = "address_history", ADDRESS_UTXOS = "address_utxos", // Health endpoint HEALTH = "health", } enum Network { MAIN = "main", TEST = "test", } // Schema for the bsv_explore tool arguments const exploreArgsSchema = z.object({ endpoint: z .nativeEnum(ExploreEndpoint) .describe("WhatsOnChain API endpoint to call"), network: z .nativeEnum(Network) .default(Network.MAIN) .describe("Network to use (main or test)"), // Parameters for specific endpoints blockHash: z .string() .optional() .describe("Block hash (required for block_by_hash endpoint)"), blockHeight: z .number() .optional() .describe( "Block height (required for block_by_height and block_stats_by_height endpoints)", ), txHash: z .string() .optional() .describe( "Transaction hash (required for tx_by_hash, tx_raw, and tx_receipt endpoints)", ), txids: z .array(z.string()) .optional() .describe("Array of transaction IDs for bulk_tx_details endpoint"), address: z .string() .optional() .describe( "Bitcoin address (required for address_history and address_utxos endpoints)", ), limit: z .number() .optional() .describe("Limit for paginated results (optional for address_history)"), pageNumber: z .number() .optional() .describe("Page number for block_pages endpoint (defaults to 1)"), days: z .number() .optional() .describe("Number of days for miner stats endpoints (defaults to 7)"), }); // Type for the tool arguments type ExploreArgs = z.infer<typeof exploreArgsSchema>; /** * Register the bsv_explore tool with the MCP server * @param server The MCP server instance */ export function registerExploreTool(server: McpServer): void { server.tool( "bsv_explore", "Explore Bitcoin SV blockchain data using the WhatsOnChain API. Access multiple data types:\n\n" + "CHAIN DATA:\n" + "- chain_info: Network stats, difficulty, and chain work\n" + "- chain_tips: Current chain tips including heights and states\n" + "- circulating_supply: Current BSV circulating supply\n" + "- peer_info: Connected peer statistics\n\n" + "BLOCK DATA:\n" + "- block_by_hash: Complete block data via hash (requires blockHash parameter)\n" + "- block_by_height: Complete block data via height (requires blockHeight parameter)\n" + "- tag_count_by_height: Stats on tag count for a specific block via height (requires blockHeight parameter)\n" + "- block_headers: Retrieves the last 10 block headers\n" + "- block_pages: Retrieves pages of transaction IDs for large blocks (requires blockHash and optional pageNumber)\n\n" + "STATS DATA:\n" + "- block_stats_by_height: Block statistics for a specific height (requires blockHeight parameter)\n" + "- block_miner_stats: Block mining statistics for a time period (optional days parameter, default 7)\n" + "- miner_summary_stats: Summary of mining statistics (optional days parameter, default 7)\n\n" + "TRANSACTION DATA:\n" + "- tx_by_hash: Detailed transaction data (requires txHash parameter)\n" + "- tx_raw: Raw transaction hex data (requires txHash parameter)\n" + "- tx_receipt: Transaction receipt (requires txHash parameter)\n" + "- bulk_tx_details: Bulk transaction details (requires txids parameter as array of transaction hashes)\n\n" + "ADDRESS DATA:\n" + "- address_history: Transaction history for address (requires address parameter, optional limit)\n" + "- address_utxos: Unspent outputs for address (requires address parameter)\n\n" + "NETWORK:\n" + "- health: API health check\n\n" + "Use the appropriate parameters for each endpoint type and specify 'main' or 'test' network.", { args: exploreArgsSchema }, async ({ args }) => { try { const params = exploreArgsSchema.parse(args); // Validate required parameters for specific endpoints if ( params.endpoint === ExploreEndpoint.BLOCK_BY_HASH && !params.blockHash ) { throw new Error("blockHash is required for block_by_hash endpoint"); } if ( (params.endpoint === ExploreEndpoint.BLOCK_BY_HEIGHT || params.endpoint === ExploreEndpoint.TAG_COUNT_BY_HEIGHT) && params.blockHeight === undefined ) { throw new Error( "blockHeight is required for block_by_height and tag_count_by_height endpoints", ); } if ( [ ExploreEndpoint.TX_BY_HASH, ExploreEndpoint.TX_RAW, ExploreEndpoint.TX_RECEIPT, ].includes(params.endpoint) && !params.txHash ) { throw new Error("txHash is required for transaction endpoints"); } if ( [ ExploreEndpoint.ADDRESS_HISTORY, ExploreEndpoint.ADDRESS_UTXOS, ].includes(params.endpoint) && !params.address ) { throw new Error("address is required for address endpoints"); } if ( params.endpoint === ExploreEndpoint.BLOCK_PAGES && !params.blockHash ) { throw new Error("blockHash is required for block_pages endpoint"); } if ( params.endpoint === ExploreEndpoint.BLOCK_STATS_BY_HEIGHT && params.blockHeight === undefined ) { throw new Error( "blockHeight is required for block_stats_by_height endpoint", ); } if ( params.endpoint === ExploreEndpoint.BULK_TX_DETAILS && (!params.txids || params.txids.length === 0) ) { throw new Error( "txids array is required for bulk_tx_details endpoint", ); } // Build API URL based on the selected endpoint let apiUrl = `${WOC_API_BASE_URL}/${params.network}`; switch (params.endpoint) { case ExploreEndpoint.CHAIN_INFO: apiUrl += "/chain/info"; break; case ExploreEndpoint.CHAIN_TIPS: apiUrl += "/chain/tips"; break; case ExploreEndpoint.CIRCULATING_SUPPLY: apiUrl += "/circulatingsupply"; break; case ExploreEndpoint.PEER_INFO: apiUrl += "/peer/info"; break; case ExploreEndpoint.BLOCK_BY_HASH: apiUrl += `/block/hash/${params.blockHash}`; break; case ExploreEndpoint.BLOCK_BY_HEIGHT: apiUrl += `/block/height/${params.blockHeight}`; break; case ExploreEndpoint.TAG_COUNT_BY_HEIGHT: apiUrl += `/block/tagcount/height/${params.blockHeight}/stats`; break; case ExploreEndpoint.BLOCK_HEADERS: apiUrl += "/block/headers"; break; case ExploreEndpoint.BLOCK_PAGES: { const pageNumber = params.pageNumber || 1; apiUrl += `/block/hash/${params.blockHash}/page/${pageNumber}`; break; } case ExploreEndpoint.BLOCK_STATS_BY_HEIGHT: apiUrl += `/block/height/${params.blockHeight}/stats`; break; case ExploreEndpoint.BLOCK_MINER_STATS: { const days = params.days !== undefined ? params.days : 7; apiUrl += `/miner/blocks/stats?days=${days}`; break; } case ExploreEndpoint.MINER_SUMMARY_STATS: { const days = params.days !== undefined ? params.days : 7; apiUrl += `/miner/summary/stats?days=${days}`; break; } case ExploreEndpoint.TX_BY_HASH: apiUrl += `/tx/hash/${params.txHash}`; break; case ExploreEndpoint.TX_RAW: apiUrl += `/tx/${params.txHash}/hex`; break; case ExploreEndpoint.TX_RECEIPT: apiUrl += `/tx/${params.txHash}/receipt`; break; case ExploreEndpoint.BULK_TX_DETAILS: apiUrl += "/txs"; break; case ExploreEndpoint.ADDRESS_HISTORY: apiUrl += `/address/${params.address}/history`; if (params.limit !== undefined) { apiUrl += `?limit=${params.limit}`; } break; case ExploreEndpoint.ADDRESS_UTXOS: apiUrl += `/address/${params.address}/unspent`; break; case ExploreEndpoint.HEALTH: apiUrl += "/woc"; break; default: throw new Error(`Unsupported endpoint: ${params.endpoint}`); } // Special handling for bulk_tx_details which requires a POST request if (params.endpoint === ExploreEndpoint.BULK_TX_DETAILS) { const response = await fetch(apiUrl, { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify({ txids: params.txids }), }); if (!response.ok) { let errorMsg = `API error: ${response.status} ${response.statusText}`; if (response.status === 404) { errorMsg += " - One or more transaction IDs not found"; } throw new Error(errorMsg); } const result = await response.json(); return { content: [ { type: "text", text: JSON.stringify(result, null, 2), }, ], }; } // For all other endpoints, use GET request const response = await fetch(apiUrl); if (!response.ok) { let errorMsg = `API error: ${response.status} ${response.statusText}`; // Provide helpful tips for specific error codes if (response.status === 404) { switch (params.endpoint) { case ExploreEndpoint.BLOCK_BY_HASH: errorMsg += ` - Block hash "${params.blockHash}" not found`; break; case ExploreEndpoint.BLOCK_BY_HEIGHT: errorMsg += ` - Block at height ${params.blockHeight} not found`; break; case ExploreEndpoint.BLOCK_PAGES: errorMsg += ` - Block hash "${params.blockHash}" not found or page ${params.pageNumber || 1} does not exist`; break; case ExploreEndpoint.TX_BY_HASH: case ExploreEndpoint.TX_RAW: case ExploreEndpoint.TX_RECEIPT: errorMsg += ` - Transaction hash "${params.txHash}" not found`; break; case ExploreEndpoint.ADDRESS_HISTORY: case ExploreEndpoint.ADDRESS_UTXOS: errorMsg += ` - No data found for address "${params.address}"`; break; } } throw new Error(errorMsg); } const result = await response.json(); return { content: [ { type: "text", text: JSON.stringify(result, null, 2), }, ], }; } catch (error) { return { content: [ { type: "text", text: `Error: ${error instanceof Error ? error.message : String(error)}`, }, ], isError: true, }; } }, ); }

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/b-open-io/bsv-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server