Skip to main content
Glama
proxy.ts•10.6 kB
import express from "express"; import cors from "cors"; import { ethers } from "ethers"; import UnlockAbi from "../abis/UnlockV14_abi.json" with { type: "json" }; import PublicLockAbi from "../abis/PublicLockV15_abi.json" with { type: "json" }; import { rpc } from "./shared.js"; import { FUNCTION_SCHEMAS, type FunctionName } from "./schemas.js"; import { UNLOCK_TOOLS, isReadFunction, isWriteFunction } from "./tools.js"; const app = express(); app.use(cors({ origin: true, // Allow all origins for development methods: ['GET', 'POST', 'OPTIONS'], allowedHeaders: ['Content-Type', 'Authorization'] })); app.use(express.json({ limit: '10mb' })); // Logging middleware app.use((req, res, next) => { const timestamp = new Date().toISOString(); console.log(`[${timestamp}] ${req.method} ${req.path}`); next(); }); // Validate environment variables if (!process.env.UNLOCK_ADDRESS) { throw new Error("UNLOCK_ADDRESS environment variable is required"); } if (!process.env.INFURA_API_KEY && !process.env.ALCHEMY_API_KEY) { throw new Error("Either INFURA_API_KEY or ALCHEMY_API_KEY is required"); } const UNLOCK = process.env.UNLOCK_ADDRESS; const LOCK = process.env.LOCK_ADDRESS; // Initialize providers for read operations const providers: Record<number, ethers.JsonRpcProvider> = {}; try { for (const id of [8453, 84532]) { const rpcUrl = rpc(id as 8453 | 84532); providers[id] = new ethers.JsonRpcProvider(rpcUrl); console.log(`Initialized read provider for chain ${id}`); } } catch (error) { console.error('Failed to initialize providers:', error); throw error; } function buildTx(chainId: number, to: string, abi: any[], method: string, args: any[], value = "0") { try { const iface = new ethers.Interface(abi); const data = iface.encodeFunctionData(method, args); return { to, data, value, chainId }; } catch (error) { throw new Error(`Failed to build transaction: ${error instanceof Error ? error.message : error}`); } } // Helper function to validate function arguments function validateFunctionCall(functionName: string, args: any) { if (!(functionName in FUNCTION_SCHEMAS)) { throw new Error(`Unknown function: ${functionName}`); } const schema = FUNCTION_SCHEMAS[functionName as FunctionName]; return schema.parse(args); } // Helper function to prepare contract arguments function prepareContractArgs(validatedArgs: any): any[] { return Object.entries(validatedArgs) .filter(([k]) => !["chainId", "lockAddress"].includes(k)) .map(([, v]) => v); } // MCP-compatible endpoints app.get('/tools', (req, res) => { res.json({ tools: UNLOCK_TOOLS }); }); app.post('/tools/call', async (req, res) => { try { const { name, arguments: args } = req.body; if (!name || !args) { return res.status(400).json({ error: 'Missing name or arguments' }); } // Validate function and arguments const validatedArgs = validateFunctionCall(name, args); const chainId = validatedArgs.chainId; // Check if this is a read function that can be executed directly if (isReadFunction(name)) { // Execute read function directly const provider = providers[chainId]; if (!provider) { return res.status(400).json({ error: `Unsupported chain ID: ${chainId}` }); } let contractAddress: string; let abi: any[]; // Check if this is an Unlock protocol function const unlockFunctions = ["createLock", "createUpgradeableLock", "upgradeLock", "chainIdRead", "unlockVersion", "governanceToken", "getGlobalTokenSymbol", "publicLockLatestVersion"]; if (unlockFunctions.includes(name)) { contractAddress = UNLOCK; abi = UnlockAbi; } else { // This is a PublicLock function const lockAddr = 'lockAddress' in validatedArgs ? validatedArgs.lockAddress : undefined; const finalAddr = lockAddr || LOCK; if (!finalAddr) { return res.status(400).json({ error: "lockAddress is required for PublicLock functions" }); } contractAddress = finalAddr as string; abi = PublicLockAbi; } const contract = new ethers.Contract(contractAddress, abi, provider); const contractArgs = prepareContractArgs(validatedArgs); // Map function names for special cases const actualFunctionName = name === "chainIdRead" ? "chainId" : name; const result = await contract[actualFunctionName](...contractArgs); res.json({ success: true, result: result.toString(), function: name, chainId }); } else { // For write functions, return transaction data for client to sign let contractAddress: string; let abi: any[]; // Check if this is an Unlock protocol function const unlockFunctions = ["createLock", "createUpgradeableLock", "upgradeLock", "chainIdRead", "unlockVersion", "governanceToken", "getGlobalTokenSymbol", "publicLockLatestVersion"]; if (unlockFunctions.includes(name)) { contractAddress = UNLOCK; abi = UnlockAbi; } else { // This is a PublicLock function const lockAddr = 'lockAddress' in validatedArgs ? validatedArgs.lockAddress : undefined; const finalAddr = lockAddr || LOCK; if (!finalAddr) { return res.status(400).json({ error: "lockAddress is required for PublicLock functions" }); } contractAddress = finalAddr as string; abi = PublicLockAbi; } const contractArgs = prepareContractArgs(validatedArgs); // Map function names for special cases const actualFunctionName = name === "chainIdRead" ? "chainId" : name; const txData = buildTx(chainId, contractAddress, abi, actualFunctionName, contractArgs); res.json({ success: true, transaction: txData, function: name, chainId }); } } catch (error) { console.error('Tool call error:', error); res.status(400).json({ success: false, error: error instanceof Error ? error.message : 'Unknown error' }); } }); // Legacy endpoints for backward compatibility app.post("/unlock/:method", async (req, res) => { try { const { chainId, args } = req.body; const method = req.params.method; if (!chainId || !args) { return res.status(400).json({ error: 'Missing chainId or args' }); } // Validate using new schema system const functionArgs = { chainId, ...args }; const validatedArgs = validateFunctionCall(method, functionArgs); const contractArgs = prepareContractArgs(validatedArgs); const txData = buildTx(chainId, UNLOCK, UnlockAbi, method, contractArgs); res.json(txData); } catch (error) { console.error('Unlock method error:', error); res.status(400).json({ error: error instanceof Error ? error.message : 'Unknown error' }); } }); app.post("/lock/:method", async (req, res) => { try { const { chainId, lockAddress, args } = req.body; const method = req.params.method; if (!chainId) { return res.status(400).json({ error: 'Missing chainId' }); } const addr = lockAddress || LOCK; if (!addr) { return res.status(400).json({ error: "lockAddress required" }); } // Validate using new schema system const functionArgs = { chainId, lockAddress: addr, ...args }; const validatedArgs = validateFunctionCall(method, functionArgs); const contractArgs = prepareContractArgs(validatedArgs); const txData = buildTx(chainId, addr, PublicLockAbi, method, contractArgs); res.json(txData); } catch (error) { console.error('Lock method error:', error); res.status(400).json({ error: error instanceof Error ? error.message : 'Unknown error' }); } }); // Enhanced SSE endpoint for n8n integration app.get('/sse', (req, res) => { res.writeHead(200, { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', 'Connection': 'keep-alive', 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Headers': 'Cache-Control' }); // Send initial connection message res.write(`data: ${JSON.stringify({ type: 'connection', message: 'Connected to Unlock MCP proxy server', timestamp: new Date().toISOString(), tools: UNLOCK_TOOLS.length })}\n\n`); // Keep connection alive const keepAlive = setInterval(() => { res.write(`data: ${JSON.stringify({ type: 'heartbeat', timestamp: new Date().toISOString() })}\n\n`); }, 30000); req.on('close', () => { clearInterval(keepAlive); }); }); // Health check endpoint app.get('/health', (req, res) => { res.json({ status: 'healthy', timestamp: new Date().toISOString(), version: '1.0.0', tools: UNLOCK_TOOLS.length, supportedChains: [8453, 84532], unlockAddress: UNLOCK, defaultLockAddress: LOCK || null }); }); // Root endpoint with API documentation app.get('/', (req, res) => { res.json({ name: 'Unlock MCP Proxy Server', version: '1.0.0', description: 'Model Context Protocol server for Unlock Protocol on Base networks', endpoints: { 'GET /': 'This documentation', 'GET /health': 'Health check', 'GET /tools': 'List available MCP tools', 'POST /tools/call': 'Execute MCP tool', 'GET /sse': 'Server-Sent Events endpoint', 'POST /unlock/:method': 'Legacy Unlock contract methods', 'POST /lock/:method': 'Legacy PublicLock contract methods' }, supportedChains: [8453, 84532], toolsCount: UNLOCK_TOOLS.length }); }); // Error handling middleware app.use((error: any, req: express.Request, res: express.Response, next: express.NextFunction) => { console.error('Unhandled error:', error); res.status(500).json({ success: false, error: 'Internal server error' }); }); const PORT = process.env.PORT || 3000; app.listen(PORT, () => { console.log(`🔓 Unlock MCP proxy server listening on port ${PORT}`); console.log(`Available tools: ${UNLOCK_TOOLS.length}`); console.log(`Supported chains: Base (8453), Base-Sepolia (84532)`); console.log(`Health check: http://localhost:${PORT}/health`); console.log(`SSE endpoint: http://localhost:${PORT}/sse`); }); // Graceful shutdown process.on('SIGTERM', () => { console.log('Received SIGTERM, shutting down gracefully'); process.exit(0); }); process.on('SIGINT', () => { console.log('Received SIGINT, shutting down gracefully'); process.exit(0); });

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/blahkheart/unlock-mcp'

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