UNPKG

nano-mcp

Version:

NANO MCP (Nano Cryptocurrency) Server for AI Assistants - A JSON-RPC 2.0 API server for Nano cryptocurrency operations with QR code generation, local work generation and auto-receive pending blocks

1,150 lines (1,093 loc) 56.5 kB
const http = require('http'); const swaggerUi = require('swagger-ui-express'); const swaggerSpecs = require('./swagger'); const { NanoTransactions } = require('../utils/nano-transactions'); const { SchemaValidator } = require('../utils/schema-validator'); const { TestWalletManager } = require('../tests/test-wallet-manager'); const { BalanceConverter } = require('../utils/balance-converter'); const { NanoConverter } = require('../utils/nano-converter'); const { EnhancedErrorHandler } = require('../utils/error-handler'); const { schemaProvider } = require('../utils/schema-provider'); const path = require('path'); const fs = require('fs'); const express = require('express'); const bodyParser = require('body-parser'); const cors = require('cors'); const DEFAULT_PORT = 8080; /** * Request templates for each method to help users with correct format */ const REQUEST_TEMPLATES = { generateWallet: { jsonrpc: "2.0", method: "generateWallet", params: {}, id: 1 }, getBalance: { jsonrpc: "2.0", method: "getBalance", params: { address: "nano_3xxxxx..." }, id: 1 }, getAccountInfo: { jsonrpc: "2.0", method: "getAccountInfo", params: { address: "nano_3xxxxx..." }, id: 1 }, getPendingBlocks: { jsonrpc: "2.0", method: "getPendingBlocks", params: { address: "nano_3xxxxx..." }, id: 1 }, initializeAccount: { jsonrpc: "2.0", method: "initializeAccount", params: { address: "nano_3xxxxx...", privateKey: "your_private_key_here" }, id: 1 }, sendTransaction: { jsonrpc: "2.0", method: "sendTransaction", params: { fromAddress: "nano_3xxxxx...", toAddress: "nano_1xxxxx...", amountRaw: "1000000000000000000000000000", privateKey: "your_private_key_here" }, id: 1 }, receiveAllPending: { jsonrpc: "2.0", method: "receiveAllPending", params: { address: "nano_3xxxxx...", privateKey: "your_private_key_here" }, id: 1 }, generateQrCode: { jsonrpc: "2.0", method: "generateQrCode", params: { address: "nano_3xxxxx...", amount: "0.1" }, id: 1 }, setupTestWallets: { jsonrpc: "2.0", method: "setupTestWallets", params: {}, id: 1 }, getTestWallets: { jsonrpc: "2.0", method: "getTestWallets", params: { includePrivateKeys: true }, id: 1 }, updateTestWalletBalance: { jsonrpc: "2.0", method: "updateTestWalletBalance", params: { walletIdentifier: "wallet1", balance: "1000000000000000000000000000" }, id: 1 }, checkTestWalletsFunding: { jsonrpc: "2.0", method: "checkTestWalletsFunding", params: {}, id: 1 }, resetTestWallets: { jsonrpc: "2.0", method: "resetTestWallets", params: {}, id: 1 }, convertBalance: { jsonrpc: "2.0", method: "convertBalance", params: { amount: "100000000000000000000000000", from: "raw", to: "nano" }, id: 1 }, getAccountStatus: { jsonrpc: "2.0", method: "getAccountStatus", params: { address: "nano_3xxxxx..." }, id: 1 }, nanoConverterHelp: { jsonrpc: "2.0", method: "nanoConverterHelp", params: {}, id: 1 } }; /** * Tool definitions for the MCP server * Each tool includes its name, description, and input schema */ const MCP_TOOLS = [ { name: 'initialize', description: 'Initialize the MCP server and get available capabilities', inputSchema: { type: 'object', properties: {}, required: [] } }, { name: 'generateWallet', description: 'Generate a new NANO wallet with address and private key', inputSchema: { type: 'object', properties: {}, required: [] } }, { name: 'getBalance', description: 'Get the balance and pending amounts for a NANO address', inputSchema: { type: 'object', properties: { address: { type: 'string', description: 'NANO address to check balance for' } }, required: ['address'] } }, { name: 'getAccountInfo', description: 'Get detailed account information for a NANO address', inputSchema: { type: 'object', properties: { address: { type: 'string', description: 'NANO address to get information for' } }, required: ['address'] } }, { name: 'getPendingBlocks', description: 'Get pending blocks (incoming transactions) for a NANO address', inputSchema: { type: 'object', properties: { address: { type: 'string', description: 'NANO address to get pending blocks for' } }, required: ['address'] } }, { name: 'initializeAccount', description: 'Initialize a NANO account by publishing the first receive block', inputSchema: { type: 'object', properties: { address: { type: 'string', description: 'NANO address to initialize' }, privateKey: { type: 'string', description: 'Private key of the NANO address' } }, required: ['address', 'privateKey'] } }, { name: 'sendTransaction', description: 'Send NANO from one address to another', inputSchema: { type: 'object', properties: { fromAddress: { type: 'string', description: 'NANO address to send from' }, toAddress: { type: 'string', description: 'NANO address to send to' }, amountRaw: { type: 'string', description: 'Amount to send in raw units' }, privateKey: { type: 'string', description: 'Private key of the sending address' } }, required: ['fromAddress', 'toAddress', 'amountRaw', 'privateKey'] } }, { name: 'receiveAllPending', description: 'Receive all pending transactions for a NANO address', inputSchema: { type: 'object', properties: { address: { type: 'string', description: 'NANO address to receive pending transactions for' }, privateKey: { type: 'string', description: 'Private key of the NANO address' } }, required: ['address', 'privateKey'] } }, { name: 'generateQrCode', description: 'Generate a QR code for a NANO payment with address and amount', inputSchema: { type: 'object', properties: { address: { type: 'string', description: 'NANO address to receive payment' }, amount: { type: 'string', description: 'Amount in decimal XNO (e.g., "0.1" for 0.1 NANO)' } }, required: ['address', 'amount'] } }, { name: 'setupTestWallets', description: 'Generate two test wallets for integration testing. Saves wallets with private keys and prompts user to fund them with test NANO.', inputSchema: { type: 'object', properties: {}, required: [] } }, { name: 'getTestWallets', description: 'Retrieve existing test wallets with their addresses, balances, and funding status', inputSchema: { type: 'object', properties: { includePrivateKeys: { type: 'boolean', description: 'Whether to include private keys in the response (default: true)' } }, required: [] } }, { name: 'updateTestWalletBalance', description: 'Update the balance and funding status for a test wallet (used after checking on-chain balance)', inputSchema: { type: 'object', properties: { walletIdentifier: { type: 'string', description: 'Wallet identifier: "wallet1" or "wallet2"' }, balance: { type: 'string', description: 'New balance in raw units' } }, required: ['walletIdentifier', 'balance'] } }, { name: 'checkTestWalletsFunding', description: 'Check the funding status of both test wallets to determine if they are ready for testing', inputSchema: { type: 'object', properties: {}, required: [] } }, { name: 'resetTestWallets', description: 'Delete existing test wallets to start fresh with new wallet generation', inputSchema: { type: 'object', properties: {}, required: [] } }, { name: 'convertBalance', description: 'Convert between NANO and raw units. Helps autonomous agents with amount formatting.', inputSchema: { type: 'object', properties: { amount: { type: 'string', description: 'Amount to convert' }, from: { type: 'string', description: 'Source unit: "nano" or "raw"' }, to: { type: 'string', description: 'Target unit: "nano" or "raw"' } }, required: ['amount', 'from', 'to'] } }, { name: 'getAccountStatus', description: 'Get comprehensive account status with readiness checks, pending blocks, and actionable recommendations for autonomous agents', inputSchema: { type: 'object', properties: { address: { type: 'string', description: 'NANO address to check status for' } }, required: ['address'] } }, { name: 'nanoConverterHelp', description: 'Get comprehensive help and examples for Nano (XNO) conversion utilities. Includes conversion formulas, common mistakes, best practices, and ready-to-use examples. Essential for clients who are unfamiliar with Nano number formats and raw unit conversions.', inputSchema: { type: 'object', properties: {}, required: [] } } ]; /** * @swagger * /api-docs: * get: * summary: Get API documentation * description: Returns the Swagger UI documentation * responses: * 200: * description: API documentation */ /** * @swagger * /tools/list: * get: * summary: Get list of available tools * description: Returns a list of all available tools and their schemas * responses: * 200: * description: List of tools */ /** * NANO MCP (NANO Cryptocurrency) Server implementation * Provides a JSON-RPC 2.0 interface for interacting with the NANO network * Supports both HTTP and stdio transports */ class NanoMCPServer { /** * Creates a new NANO MCP Server instance * @param {Object} config - Server configuration * @param {number} [config.port=3000] - HTTP server port * @param {string} [config.apiUrl='https://rpc.nano.to'] - NANO RPC node URL * @param {string} [config.rpcKey] - API key for authenticated RPC nodes * @param {string} [config.defaultRepresentative] - Default representative for new accounts */ constructor(config = {}) { this.config = { port: process.env.MCP_PORT || 8080, apiUrl: process.env.NANO_RPC_URL || 'https://rpc.nano.to', rpcKey: process.env.NANO_RPC_KEY || 'RPC-KEY-BAB822FCCDAE42ECB7A331CCAAAA23', defaultRepresentative: process.env.NANO_REPRESENTATIVE || 'nano_3qya5xpjfsbk3ndfebo9dsrj6iy6f6idmogqtn1mtzdtwnxu6rw3dz18i6xf', ...config }; this.nanoTransactions = new NanoTransactions(this.config); this.schemaValidator = SchemaValidator.getInstance(); this.testWalletManager = new TestWalletManager(); } /** * Handles incoming JSON-RPC requests * @param {Object} request - JSON-RPC request object * @param {string} request.method - Method name to execute * @param {Object} request.params - Method parameters * @param {number} request.id - Request identifier * @returns {Promise<Object>} JSON-RPC response object * @throws {Error} When method is not found or parameters are invalid */ async handleRequest(request) { try { const { method, params, id } = request; let result; switch (method) { case 'tools/list': result = { tools: MCP_TOOLS }; break; case 'initialize': result = { version: "1.0.0", capabilities: { methods: MCP_TOOLS.map(tool => tool.name) } }; break; case 'generateWallet': result = await this.nanoTransactions.generateWallet(); break; case 'getBalance': // Validate address parameter if (!params || !params.address) { return EnhancedErrorHandler.missingParameter('address', 'getBalance', { address: "nano_3h3m6kfckrxpc4t33jn36eu8smfpukwuq1zq4hy35dh4a7drs6ormhwhkncn" }); } const balanceAddressError = EnhancedErrorHandler.validateAddress(params.address, 'address'); if (balanceAddressError) { return balanceAddressError; } const balanceInfo = await this.nanoTransactions.getAccountInfo(params.address); result = { balance: balanceInfo.balance || '0', pending: balanceInfo.pending || '0' }; break; case 'getAccountInfo': // Validate address parameter if (!params || !params.address) { return EnhancedErrorHandler.missingParameter('address', 'getAccountInfo', { address: "nano_3h3m6kfckrxpc4t33jn36eu8smfpukwuq1zq4hy35dh4a7drs6ormhwhkncn" }); } const accountInfoAddressError = EnhancedErrorHandler.validateAddress(params.address, 'address'); if (accountInfoAddressError) { return accountInfoAddressError; } result = await this.nanoTransactions.getAccountInfo(params.address); break; case 'getPendingBlocks': // Validate address parameter if (!params || !params.address) { return EnhancedErrorHandler.missingParameter('address', 'getPendingBlocks', { address: "nano_3h3m6kfckrxpc4t33jn36eu8smfpukwuq1zq4hy35dh4a7drs6ormhwhkncn" }); } const pendingAddressError = EnhancedErrorHandler.validateAddress(params.address, 'address'); if (pendingAddressError) { return pendingAddressError; } result = await this.nanoTransactions.getPendingBlocks(params.address); break; case 'initializeAccount': // Validate address parameter if (!params || !params.address) { return EnhancedErrorHandler.missingParameter('address', 'initializeAccount', { address: "nano_3h3m6kfckrxpc4t33jn36eu8smfpukwuq1zq4hy35dh4a7drs6ormhwhkncn", privateKey: "your_private_key_here" }); } const initAddressError = EnhancedErrorHandler.validateAddress(params.address, 'address'); if (initAddressError) { return initAddressError; } // Validate privateKey parameter if (!params.privateKey) { return EnhancedErrorHandler.missingParameter('privateKey', 'initializeAccount', { address: params.address, privateKey: "your_64_character_hexadecimal_private_key" }); } const initKeyError = EnhancedErrorHandler.validatePrivateKey(params.privateKey); if (initKeyError) { return initKeyError; } result = await this.nanoTransactions.initializeAccount(params.address, params.privateKey); break; case 'sendTransaction': // Validate fromAddress parameter if (!params || !params.fromAddress) { return EnhancedErrorHandler.missingParameter('fromAddress', 'sendTransaction', { fromAddress: "nano_3sender_address_here", toAddress: "nano_3receiver_address_here", amountRaw: "100000000000000000000000000000", privateKey: "your_private_key_here" }); } const fromAddressError = EnhancedErrorHandler.validateAddress(params.fromAddress, 'fromAddress'); if (fromAddressError) { return fromAddressError; } // Validate toAddress parameter if (!params.toAddress) { return EnhancedErrorHandler.missingParameter('toAddress', 'sendTransaction', { fromAddress: params.fromAddress, toAddress: "nano_3receiver_address_here", amountRaw: "100000000000000000000000000000", privateKey: "your_private_key_here" }); } const toAddressError = EnhancedErrorHandler.validateAddress(params.toAddress, 'toAddress'); if (toAddressError) { return toAddressError; } // Validate amountRaw parameter if (!params.amountRaw) { return EnhancedErrorHandler.missingParameter('amountRaw', 'sendTransaction', { fromAddress: params.fromAddress, toAddress: params.toAddress, amountRaw: "100000000000000000000000000000", privateKey: "your_private_key_here" }); } const amountError = EnhancedErrorHandler.validateAmountRaw(params.amountRaw, 'amountRaw'); if (amountError) { return amountError; } // Validate privateKey parameter if (!params.privateKey) { return EnhancedErrorHandler.missingParameter('privateKey', 'sendTransaction', { fromAddress: params.fromAddress, toAddress: params.toAddress, amountRaw: params.amountRaw, privateKey: "your_64_character_hexadecimal_private_key" }); } const sendKeyError = EnhancedErrorHandler.validatePrivateKey(params.privateKey); if (sendKeyError) { return sendKeyError; } result = await this.nanoTransactions.sendTransaction( params.fromAddress, params.privateKey, params.toAddress, params.amountRaw ); break; case 'receiveAllPending': // Validate address parameter if (!params || !params.address) { return EnhancedErrorHandler.missingParameter('address', 'receiveAllPending', { address: "nano_3h3m6kfckrxpc4t33jn36eu8smfpukwuq1zq4hy35dh4a7drs6ormhwhkncn", privateKey: "your_private_key_here" }); } const receiveAddressError = EnhancedErrorHandler.validateAddress(params.address, 'address'); if (receiveAddressError) { return receiveAddressError; } // Validate privateKey parameter if (!params.privateKey) { return EnhancedErrorHandler.missingParameter('privateKey', 'receiveAllPending', { address: params.address, privateKey: "your_64_character_hexadecimal_private_key" }); } const receiveKeyError = EnhancedErrorHandler.validatePrivateKey(params.privateKey); if (receiveKeyError) { return receiveKeyError; } result = await this.nanoTransactions.receiveAllPending(params.address, params.privateKey); break; case 'generateQrCode': // Validate address parameter if (!params || !params.address) { return EnhancedErrorHandler.missingParameter('address', 'generateQrCode', { address: "nano_3h3m6kfckrxpc4t33jn36eu8smfpukwuq1zq4hy35dh4a7drs6ormhwhkncn", amount: "0.1" }); } const qrAddressError = EnhancedErrorHandler.validateAddress(params.address, 'address'); if (qrAddressError) { return qrAddressError; } // Validate amount parameter (in NANO format) if (!params.amount) { return EnhancedErrorHandler.missingParameter('amount', 'generateQrCode', { address: params.address, amount: "0.1" }); } if (typeof params.amount !== 'string') { return { success: false, error: "Invalid amount parameter", errorCode: "INVALID_AMOUNT_TYPE", details: { parameter: "amount", providedValue: params.amount, providedType: typeof params.amount, expectedType: "string", expectedFormat: "Decimal string in NANO (e.g., '0.1', '1.5')" }, nextSteps: [ "Step 1: Provide amount as a string", "Step 2: Amount should be in NANO format (decimal allowed)", "Step 3: Examples: '0.1', '1.0', '5.5'", "Step 4: Do NOT use raw format for QR codes" ], exampleRequest: { jsonrpc: "2.0", method: "generateQrCode", params: { address: params.address, amount: "0.1" }, id: 1 } }; } // Validate amount is a valid decimal if (!/^\d+\.?\d*$/.test(params.amount.trim())) { return { success: false, error: "Invalid amount format for QR code", errorCode: "INVALID_QR_AMOUNT_FORMAT", details: { parameter: "amount", providedValue: params.amount, expectedFormat: "Decimal string in NANO (e.g., '0.1', '1.5')", issue: "Amount must be a valid decimal number" }, nextSteps: [ "Step 1: Use decimal NANO format (not raw)", "Step 2: Examples: '0.1', '1.0', '5.5', '100'", "Step 3: No special characters except decimal point", "Step 4: Amount must be positive" ], exampleRequest: { jsonrpc: "2.0", method: "generateQrCode", params: { address: params.address, amount: "0.1" }, id: 1 } }; } result = await this.nanoTransactions.generateQrCode(params.address, params.amount); break; case 'setupTestWallets': result = await this.testWalletManager.generateTestWallets(); break; case 'getTestWallets': const includePrivateKeys = params.includePrivateKeys !== undefined ? params.includePrivateKeys : true; const wallets = await this.testWalletManager.getTestWallets(includePrivateKeys); if (!wallets) { result = { exists: false, message: 'No test wallets found. Use setupTestWallets to generate new wallets.' }; } else { result = { exists: true, ...wallets }; } break; case 'updateTestWalletBalance': this.schemaValidator.validate(params, { type: 'object', required: ['walletIdentifier', 'balance'], properties: { walletIdentifier: { type: 'string' }, balance: { type: 'string' } } }); result = await this.testWalletManager.updateWalletBalance(params.walletIdentifier, params.balance); break; case 'checkTestWalletsFunding': result = await this.testWalletManager.checkFundingStatus(); break; case 'resetTestWallets': result = await this.testWalletManager.resetTestWallets(); break; case 'convertBalance': // Validate amount parameter if (!params || !params.amount) { return EnhancedErrorHandler.missingParameter('amount', 'convertBalance', { amount: "0.1", from: "nano", to: "raw" }); } if (typeof params.amount !== 'string') { return { success: false, error: "Invalid amount parameter", errorCode: "INVALID_AMOUNT_TYPE", details: { parameter: "amount", providedType: typeof params.amount, expectedType: "string" }, nextSteps: [ "Step 1: Provide amount as a string", "Step 2: Example: '0.1' or '100000000000000000000000000000'", "Step 3: Do not use number type, use string" ] }; } // Validate from parameter if (!params.from) { return EnhancedErrorHandler.missingParameter('from', 'convertBalance', { amount: params.amount, from: "nano", to: "raw" }); } // Validate to parameter if (!params.to) { return EnhancedErrorHandler.missingParameter('to', 'convertBalance', { amount: params.amount, from: params.from, to: "raw" }); } const from = params.from.toLowerCase(); const to = params.to.toLowerCase(); // Validate from and to values if (!['nano', 'raw'].includes(from) || !['nano', 'raw'].includes(to)) { return { success: false, error: "Invalid conversion units", errorCode: "INVALID_CONVERSION_UNITS", details: { providedFrom: params.from, providedTo: params.to, allowedValues: ['nano', 'raw'] }, nextSteps: [ "Step 1: 'from' parameter must be either 'nano' or 'raw'", "Step 2: 'to' parameter must be either 'nano' or 'raw'", "Step 3: Supported conversions: nano→raw or raw→nano", "Step 4: Parameter values are case-insensitive" ], exampleRequests: [ { description: "Convert NANO to raw", request: { jsonrpc: "2.0", method: "convertBalance", params: { amount: "0.1", from: "nano", to: "raw" }, id: 1 } }, { description: "Convert raw to NANO", request: { jsonrpc: "2.0", method: "convertBalance", params: { amount: "100000000000000000000000000000", from: "raw", to: "nano" }, id: 1 } } ] }; } if (from === to) { return { success: false, error: "Conversion units are the same", errorCode: "SAME_CONVERSION_UNITS", details: { from: from, to: to, issue: "Cannot convert to the same unit" }, nextSteps: [ "Step 1: 'from' and 'to' must be different units", "Step 2: Use 'nano' and 'raw' for conversion", "Step 3: Example: from='nano' to='raw' or from='raw' to='nano'" ] }; } try { if (from === 'nano' && to === 'raw') { const raw = BalanceConverter.nanoToRaw(params.amount); result = { original: params.amount, originalUnit: 'NANO', converted: raw, convertedUnit: 'raw', formula: 'raw = NANO × 10^30' }; } else if (from === 'raw' && to === 'nano') { const nano = BalanceConverter.rawToNano(params.amount); result = { original: params.amount, originalUnit: 'raw', converted: nano, convertedUnit: 'NANO', formula: 'NANO = raw ÷ 10^30' }; } } catch (error) { return { success: false, error: "Conversion failed", errorCode: "CONVERSION_ERROR", details: { originalError: error.message, amount: params.amount, from: from, to: to }, nextSteps: [ "Step 1: Verify the amount format is correct", "Step 2: For NANO: use decimal format (e.g., '0.1', '1.5')", "Step 3: For raw: use integer string (e.g., '100000000000000000000000000000')", "Step 4: Ensure amount is positive and within valid range" ] }; } break; case 'getAccountStatus': // Validate address parameter if (!params || !params.address) { return EnhancedErrorHandler.missingParameter('address', 'getAccountStatus', { address: "nano_3h3m6kfckrxpc4t33jn36eu8smfpukwuq1zq4hy35dh4a7drs6ormhwhkncn" }); } const statusAddressError = EnhancedErrorHandler.validateAddress(params.address, 'address'); if (statusAddressError) { return statusAddressError; } // Get comprehensive account status let accountInfo; try { accountInfo = await this.nanoTransactions.getAccountInfo(params.address); } catch (error) { accountInfo = { error: 'Account not found' }; } const pendingBlocks = await this.nanoTransactions.getPendingBlocks(params.address); const hasPending = pendingBlocks.blocks && Object.keys(pendingBlocks.blocks).length > 0; let pendingCount = 0; let totalPending = BigInt(0); if (hasPending) { pendingCount = Object.keys(pendingBlocks.blocks).length; for (const block of Object.values(pendingBlocks.blocks)) { totalPending += BigInt(block.amount); } } const initialized = !accountInfo.error; const balance = initialized ? accountInfo.balance : '0'; const canSend = initialized && BigInt(balance) > 0; const canReceive = true; // Can always receive const needsAction = []; if (!initialized && hasPending) { needsAction.push({ action: 'initializeAccount', reason: 'Account not initialized but has pending blocks', priority: 'high' }); } if (initialized && hasPending) { needsAction.push({ action: 'receiveAllPending', reason: `${pendingCount} pending block(s) waiting to be received`, priority: 'medium' }); } if (!initialized && !hasPending) { needsAction.push({ action: 'fundAccount', reason: 'Account has no balance and no pending blocks', priority: 'high' }); } result = { address: params.address, initialized: initialized, balance: { raw: balance, nano: BalanceConverter.rawToNano(balance) }, pending: { count: pendingCount, totalAmount: totalPending.toString(), totalAmountNano: BalanceConverter.rawToNano(totalPending.toString()) }, capabilities: { canSend: canSend, canReceive: canReceive }, needsAction: needsAction, readyForTesting: initialized && canSend, recommendations: needsAction.length === 0 ? ['Account is ready for transactions'] : needsAction.map(a => `${a.priority.toUpperCase()}: ${a.action} - ${a.reason}`) }; break; case 'nanoConverterHelp': // Return comprehensive Nano conversion help for clients unfamiliar with Nano formats result = { ...NanoConverter.getConversionHelp(), utilityFunctions: { xnoToRaw: { description: "Convert XNO amount to raw units (use this for all transaction amounts)", example: "xnoToRaw(1) => '1000000000000000000000000000000'", usage: "Always use this before sending transactions" }, rawToXNO: { description: "Convert raw units to XNO amount (use for display purposes)", example: "rawToXNO('1000000000000000000000000000000') => '1'", usage: "Use for human-readable display of balances" }, isValidNanoAddress: { description: "Validate Nano address format before transactions", example: "isValidNanoAddress('nano_3xxx...') => true", usage: "Always validate addresses before sending" }, formatXNO: { description: "Format XNO amount for display with specific decimal places", example: "formatXNO('0.123456789', 6) => '0.123457'", usage: "Use for consistent display formatting (display only, not for calculations)" } }, exampleWorkflow: [ "Step 1: Get user input in XNO (e.g., '0.1')", "Step 2: Convert to raw using NanoConverter.xnoToRaw('0.1')", "Step 3: Validate address using NanoConverter.isValidNanoAddress(address)", "Step 4: Use raw amount in sendTransaction", "Step 5: Display confirmation using NanoConverter.formatXNO()" ], integrationWithMCP: { convertBalance: "Use 'convertBalance' MCP method for conversions in production", getAccountStatus: "Use 'getAccountStatus' to see balances in both raw and XNO", sendTransaction: "Always use raw amounts for 'amountRaw' parameter" }, warning: "IMPORTANT: Most clients don't know Nano uses 30 decimal places. Always educate users that 1 XNO = 10^30 raw units. Never use floating-point arithmetic for currency calculations." }; break; default: return EnhancedErrorHandler.methodNotFound(method, MCP_TOOLS.map(t => t.name)); } return { jsonrpc: "2.0", result, id }; } catch (error) { // Determine error type and provide helpful template let errorCode = -32603; // Internal error let errorMessage = error.message || 'Internal server error'; let helpfulInfo = {}; // Check for schema validation errors (from SchemaValidator) if (error.code === -32602 || error.message === 'Invalid parameters') { errorCode = -32602; // Invalid params const method = request.method; // Extract validation details if (error.details && error.details.errors) { errorMessage = 'Invalid parameters: '; const missingFields = error.details.errors .filter(e => e.message && e.message.includes('required')) .map(e => e.params?.missingProperty || 'unknown'); if (missingFields.length > 0) { errorMessage += `Missing required field(s): ${missingFields.join(', ')}`; } else { errorMessage += error.details.errors.map(e => e.message).join(', '); } } if (REQUEST_TEMPLATES[method]) { helpfulInfo = { correctFormat: REQUEST_TEMPLATES[method], hint: `Please use the correct format shown in 'correctFormat'. Ensure all required parameters are included.`, yourRequest: { method: method, params: request.params || {} } }; } } // Check for generic validation errors else if (error.message && (error.message.includes('required') || error.message.includes('Missing') || error.message.includes('Invalid'))) { errorCode = -32602; // Invalid params const method = request.method; if (REQUEST_TEMPLATES[method]) { helpfulInfo = { correctFormat: REQUEST_TEMPLATES[method], hint: `Please use the correct format shown in 'correctFormat'. Ensure all required parameters are included.` }; } } // Method not found else if (error.message && error.message.includes('not found')) { errorCode = -32601; helpfulInfo = { availableMethods: Object.keys(REQUEST_TEMPLATES), exampleRequest: REQUEST_TEMPLATES['generateWallet'], hint: "Please use one of the available methods listed above. See 'exampleRequest' for proper JSON-RPC format." }; } // Missing jsonrpc or invalid request structure else if (!request.method) { errorCode = -32600; // Invalid Request errorMessage = "Invalid JSON-RPC request. Missing 'method' field."; helpfulInfo = { correctFormat: { jsonrpc: "2.0", method: "methodName", params: { /* method parameters */ }, id: 1 }, availableMethods: Object.keys(REQUEST_TEMPLATES), hint: "All requests must include: jsonrpc, method, params (if required), and id" }; } return { jsonrpc: "2.0", error: { code: errorCode, message: errorMessage, data: Object.keys(helpfulInfo).length > 0 ? helpfulInfo : undefined }, id: request.id || null }; } } /** * Starts the HTTP server * @returns {http.Server} The HTTP server instance * @throws {Error} When server fails to start */ startHttp() { const app = express(); app.use(co