Skip to main content
Glama
stdio.ts•6.97 kB
import { Server } from "@modelcontextprotocol/sdk/server/index.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { ListToolsRequestSchema, CallToolRequestSchema } from "@modelcontextprotocol/sdk/types.js"; 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"; // Validate environment variables if (!process.env.UNLOCK_ADDRESS) { throw new Error("UNLOCK_ADDRESS environment variable is required"); } if (!process.env.PRIVATE_KEY) { throw new Error("PRIVATE_KEY 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; const PK = process.env.PRIVATE_KEY; // Logging utility function log(level: 'info' | 'error' | 'warn', message: string, data?: any) { const timestamp = new Date().toISOString(); const logData = data ? ` ${JSON.stringify(data)}` : ''; console.error(`[${timestamp}] ${level.toUpperCase()}: ${message}${logData}`); } // Initialize providers and signers with error handling const providers: Record<number, ethers.JsonRpcProvider> = {}; const signers: Record<number, ethers.Wallet> = {}; try { for (const id of [8453, 84532]) { const rpcUrl = rpc(id as 8453 | 84532); providers[id] = new ethers.JsonRpcProvider(rpcUrl); signers[id] = new ethers.Wallet(PK, providers[id]); log('info', `Initialized provider for chain ${id}`, { rpcUrl: rpcUrl.split('/').slice(0, 3).join('/') }); } } catch (error) { log('error', 'Failed to initialize providers', { error: error instanceof Error ? error.message : error }); throw error; } const server = new Server( { name: "unlock-stdio", version: "1.0.0" }, { capabilities: { tools: {} } } ); server.setRequestHandler(ListToolsRequestSchema, async () => { log('info', 'Tools list requested'); return { tools: UNLOCK_TOOLS }; }); server.setRequestHandler(CallToolRequestSchema, async ({ params: { name, arguments: args } }) => { try { log('info', 'Tool call received', { name, args }); // Validate function name if (!(name in FUNCTION_SCHEMAS)) { throw new Error(`Unknown function: ${name}`); } // Validate input arguments const schema = FUNCTION_SCHEMAS[name as FunctionName]; const validatedArgs = schema.parse(args); const chainId = validatedArgs.chainId; // Check if provider exists for chain if (!(chainId in providers)) { throw new Error(`Unsupported chain ID: ${chainId}`); } // Determine contract address and ABI 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; // For upgradeLock, we need the lockAddress parameter but still call Unlock contract if (name === "upgradeLock") { // Validation already ensures lockAddress is present in validatedArgs } } else { // This is a PublicLock function const lockAddr = 'lockAddress' in validatedArgs ? validatedArgs.lockAddress : undefined; const finalAddr = lockAddr || LOCK; if (!finalAddr) { throw new Error("lockAddress is required for PublicLock functions"); } contractAddress = finalAddr as string; abi = PublicLockAbi; } // Prepare contract interaction const signer = signers[chainId]; const contract = new ethers.Contract(contractAddress, abi, signer); // Map function names for special cases const actualFunctionName = name === "chainIdRead" ? "chainId" : name; // Check if function exists on contract if (!(actualFunctionName in contract)) { throw new Error(`Function ${actualFunctionName} not found on contract`); } // Prepare function arguments (exclude metadata fields) const txArgs = Object.entries(validatedArgs) .filter(([k]) => !["chainId", "lockAddress"].includes(k)) .map(([, v]) => v); log('info', 'Calling contract function', { function: actualFunctionName, contractAddress, chainId, argsCount: txArgs.length }); // Check if this is a read or write function if (isReadFunction(name)) { // For read functions, just call the function const result = await contract[actualFunctionName](...txArgs); log('info', 'Read function completed', { function: actualFunctionName, result: result.toString() }); return { content: [{ type: "text", text: `Function ${name} returned: ${result.toString()}` }] }; } else if (isWriteFunction(name)) { // For write functions, send transaction and wait for confirmation const tx = await contract[actualFunctionName](...txArgs); log('info', 'Transaction sent', { function: actualFunctionName, txHash: tx.hash }); const receipt = await tx.wait(); log('info', 'Transaction mined', { function: name, txHash: tx.hash, blockNumber: receipt.blockNumber, gasUsed: receipt.gasUsed.toString() }); return { content: [{ type: "text", text: `Transaction ${tx.hash} mined in block ${receipt.blockNumber}. Gas used: ${receipt.gasUsed.toString()}` }] }; } else { throw new Error(`Function ${name} is not categorized as read or write`); } } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Unknown error'; log('error', 'Tool call failed', { name, error: errorMessage }); return { content: [{ type: "text", text: `Error: ${errorMessage}` }], isError: true }; } }); // Start the server async function startServer() { try { const transport = new StdioServerTransport(); await server.connect(transport); log('info', '🔓 Unlock MCP stdio server ready', { toolsCount: UNLOCK_TOOLS.length, supportedChains: [8453, 84532], unlockAddress: UNLOCK, defaultLockAddress: LOCK || 'none' }); } catch (error) { log('error', 'Failed to start server', { error: error instanceof Error ? error.message : error }); process.exit(1); } } startServer();

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