UNPKG

stbl-mcp

Version:

Stability MCP Server - Blockchain tools for AI agents with zero gas fees

967 lines (880 loc) 29.5 kB
#!/usr/bin/env node import { Server } from '@modelcontextprotocol/sdk/server/index.js'; import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js'; import { promises as fs } from 'fs'; import { join } from 'path'; import { homedir } from 'os'; // Get API key from environment variable or use default const API_KEY = process.env.STABILITY_API_KEY || 'try-it-out'; // Lightweight storage functions for tracking class TrackingStorage { constructor() { this.baseDir = join(homedir(), '.stbl-mcp'); this.configPath = join(this.baseDir, 'config.json'); this.contractsRegistryPath = join(this.baseDir, 'contracts', 'deployed.json'); this.transactionHistoryPath = join(this.baseDir, 'transactions', 'history.json'); this.addressBookPath = join(this.baseDir, 'addresses', 'known.json'); } async ensureInitialized() { try { await fs.mkdir(join(this.baseDir, 'contracts'), { recursive: true }); await fs.mkdir(join(this.baseDir, 'contracts', 'artifacts'), { recursive: true }); await fs.mkdir(join(this.baseDir, 'transactions'), { recursive: true }); await fs.mkdir(join(this.baseDir, 'addresses'), { recursive: true }); // Initialize files if they don't exist const defaultConfig = { apiKey: 'try-it-out', setupComplete: false, created: Date.now(), lastUpdated: Date.now() }; await this.ensureFileExists(this.configPath, defaultConfig); await this.ensureFileExists(this.contractsRegistryPath, []); await this.ensureFileExists(this.transactionHistoryPath, []); await this.ensureFileExists(this.addressBookPath, []); } catch (error) { console.error('Storage initialization failed:', error); } } async ensureFileExists(filePath, defaultContent) { try { await fs.access(filePath); } catch { await fs.writeFile(filePath, JSON.stringify(defaultContent, null, 2)); } } async addTransaction(transaction) { try { const history = await this.getTransactionHistory(); const filtered = history.filter(tx => tx.hash !== transaction.hash); filtered.unshift(transaction); const trimmed = filtered.slice(0, 1000); await fs.writeFile(this.transactionHistoryPath, JSON.stringify(trimmed, null, 2)); } catch (error) { console.error('Failed to track transaction:', error); } } async addDeployedContract(contract) { try { const contracts = await this.getDeployedContracts(); const filtered = contracts.filter(c => c.address !== contract.address); filtered.push(contract); await fs.writeFile(this.contractsRegistryPath, JSON.stringify(filtered, null, 2)); // Save individual artifact const artifactPath = join(this.baseDir, 'contracts', 'artifacts', `${contract.address}.json`); await fs.writeFile(artifactPath, JSON.stringify(contract, null, 2)); } catch (error) { console.error('Failed to track contract:', error); } } async getTransactionHistory() { try { const content = await fs.readFile(this.transactionHistoryPath, 'utf8'); return JSON.parse(content); } catch { return []; } } async getDeployedContracts() { try { const content = await fs.readFile(this.contractsRegistryPath, 'utf8'); return JSON.parse(content); } catch { return []; } } async getConfig() { try { const content = await fs.readFile(this.configPath, 'utf8'); return JSON.parse(content); } catch { return { apiKey: 'try-it-out', setupComplete: false, created: Date.now(), lastUpdated: Date.now() }; } } async updateConfig(updates) { try { const config = await this.getConfig(); const updatedConfig = { ...config, ...updates, lastUpdated: Date.now() }; await fs.writeFile(this.configPath, JSON.stringify(updatedConfig, null, 2)); return updatedConfig; } catch (error) { console.error('Failed to update config:', error); return null; } } } const tracking = new TrackingStorage(); // Real API functions using Stability blockchain async function postZktV1(message, apiKey = API_KEY) { const url = `https://rpc.stabilityprotocol.com/zkt/${apiKey}`; const payload = { arguments: message }; // Warn about try-it-out key limitations if (apiKey === "try-it-out") { console.error("⚠️ Warning: Using 'try-it-out' API key (limited functionality)"); console.error(" For production use, get a FREE API key at: https://portal.stabilityprotocol.com/"); console.error(" Free tier: 1000 writes/month, 200 reads/minute, up to 3 keys"); } try { const response = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) }); const responseText = await response.text(); // Try to parse as JSON, fallback to raw text try { return JSON.parse(responseText); } catch { return { success: true, raw_response: responseText }; } } catch (error) { return { success: false, error: error.message, message: "Failed to connect to Stability blockchain" }; } } async function callContractRead(args, apiKey = API_KEY) { const url = `https://rpc.stabilityprotocol.com/zkt/${apiKey}`; const payload = { to: args.to, abi: args.abi || [], method: args.method, arguments: args.arguments || [], id: args.id || 1 }; try { const response = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) }); const responseText = await response.text(); // Try to parse as JSON, fallback to raw text try { return JSON.parse(responseText); } catch { return { success: true, raw_response: responseText }; } } catch (error) { return { success: false, error: error.message, message: "Failed to connect to Stability blockchain" }; } } async function callContractWrite(args, apiKey = API_KEY) { const url = `https://rpc.stabilityprotocol.com/zkt/${apiKey}`; const payload = { to: args.to, abi: args.abi || [], method: args.method, arguments: args.arguments || [], wait: args.wait !== false, id: args.id || 1 }; try { const response = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) }); const responseText = await response.text(); try { return JSON.parse(responseText); } catch { return { success: true, raw_response: responseText }; } } catch (error) { return { success: false, error: error.message, message: "Failed to execute contract transaction" }; } } async function deployContract(args, apiKey = API_KEY) { const url = `https://rpc.stabilityprotocol.com/zkt/${apiKey}`; const payload = { code: args.code, arguments: args.arguments || [], wait: args.wait !== false, id: args.id || 1 }; try { const response = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) }); const responseText = await response.text(); try { return JSON.parse(responseText); } catch { return { success: true, raw_response: responseText }; } } catch (error) { return { success: false, error: error.message, message: "Failed to deploy contract" }; } } // Tool definitions const TOOLS = [ { name: 'stbl_write', description: 'Post ZKTv1 message to Stability blockchain with zero gas fees', inputSchema: { type: 'object', properties: { arguments: { type: 'string', description: 'Message to post to blockchain' } }, required: ['arguments'] } }, { name: 'stbl_read', description: 'Execute read-only contract calls on Stability blockchain', inputSchema: { type: 'object', properties: { to: { type: 'string', description: 'Contract address to call' }, method: { type: 'string', description: 'Function name to call' }, arguments: { type: 'array', description: 'Function arguments', items: { type: 'string' } }, abi: { type: 'array', description: 'Contract ABI (Application Binary Interface)', items: { type: 'string' } }, id: { type: 'number', description: 'Request ID for tracking', default: 1 } }, required: ['to', 'method'] } }, { name: 'stbl_write_contract', description: 'Execute state-changing smart contract calls on Stability blockchain', inputSchema: { type: 'object', properties: { to: { type: 'string', description: 'Contract address to call' }, method: { type: 'string', description: 'Function name to call' }, arguments: { type: 'array', description: 'Function arguments', items: { type: 'string' } }, abi: { type: 'array', description: 'Contract ABI (Application Binary Interface)', items: { type: 'string' } }, wait: { type: 'boolean', description: 'Wait for transaction confirmation', default: true }, id: { type: 'number', description: 'Request ID for tracking', default: 1 } }, required: ['to', 'method'] } }, { name: 'stbl_deploy', description: 'Deploy a Solidity smart contract to Stability blockchain', inputSchema: { type: 'object', properties: { code: { type: 'string', description: 'Solidity contract source code' }, arguments: { type: 'array', description: 'Constructor arguments', items: { type: 'string' } }, wait: { type: 'boolean', description: 'Wait for deployment confirmation', default: false }, id: { type: 'number', description: 'Request ID for tracking', default: 1 } }, required: ['code'] } }, { name: 'setup_init', description: 'Initialize STBL-MCP storage and configuration', inputSchema: { type: 'object', properties: {}, required: [] } }, { name: 'setup_status', description: 'Get current setup and configuration status', inputSchema: { type: 'object', properties: {}, required: [] } }, { name: 'discover_address', description: 'Automatically discover user address via contract deployment', inputSchema: { type: 'object', properties: { force: { type: 'boolean', description: 'Force rediscovery even if address already exists', default: false } }, required: [] } }, { name: 'setup_api_key', description: 'Set or update API key configuration', inputSchema: { type: 'object', properties: { apiKey: { type: 'string', description: 'Stability API key (get free key at https://portal.stabilityprotocol.com/)' } }, required: ['apiKey'] } }, { name: 'contracts_deployed', description: 'List all deployed contracts with their ABIs and deployment info', inputSchema: { type: 'object', properties: { limit: { type: 'number', description: 'Maximum number of contracts to return', default: 50 } }, required: [] } }, { name: 'contract_info', description: 'Get detailed information about a specific deployed contract', inputSchema: { type: 'object', properties: { address: { type: 'string', description: 'Contract address to get info for' } }, required: ['address'] } }, { name: 'tx_history', description: 'View transaction history with filtering options', inputSchema: { type: 'object', properties: { limit: { type: 'number', description: 'Maximum number of transactions to return', default: 20 }, type: { type: 'string', description: 'Filter by transaction type (message, deploy, call)', enum: ['message', 'deploy', 'call'] }, address: { type: 'string', description: 'Filter by contract address' } }, required: [] } } ]; async function main() { // Initialize tracking storage await tracking.ensureInitialized(); // Create the MCP server const server = new Server( { name: 'stbl-mcp', version: '0.1.0', }, { capabilities: { tools: {} } } ); // Handle list tools request server.setRequestHandler(ListToolsRequestSchema, async () => { return { tools: TOOLS }; }); // Handle call tool request server.setRequestHandler(CallToolRequestSchema, async (request) => { const { name, arguments: args } = request.params; try { switch (name) { case 'stbl_write': const writeResult = await postZktV1(args.arguments, API_KEY); // Track successful message posting if (writeResult.success && writeResult.hash) { await tracking.addTransaction({ hash: writeResult.hash, type: 'message', timestamp: Date.now(), method: 'stbl_write', args: [args.arguments], status: 'success', details: { message: args.arguments } }); } return { content: [{ type: 'text', text: JSON.stringify(writeResult, null, 2) }] }; case 'stbl_read': const readResult = await callContractRead(args, API_KEY); return { content: [{ type: 'text', text: JSON.stringify(readResult, null, 2) }] }; case 'stbl_write_contract': const writeContractResult = await callContractWrite(args, API_KEY); // Track successful contract write if (writeContractResult.success && writeContractResult.hash) { await tracking.addTransaction({ hash: writeContractResult.hash, type: 'call', timestamp: Date.now(), to: args.to, method: args.method, args: args.arguments || [], status: 'success', details: { contractAddress: args.to, functionName: args.method } }); } return { content: [{ type: 'text', text: JSON.stringify(writeContractResult, null, 2) }] }; case 'stbl_deploy': const deployResult = await deployContract(args, API_KEY); // Track successful contract deployment if (deployResult.success && deployResult.contractAddress && deployResult.hash) { // Get current user address from config const config = await tracking.getConfig(); // Add to deployed contracts registry await tracking.addDeployedContract({ address: deployResult.contractAddress, abi: deployResult.abi || [], constructorArgs: args.arguments || [], deploymentHash: deployResult.hash, deployedAt: Date.now(), deployer: config.userAddress || 'unknown', source: args.code || '' }); // Track deployment transaction await tracking.addTransaction({ hash: deployResult.hash, type: 'deploy', timestamp: Date.now(), from: config.userAddress, to: deployResult.contractAddress, method: 'constructor', args: args.arguments || [], status: 'success', details: { contractAddress: deployResult.contractAddress, constructorArgs: args.arguments || [] } }); } return { content: [{ type: 'text', text: JSON.stringify(deployResult, null, 2) }] }; case 'setup_init': await tracking.ensureInitialized(); const initResult = { success: true, message: '✅ STBL-MCP storage initialized successfully', storagePath: tracking.baseDir, directories: [ 'contracts/', 'contracts/artifacts/', 'transactions/', 'addresses/' ], files: [ 'config.json', 'contracts/deployed.json', 'transactions/history.json', 'addresses/known.json' ], timestamp: Date.now() }; return { content: [{ type: 'text', text: JSON.stringify(initResult, null, 2) }] }; case 'setup_status': const config = await tracking.getConfig(); const contracts = await tracking.getDeployedContracts(); const transactions = await tracking.getTransactionHistory(); const warnings = []; if (config.apiKey === 'try-it-out') { warnings.push('Using limited "try-it-out" API key. Get a FREE production key at https://portal.stabilityprotocol.com/'); } if (!config.userAddress) { warnings.push('User address not discovered. Run discover_address tool to find your address automatically.'); } const statusResult = { initialized: true, configuration: { apiKey: config.apiKey === 'try-it-out' ? 'try-it-out (limited)' : '***configured***', userAddress: config.userAddress || 'not discovered', setupComplete: config.setupComplete }, statistics: { contractsDeployed: contracts.length, transactionsRecorded: transactions.length, storageLocation: tracking.baseDir }, warnings, recommendations: warnings.length > 0 ? [ 'Run setup_api_key to set production API key', 'Run discover_address to find your address automatically' ] : ['Setup is complete! ✅'] }; return { content: [{ type: 'text', text: JSON.stringify(statusResult, null, 2) }] }; case 'discover_address': const currentConfig = await tracking.getConfig(); if (currentConfig.userAddress && !args.force) { return { content: [{ type: 'text', text: JSON.stringify({ success: false, error: 'Address already discovered', currentAddress: currentConfig.userAddress, message: 'Use force: true to rediscover address' }, null, 2) }] }; } try { // Deploy discovery contract const discoveryResult = await deployContract({ code: `// SPDX-License-Identifier: MIT pragma solidity ^0.8.0; contract AddressDiscovery { address public discoveredAddress; uint256 public timestamp; constructor() { discoveredAddress = msg.sender; timestamp = block.timestamp; } function getDiscoveredAddress() public view returns (address) { return discoveredAddress; } }`, arguments: [], wait: true }, API_KEY); if (discoveryResult.success && discoveryResult.contractAddress) { // Read the discovered address const readResult = await callContractRead({ to: discoveryResult.contractAddress, abi: ['function getDiscoveredAddress() public view returns (address)'], method: 'getDiscoveredAddress', arguments: [] }, API_KEY); if (readResult.success && readResult.output) { const discoveredAddress = readResult.output; // Update config with discovered address await tracking.updateConfig({ userAddress: discoveredAddress, setupComplete: true }); return { content: [{ type: 'text', text: JSON.stringify({ success: true, address: discoveredAddress, method: 'contract-deployment', transactionHash: discoveryResult.hash, contractAddress: discoveryResult.contractAddress, message: `✅ Address discovered: ${discoveredAddress}` }, null, 2) }] }; } } return { content: [{ type: 'text', text: JSON.stringify({ success: false, error: 'Failed to discover address via contract deployment' }, null, 2) }] }; } catch (error) { return { content: [{ type: 'text', text: JSON.stringify({ success: false, error: `Address discovery failed: ${error.message}` }, null, 2) }] }; } case 'setup_api_key': if (!args.apiKey || args.apiKey.trim() === '') { return { content: [{ type: 'text', text: JSON.stringify({ success: false, error: 'API key cannot be empty' }, null, 2) }] }; } const updatedConfig = await tracking.updateConfig({ apiKey: args.apiKey.trim() }); return { content: [{ type: 'text', text: JSON.stringify({ success: true, message: '✅ API key updated successfully', previousKey: currentConfig.apiKey === 'try-it-out' ? 'try-it-out (default)' : '***previous***', newKey: args.apiKey === 'try-it-out' ? 'try-it-out (limited)' : '***configured***', note: 'API key changes will take effect on next tool usage' }, null, 2) }] }; case 'contracts_deployed': const deployedContracts = await tracking.getDeployedContracts(); const limit = args.limit || 50; const limitedContracts = deployedContracts.slice(0, limit); return { content: [{ type: 'text', text: JSON.stringify({ success: true, totalContracts: deployedContracts.length, showing: limitedContracts.length, contracts: limitedContracts.map(contract => ({ address: contract.address, deploymentHash: contract.deploymentHash, deployedAt: new Date(contract.deployedAt).toISOString(), deployer: contract.deployer, name: contract.name || 'Unknown', hasABI: contract.abi && contract.abi.length > 0, functionCount: contract.abi ? contract.abi.filter(item => item.type === 'function').length : 0 })) }, null, 2) }] }; case 'contract_info': const contractAddress = args.address.toLowerCase(); const allContracts = await tracking.getDeployedContracts(); const contractInfo = allContracts.find(c => c.address.toLowerCase() === contractAddress); if (!contractInfo) { return { content: [{ type: 'text', text: JSON.stringify({ success: false, error: `Contract not found: ${args.address}`, suggestion: 'Use contracts_deployed to see all deployed contracts' }, null, 2) }] }; } return { content: [{ type: 'text', text: JSON.stringify({ success: true, contract: { address: contractInfo.address, deploymentHash: contractInfo.deploymentHash, deployedAt: new Date(contractInfo.deployedAt).toISOString(), deployer: contractInfo.deployer, name: contractInfo.name || 'Unknown', constructorArgs: contractInfo.constructorArgs, abi: contractInfo.abi, functions: contractInfo.abi ? contractInfo.abi.filter(item => item.type === 'function').map(fn => ({ name: fn.name, inputs: fn.inputs, outputs: fn.outputs, stateMutability: fn.stateMutability })) : [], source: contractInfo.source ? contractInfo.source.substring(0, 500) + (contractInfo.source.length > 500 ? '...' : '') : null } }, null, 2) }] }; case 'tx_history': const allTransactions = await tracking.getTransactionHistory(); let filteredTransactions = allTransactions; // Apply filters if (args.type) { filteredTransactions = filteredTransactions.filter(tx => tx.type === args.type); } if (args.address) { const targetAddress = args.address.toLowerCase(); filteredTransactions = filteredTransactions.filter(tx => (tx.to && tx.to.toLowerCase() === targetAddress) || (tx.from && tx.from.toLowerCase() === targetAddress) || (tx.details?.contractAddress && tx.details.contractAddress.toLowerCase() === targetAddress) ); } const txLimit = args.limit || 20; const limitedTransactions = filteredTransactions.slice(0, txLimit); return { content: [{ type: 'text', text: JSON.stringify({ success: true, totalTransactions: allTransactions.length, filteredTransactions: filteredTransactions.length, showing: limitedTransactions.length, filters: { type: args.type || 'all', address: args.address || 'all' }, transactions: limitedTransactions.map(tx => ({ hash: tx.hash, type: tx.type, timestamp: new Date(tx.timestamp).toISOString(), from: tx.from, to: tx.to, method: tx.method, status: tx.status, summary: tx.type === 'message' ? `Message: ${tx.details?.message?.substring(0, 50) || 'N/A'}` : tx.type === 'deploy' ? `Contract deployed: ${tx.to}` : tx.type === 'call' ? `Called ${tx.method} on ${tx.to}` : 'Unknown' })) }, null, 2) }] }; default: throw new Error(`Unknown tool: ${name}`); } } catch (error) { return { content: [{ type: 'text', text: `❌ Error: ${error.message}` }], isError: true }; } }); // Start the server with stdio transport const transport = new StdioServerTransport(); await server.connect(transport); console.error('🚀 STBL-MCP Server running via stdio'); } // Handle uncaught errors process.on('uncaughtException', (error) => { console.error('Uncaught exception:', error); process.exit(1); }); process.on('unhandledRejection', (reason, promise) => { console.error('Unhandled rejection at:', promise, 'reason:', reason); process.exit(1); }); // Start the server main().catch((error) => { console.error('Failed to start server:', error); process.exit(1); });