Skip to main content
Glama
generate.ts13.3 kB
import os from "node:os"; import path from "node:path"; import { P2PKH, PrivateKey, Script, Transaction, fromUtxo, isBroadcastFailure, } from "@bsv/sdk"; import { Utils as BSVUtils, HD } from "@bsv/sdk"; import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js"; import { BAP } from "bsv-bap"; import { z } from "zod"; import { V5Broadcaster } from "../../utils/broadcaster"; import { SecureKeyManager } from "../../utils/keyManager"; import { BAP_PREFIX } from "../constants"; import { fetchPaymentUtxos } from "../wallet/fetchPaymentUtxos"; // Get toArray from BSV SDK Utils const { toArray } = BSVUtils; const KEY_DIR = path.join(os.homedir(), ".bsv-mcp"); const KEY_FILE_PATH = path.join(KEY_DIR, "keys.json"); const bapGenerateArgsSchema = z .object({ alternateName: z.string().optional(), description: z.string().optional(), }) .optional(); export type BapGenerateArgs = z.infer<typeof bapGenerateArgsSchema>; /** * Generates a new BAP HD master key and derives the first identity, * saving the keys to secure storage. * Attempts to register the ID on-chain, unless broadcasting is disabled. */ async function generateBapKeys( args?: BapGenerateArgs & { disableBroadcasting?: boolean }, ): Promise<{ success: boolean; xprv?: string; identityAddress?: string; identityKey?: string; error?: string; message?: string; txid?: string; rawTx?: string; }> { const keyManager = new SecureKeyManager({ keyDir: KEY_DIR }); let tempXprv = ""; let identityPk: string; let payPkToPreserve: PrivateKey | undefined; // 1. Load existing keys and check preconditions try { let keys: { payPk?: PrivateKey; identityPk?: PrivateKey; xprv?: string }; let source: string; try { const result = await keyManager.loadKeys(); keys = result.keys; source = result.source; } catch (loadError) { const errorMsg = loadError instanceof Error ? loadError.message : String(loadError); console.error( `ERROR: Failed to read keys from secure storage: ${errorMsg}`, ); // Check for specific error patterns if ( errorMsg.includes("JSON Parse error") || errorMsg.includes("JSON.parse") ) { // Extract just the JSON parse error part const match = errorMsg.match(/JSON Parse error[^.]*\.?/); const jsonError = match ? match[0] : "JSON Parse error"; return { success: false, error: `Could not read or parse keys.json: ${jsonError}`, }; } return { success: false, error: `Failed to read keys: ${errorMsg}`, }; } if (!keys.payPk) { console.error( "ERROR: Payment Private Key (payPk) does not exist in secure storage.", ); const errorMessage = source === "none" ? "keys.json not found. Payment Private Key (payPk) is required." : "Payment Private Key (payPk) does not exist in keys.json."; return { success: false, error: errorMessage, }; } payPkToPreserve = keys.payPk; if (keys.xprv) { console.error( "ERROR: BAP Master Key (xprv) already exists in secure storage.", ); return { success: false, error: "BAP Master Key (xprv) already exists in keys.json.", }; } if (keys.identityPk) { console.error( "ERROR: BAP Identity Key (identityPk) already exists. Cannot generate HD key.", ); return { success: false, error: "BAP Identity Key (identityPk) already exists in keys.json.", }; } const status = keyManager.getStatus(); if (source === "legacy" && !status.hasEncrypted) { console.warn( "WARN: Using unencrypted keys. Run the server again to encrypt them.", ); } } catch (fileError) { console.error("ERROR: Failed to read keys from secure storage:", fileError); return { success: false, error: `Failed to read keys: ${fileError instanceof Error ? fileError.message : String(fileError)}`, }; } try { const payPk = payPkToPreserve; const paymentAddress = payPk.toAddress(); // 2. Generate HD Key const hdKey = HD.fromRandom(); tempXprv = hdKey.toString(); // 3. Create BAP instance and generate identity const bapInstance = new BAP(tempXprv); // Create identity attributes const identityAttributes = { name: { value: args?.alternateName || "Anonymous User", nonce: BSVUtils.toHex(BSVUtils.toArray(Math.random().toString())), }, description: { value: args?.description || "A BAP identity managed by BSV-MCP.", nonce: BSVUtils.toHex(BSVUtils.toArray(Math.random().toString())), }, }; // Create new ID with the BAP instance const bapId = bapInstance.newId(undefined, identityAttributes); // Get the generated identity information const generatedIdentityKey = bapId.getIdentityKey(); const generatedIdentityAddress = bapId.rootAddress; // Export member backup to get the private key const memberBackup = bapId.exportMemberBackup(); identityPk = memberBackup.derivedPrivateKey; console.error(`INFO: Generated BAP identity key: ${generatedIdentityKey}`); console.error( `INFO: Generated BAP identity address: ${generatedIdentityAddress}`, ); // 5. Save Keys to secure storage const updatedKeys = { payPk: payPkToPreserve, identityPk: PrivateKey.fromWif(identityPk), xprv: tempXprv, }; await keyManager.saveKeys(updatedKeys); const status = keyManager.getStatus(); if (status.hasEncrypted) { console.error( `INFO: BAP HD Master Key and initial Identity Key have been generated and saved (encrypted) to ${KEY_DIR}/keys.bep`, ); } else { console.error( `INFO: BAP HD Master Key and initial Identity Key have been generated and saved to ${KEY_FILE_PATH}`, ); } // Create the ID registration transaction output // BAP ID format: OP_0 OP_RETURN <BAP_PREFIX> "ID" <identity_key> <root_address> <current_address> const idPayload = [ toArray(BAP_PREFIX, "utf8"), toArray("ID"), toArray(generatedIdentityKey), toArray(bapId.rootAddress), toArray(bapId.rootAddress), // For initial registration, current = root ]; // Sign the ID payload with AIP const signedIdPayload = bapId.signOpReturnWithAIP(idPayload); // Convert to hex strings for Script const idHexStrings = signedIdPayload.map((bytes) => BSVUtils.toHex(bytes as number[]), ); const idAsmString = `OP_0 OP_RETURN ${idHexStrings.join(" ")}`; const idScript = Script.fromASM(idAsmString); // Create the ALIAS transaction output // First create the structured data object for the alias const aliasData = { "@context": "https://schema.org", "@type": "Person", name: args?.alternateName || "Anonymous User", description: args?.description || "A BAP identity managed by BSV-MCP.", url: `bitcoin:${paymentAddress}`, }; const aliasPayload = [ toArray(BAP_PREFIX, "utf8"), toArray("ALIAS"), toArray(generatedIdentityKey), toArray(JSON.stringify(aliasData)), ]; // Sign the ALIAS payload with AIP const signedAliasPayload = bapId.signOpReturnWithAIP(aliasPayload); // Convert to hex strings for Script const aliasHexStrings = signedAliasPayload.map((bytes) => BSVUtils.toHex(bytes as number[]), ); const aliasAsmString = `OP_0 OP_RETURN ${aliasHexStrings.join(" ")}`; const aliasScript = Script.fromASM(aliasAsmString); // 6. Build Transaction const tx = new Transaction(); // Add the ID registration output tx.addOutput({ lockingScript: idScript, satoshis: 0, }); // Add the ALIAS output tx.addOutput({ lockingScript: aliasScript, satoshis: 0, }); console.error( `INFO: Fetching UTXOs for payment address: ${paymentAddress}`, ); const paymentUtxos = await fetchPaymentUtxos(paymentAddress); if (!paymentUtxos || paymentUtxos.length === 0) { console.error( `ERROR: No UTXOs found for payment address ${paymentAddress}. Cannot fund BAP registration.`, ); // Keys were generated and saved, but no UTXOs to broadcast return { success: true, identityAddress: generatedIdentityAddress, identityKey: generatedIdentityKey, message: "BAP HD Key and Initial Identity Generated & Saved to secure storage. No UTXOs available to fund registration.", error: "No UTXOs found for payment address. Cannot fund BAP registration.", }; } console.error(`INFO: Found ${paymentUtxos.length} payment UTXOs.`); // Estimate fee let estimatedSize = tx.toHex().length / 2; estimatedSize += paymentUtxos.length * 148; estimatedSize += 34; const estimatedFee = Math.ceil(estimatedSize * 0.05); for (const utxo of paymentUtxos) { const input = fromUtxo(utxo, new P2PKH().unlock(payPk)); tx.addInput(input); } const totalInputValue = paymentUtxos.reduce( (sum: number, utxo: { satoshis: number }) => sum + utxo.satoshis, 0, ); const changeAmount = totalInputValue - estimatedFee; if (changeAmount > 0) { tx.addOutput({ lockingScript: new P2PKH().lock(paymentAddress), satoshis: changeAmount, }); } await tx.sign(); const rawTxHex = tx.toHex(); const txid = tx.id("hex"); console.error(`INFO: BAP registration transaction created. TXID: ${txid}`); console.error(`DEBUG: Raw transaction hex: ${rawTxHex}`); // 7. Broadcast Transaction const disableBroadcasting = args?.disableBroadcasting || process.env.DISABLE_BROADCASTING === "true"; if (disableBroadcasting) { console.error( "INFO: Broadcasting disabled. Transaction not sent to network.", ); const message = `BAP HD Key and Initial Identity Generated & Saved to secure storage. Registration transaction created but not broadcast (TXID: ${txid}).`; return { success: true, identityAddress: bapId.rootAddress, identityKey: generatedIdentityKey, txid, rawTx: rawTxHex, message, }; } try { const broadcaster = new V5Broadcaster(); const broadcastResult = await tx.broadcast(broadcaster); let effectiveTxid = txid; if (!isBroadcastFailure(broadcastResult)) { effectiveTxid = broadcastResult.txid; console.error( `INFO: BAP registration transaction broadcast successfully. TXID: ${effectiveTxid}`, ); } else if (isBroadcastFailure(broadcastResult)) { const failureTxid = broadcastResult.txid ? ` (${broadcastResult.txid})` : ""; console.warn( `WARN: Transaction broadcast failed${failureTxid}. Code: ${broadcastResult.code}, Description: ${broadcastResult.description}. Original TXID ${txid}.`, ); if (broadcastResult.txid) { effectiveTxid = broadcastResult.txid; } } const message = `BAP HD Key and Initial Identity Generated & Saved to secure storage. Registration TXID: ${effectiveTxid}.`; let overallSuccess = true; let errorMessageFromBroadcast: string | undefined; if (isBroadcastFailure(broadcastResult)) { overallSuccess = false; errorMessageFromBroadcast = `Broadcast failed: ${broadcastResult.description} (Code: ${broadcastResult.code})`; } return { success: overallSuccess, identityAddress: generatedIdentityAddress, identityKey: generatedIdentityKey, txid: effectiveTxid, rawTx: rawTxHex, message: isBroadcastFailure(broadcastResult) ? `${message} Note: ${errorMessageFromBroadcast}` : message, error: isBroadcastFailure(broadcastResult) ? errorMessageFromBroadcast : undefined, }; } catch (broadcastError) { const errorMsg = broadcastError instanceof Error ? broadcastError.message : String(broadcastError); console.error( `ERROR: Failed to broadcast BAP registration transaction ${txid}: ${errorMsg}`, ); // Keys were generated and saved, but broadcast failed return { success: true, identityAddress: generatedIdentityAddress, identityKey: generatedIdentityKey, txid, rawTx: rawTxHex, message: `Keys generated and saved, but failed to broadcast BAP registration transaction: ${errorMsg}`, error: `Broadcast failed: ${errorMsg}. You can manually broadcast the raw transaction.`, }; } } catch (error) { const errorMsg = error instanceof Error ? error.message : String(error); console.error( `ERROR: Failed to generate BAP HD key or register identity: ${errorMsg}`, ); return { success: false, error: `Failed during BAP key generation/saving or transaction construction: ${errorMsg}`, }; } } /** * Registers the bap_generate tool */ export function registerBapGenerateTool(server: McpServer) { server.tool( "bap_generate", "Generates a BAP HD master key AND derives the first identity key if no BAP keys (xprv or identityPk) exist. Saves keys to secure storage. Attempts on-chain registration using payPk (honors DISABLE_BROADCASTING). Optionally takes alternateName and description for the profile.", { args: bapGenerateArgsSchema }, async ({ args }: { args?: BapGenerateArgs }): Promise<CallToolResult> => { try { const result = await generateBapKeys(args); // Format result as JSON return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }], isError: !result.success, }; } catch (e) { const msg = e instanceof Error ? e.message : String(e); return { content: [{ type: "text", text: msg }], isError: true }; } }, ); }

Latest Blog Posts

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