Skip to main content
Glama
index.ts22.6 kB
import { GithubRegistry } from '@hyperlane-xyz/registry'; import { ChainMap, ChainMetadata, MultiProtocolProvider, MultiProvider, TokenType, WarpCore, WarpCoreConfig, WarpRouteDeployConfig, } from '@hyperlane-xyz/sdk'; import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; import { ListResourceTemplatesRequestSchema, ReadResourceRequestSchema, } from '@modelcontextprotocol/sdk/types.js'; import { config } from 'dotenv'; import { ethers } from 'ethers'; import fs from 'fs'; import path from 'path'; import URITemplate from 'uri-templates'; import * as yaml from 'yaml'; import { z } from 'zod'; import { assetTransfer } from './assetTransfer.js'; import { LocalRegistry } from './localRegistry.js'; import { msgTransfer } from './msgTransfer.js'; import { TYPE_CHOICES } from './types.js'; import { privateKeyToSigner } from './utils.js'; import { createWarpRouteDeployConfig, deployWarpRoute } from './warpRoute.js'; import { ProtocolType } from '@hyperlane-xyz/utils'; import { createAgentConfigs, createChainConfig, loadChainDeployConfig, runCoreDeploy, } from './hyperlaneDeployer.js'; import logger from './logger.js'; import { RelayerRunner } from './RunRelayer.js'; import { ValidatorRunner } from './RunValidator.js'; // Load environment variables from .env file config(); // Create server instance const server = new McpServer( { name: 'hyperlane-mcp', version: '1.0.0', capabilities: { resources: {}, tools: {}, }, }, { capabilities: { logging: { jsonrpc: '2.0', id: 1, method: 'logging/setLevel', params: { level: 'info', }, }, resources: { subscribe: true, }, }, } ); // Create directory for hyperlane-mcp if it doesn't exist const homeDir = process.env.CACHE_DIR || process.env.HOME; let mcpDir; if (homeDir) { mcpDir = path.join(homeDir, '.hyperlane-mcp'); if (!fs.existsSync(mcpDir)) { fs.mkdirSync(mcpDir); } } else { throw new Error( 'Environment variable CACHE_DIR or HOME not set. Set it to a valid directory path.' ); } // init key const key = process.env.PRIVATE_KEY; if (!key) { throw new Error('No private key provided'); } const signer = privateKeyToSigner(key); // Initialize Github Registry once for server const githubRegistry = new GithubRegistry({ authToken: process.env.GITHUB_TOKEN, }); // Initialize Local Registry with Github Registry as source const registry = new LocalRegistry({ sourceRegistry: githubRegistry, storagePath: path.join(homeDir, '.hyperlane-mcp'), logger, }); // logger.info(JSON.stringify(await registry.getAddresses())); // logger.info(JSON.stringify(await registry.getWarpRoutes())); const URI_TEMPLATE_STRING = 'hyperlane-warp://{symbol}/{chain}'; const URI_TEMPLATE = URITemplate(URI_TEMPLATE_STRING); const URI_OBJ_TEMPATE = z.object({ symbol: z.string(), chain: z.array(z.string()), }); server.server.setRequestHandler( ListResourceTemplatesRequestSchema, async () => { return { resourceTemplates: [ { uriTemplate: URI_TEMPLATE_STRING, name: 'warpRoute', description: 'Hyperlane Warp Route for the given combination of symbol and chains. This can be fetched and used for asset transfers between chains.', mimeType: 'application/json', }, ], }; } ); server.server.setRequestHandler(ReadResourceRequestSchema, async (request) => { const values = URI_TEMPLATE.fromUri(decodeURIComponent(request.params.uri)); server.server.sendLoggingMessage({ level: 'info', data: `Parsed URI values: ${JSON.stringify( values )} from URI: ${decodeURIComponent(request.params.uri)}`, }); const parsed = URI_OBJ_TEMPATE.safeParse(values); if (!parsed.success) { throw new Error('Invalid URI parameters'); } let { symbol, chain } = parsed.data; chain = chain.filter((c) => c !== ''); server.server.sendLoggingMessage({ level: 'info', data: `Fetching warp routes for symbol: ${typeof symbol}:${symbol} and chains: ${typeof chain}:${JSON.stringify( chain )}`, }); let warpRoutes; try { warpRoutes = await registry.getWarpRoutesBySymbolAndChains(symbol, chain); } catch (error) { server.server.sendLoggingMessage({ level: 'error', data: `Error fetching warp routes: ${error}`, }); throw new Error(`Error fetching warp routes: ${error}`); } return { contents: [ { uri: request.params.uri, name: `Hyperlane Warp Route for ${symbol} on ${chain.join('-')}`, mimeType: 'application/json', text: JSON.stringify(warpRoutes, null, 2), }, ], }; }); server.tool( 'cross-chain-message-transfer', 'Transfers a cross-chain message.', { origin: z.string().describe('Origin chain'), destination: z.string().describe('Destination chain'), recipient: z .string() .length(42) .regex(/^0x[a-fA-F0-9]{40}$/, 'Invalid EVM address') .describe('Recipient address'), messageBody: z.string().describe('Message body'), }, async ({ origin, destination, recipient, messageBody }) => { server.server.sendLoggingMessage({ level: 'info', data: `Starting cross-chain message transfer... Parameters: origin=${origin}, destination=${destination}, recipient=${recipient}, messageBody=${messageBody}`, }); server.server.sendLoggingMessage({ level: 'info', data: `Using signer with address: ${signer.address}`, }); server.server.sendLoggingMessage({ level: 'info', data: 'Initializing Github Registry...', }); registry.listRegistryContent(); const originChainMetadata = (await registry.getChainMetadata(origin))!; const destinationChainMetadata = (await registry.getChainMetadata( destination ))!; const chainMetadata: ChainMap<ChainMetadata> = { [origin]: originChainMetadata, [destination]: destinationChainMetadata, }; server.server.sendLoggingMessage({ level: 'info', data: `Chain metadata fetched: ${JSON.stringify(chainMetadata, null, 2)}`, }); const multiProvider = new MultiProvider(chainMetadata, { signers: { [origin]: signer, [destination]: signer, }, }); server.server.sendLoggingMessage({ level: 'info', data: `MultiProvider initialized with chains: ${JSON.stringify( multiProvider, null, 2 )}`, }); server.server.sendLoggingMessage({ level: 'info', data: 'Initiating message transfer...', }); const [dispatchTx, message] = await msgTransfer({ origin, destination, recipient, messageBody: ethers.utils.formatBytes32String(messageBody), registry, multiProvider, }); server.server.sendLoggingMessage({ level: 'info', data: 'Message transfer completed successfully', }); return { content: [ { type: 'text', text: `Message dispatched successfully. Transaction Hash: ${dispatchTx.transactionHash}.\n Message ID for the dispatched message: ${message.id}`, }, ], }; } ); server.tool( 'cross-chain-asset-transfer', "Transfers tokens/assets between multiple blockchain networks using Hyperlane's cross-chain infrastructure.\n\n" + 'FUNCTIONALITY:\n' + '• Moves tokens from one blockchain to another (e.g., USDC from Ethereum to Polygon)\n' + '• Supports sequential transfers across multiple chains in a single operation\n' + '• Handles various token types including native tokens, ERC20 tokens, and synthetic tokens\n\n' + 'PREREQUISITES:\n' + '• A warp route must exist for the specified token symbol and chain combination\n' + '• If no warp route exists, deploy one first using the `deploy-warp-route` tool\n' + '• Sufficient token balance on the origin chain\n' + '• Sufficient gas tokens on all involved chains for transaction fees\n\n' + 'PARAMETERS:\n' + '• symbol: The token identifier (e.g., "USDC", "ETH", "WBTC")\n' + '• chains: Array of blockchain names in transfer order (e.g., ["ethereum", "polygon", "arbitrum"])\n' + '• amount: Token amount in wei or smallest token units (e.g., "1000000" for 1 USDC with 6 decimals)\n' + '• recipient: Destination wallet address (defaults to sender if not specified)\n\n' + 'OUTPUT:\n' + '• Returns transaction hashes and message IDs for each cross-chain transfer\n' + '• Each transfer between adjacent chains generates one transaction\n' + '• Use message IDs to track delivery status across chains\n\n' + 'EXAMPLE USE CASES:\n' + '• Bridge USDC from Ethereum to Polygon\n' + '• Multi-hop transfer: ETH from Ethereum → Arbitrum → Base\n' + '• Cross-chain token arbitrage or yield farming', { symbol: z.string().describe('Token symbol to transfer'), chains: z .array(z.string()) .describe('Chains to transfer asset between in order of transfer'), amount: z.string().describe('Amount to transfer (in wei or token units)'), recipient: z .string() .length(42) .regex(/^0x[a-fA-F0-9]{40}$/, 'Invalid EVM address') .optional() .default(signer.address) .describe('Recipient address'), }, async ({ symbol, chains, amount, recipient }) => { server.server.sendLoggingMessage({ level: 'info', data: `Starting cross-chain asset transfer... Parameters: symbol=${symbol}, chains=${chains.join( ', ' )}, amount=${amount}, recipient=${recipient}`, }); // Fetch warp route config from registry const warpRoutes = await registry.getWarpRoutesBySymbolAndChains( symbol, chains ); if (!warpRoutes || warpRoutes.length === 0) { return { content: [ { type: 'text', text: `No warp route config found for symbol "${symbol}" and chains [${chains.join( ', ' )}]. Please deploy a warp route first using the 'deploy-warp-route' tool.`, }, ], }; } const chainMetadata: ChainMap<ChainMetadata> = Object.fromEntries( await Promise.all( chains.map(async (chain) => [ chain, (await registry.getChainMetadata(chain))!, ]) ) ); const multiProvider = new MultiProvider(chainMetadata, { signers: Object.fromEntries(chains.map((chain) => [chain, signer])), providers: Object.fromEntries( await Promise.all( chains.map(async (chain) => [ chain, new ethers.providers.JsonRpcProvider( chainMetadata[chain].rpcUrls[0].http ), ]) ) ), }); server.server.sendLoggingMessage({ level: 'info', data: `MultiProvider initialized with chains: ${JSON.stringify( multiProvider, null, 2 )}`, }); let warpCoreConfig: WarpCoreConfig | null = null; for (const route of warpRoutes) { const warpCore = WarpCore.FromConfig( MultiProtocolProvider.fromMultiProvider(multiProvider), route ); const tokensForRoute = warpCore.getTokensForRoute(chains[0], chains[1]); if (tokensForRoute.length > 0) { warpCoreConfig = route; break; } } if (!warpCoreConfig) { return { content: [ { type: 'text', text: `No valid warp core config found for symbol "${symbol}" and chains [${chains.join( ', ' )}]. Please deploy a warp route first using the 'deploy-warp-route' tool.`, }, ], }; } else { server.server.sendLoggingMessage({ level: 'info', data: `Found warp core config: ${JSON.stringify( warpCoreConfig, null, 2 )}`, }); } server.server.sendLoggingMessage({ level: 'info', data: 'Initiating asset transfer...', }); const deliveryResult = await assetTransfer({ warpCoreConfig, chains, amount, recipient, multiProvider, }); if (!deliveryResult || deliveryResult.length !== chains.length - 1) { return { content: [ { type: 'text', text: `Error in asset transfer. No delivery result couldn't be generated`, }, ], }; } server.server.sendLoggingMessage({ level: 'info', data: 'Asset transfer completed successfully', }); return { content: [ { mimeType: 'application/json', type: 'text', text: JSON.stringify( deliveryResult.map(([dispatchTx, message]) => ({ transactionHash: dispatchTx.transactionHash, messageId: message.id, })), null, 2 ), }, ], }; } ); server.tool( 'deploy-warp-route', 'Deploys a warp route.', { warpChains: z .array(z.string()) .describe('Warp chains to deploy the route on'), tokenTypes: z .array( z.enum( TYPE_CHOICES.map((choice) => choice.name) as [string, ...string[]] ) ) .describe('Token types to deploy'), }, async ({ warpChains, tokenTypes }) => { server.server.sendLoggingMessage({ level: 'info', data: `Deploying warp route with chains: ${warpChains.join( ', ' )} and token types: ${tokenTypes.join(', ')}.`, }); const fileName = `routes/${ warpChains.map((chain, i) => `${chain}:${tokenTypes[i]}`).join('-') + '.yaml' }`; let warpRouteConfig: WarpRouteDeployConfig; const filePath = path.join(mcpDir, fileName); if (fs.existsSync(filePath)) { server.server.sendLoggingMessage({ level: 'info', data: `Warp Route Already exists @ ${fileName} already exists. Skipping Config Creation.`, }); const fileContent = fs.readFileSync(filePath, 'utf-8'); warpRouteConfig = yaml.parse(fileContent) as WarpRouteDeployConfig; return { content: [ { type: 'text', text: `Warp Route Config already exists @ ${fileName}. Skipping Config Creation. Config: ${JSON.stringify( warpRouteConfig, null, 2 )}`, }, ], }; } else { server.server.sendLoggingMessage({ level: 'info', data: `Creating Warp Route Config @ ${fileName}`, }); warpRouteConfig = await createWarpRouteDeployConfig({ warpChains, tokenTypes: tokenTypes.map( (t) => TokenType[t as keyof typeof TokenType] ), signerAddress: signer.address, registry, outPath: './warpRouteDeployConfig.yaml', }); server.server.sendLoggingMessage({ level: 'info', data: `Warp route deployment config created: ${JSON.stringify( warpRouteConfig, null, 2 )}`, }); } const chainMetadata: ChainMap<ChainMetadata> = {}; for (const chain of warpChains) { chainMetadata[chain] = (await registry.getChainMetadata(chain))!; } const multiProvider = new MultiProvider(chainMetadata, { signers: Object.fromEntries(warpChains.map((chain) => [chain, signer])), }); const deploymentConfig = await deployWarpRoute({ registry, chainMetadata, multiProvider, warpRouteDeployConfig: warpRouteConfig, filePath, }); server.server.sendLoggingMessage({ level: 'info', data: `Warp route deployed successfully. Config: ${JSON.stringify( warpRouteConfig, null, 2 )}`, }); return { content: [ { type: 'text', text: `Warp route deployment config created successfully. Config: ${JSON.stringify( deploymentConfig, null, 2 )}`, }, ], }; } ); server.tool( 'deploy-chain', 'Deploys a new chain to the Hyperlane network.', { chainName: z.string().describe('Name of the chain to deploy'), chainId: z.number().describe('Chain ID of the chain to deploy'), rpcUrl: z.string().url().describe('RPC URL for the chain'), tokenSymbol: z.string().describe('Native token symbol'), tokenName: z.string().describe('Native token name'), isTestnet: z .boolean() .default(false) .describe('Whether this is a testnet chain'), }, async ({ chainName, chainId, rpcUrl, tokenSymbol, tokenName, isTestnet }) => { const existingConfig = await loadChainDeployConfig(chainName); if (existingConfig) { server.server.sendLoggingMessage({ level: 'info', data: `Chain deployment config already exists for ${chainName}. Using existing config.`, }); return { content: [ { type: 'text', text: `Chain config already exists. Skipping config creation.\n${JSON.stringify( existingConfig, null, 2 )}`, }, ], }; } server.server.sendLoggingMessage({ level: 'info', data: `Deploying chain ${chainName} with ID ${chainId}...`, }); // Step 1: Create Chain Config + Save const chainConfig = { chainName, chainId, rpcUrl, tokenSymbol, tokenName, isTestnet, }; await createChainConfig({ config: chainConfig, registry, }); server.server.sendLoggingMessage({ level: 'info', data: `Chain config created successfully: ${JSON.stringify( chainConfig, null, 2 )}`, }); // Step 2: Deploy Core Contracts const deployConfig = { config: chainConfig, registry }; // server.server.sendLoggingMessage({ // level: 'info', // data: `this is the deploy config: ${JSON.stringify(deployConfig, null, 2)}`, // }); const deployedAddress = await runCoreDeploy(deployConfig); server.server.sendLoggingMessage({ level: 'info', data: `Core contracts deployed successfully for ${chainName}. Deployed address: ${JSON.stringify( deployedAddress, null, 2 )}`, }); // Step 3: Create Agent Configs const metadata = { [chainName]: { name: chainName, displayName: chainName, chainId, domainId: chainId, protocol: ProtocolType.Ethereum, rpcUrls: [{ http: rpcUrl }], isTestnet, }, } as ChainMap<ChainMetadata>; server.server.sendLoggingMessage({ level: 'info', data: `Create metadata for ${chainName}: ${JSON.stringify( metadata, null, 2 )}`, }); const multiProvider = new MultiProvider(metadata, { signers: { [signer.address]: signer, }, }); const outPath = path.join(mcpDir, 'agents'); await createAgentConfigs(registry, multiProvider, outPath, chainName); server.server.sendLoggingMessage({ level: 'info', data: `✅ Chain deployment and agent config creation complete for ${chainName}`, }); return { content: [ { type: 'text', text: `✅ Successfully deployed ${chainName} and generated agent config.\n\nSaved config: ${JSON.stringify( chainConfig, null, 2 )}`, }, ], }; } ); server.tool( 'run-validator', 'Runs a validator for a specific chain.', { chainName: z.string().describe('Name of the chain to validate'), }, async ({ chainName }) => { server.server.sendLoggingMessage({ level: 'info', data: `Starting validator for chain: ${chainName}...`, }); const configFilePath = path.join( mcpDir, `agents/${chainName}-agent-config.json` ); server.server.sendLoggingMessage({ level: 'info', data: `Config file path: ${configFilePath}`, }); const validatorKey = process.env.PRIVATE_KEY; if (!validatorKey) { throw new Error('No private key provided'); } try { const validatorRunner = new ValidatorRunner( chainName, validatorKey, configFilePath ); await validatorRunner.run(); return { content: [ { type: 'text', text: `Validator started successfully for chain: ${chainName}`, }, ], }; } catch (error) { server.server.sendLoggingMessage({ level: 'error', data: `Error starting validator for chain ${chainName}: ${error}`, }); throw error; } } ); server.tool( 'run-relayer', 'Runs a relayer for specified chains.', { relayChains: z.array(z.string()).describe('Chains to relay between'), validatorChainName: z.string().describe('Name of the validator chain'), }, async ({ relayChains, validatorChainName }) => { server.server.sendLoggingMessage({ level: 'info', data: `Starting relayer for chains: ${relayChains.join(', ')}...`, }); const configFilePath = path.join( homeDir!, '.hyperlane-mcp', 'agents', `${validatorChainName}-agent-config.json` ); const relayerKey = process.env.PRIVATE_KEY; if (!relayerKey) { throw new Error('No private key provided'); } try { const relayerRunner = new RelayerRunner( relayChains, relayerKey, configFilePath, validatorChainName ); await relayerRunner.run(); return { content: [ { type: 'text', text: `Relayer started successfully for chains: ${relayChains.join( ', ' )}`, }, ], }; } catch (error) { server.server.sendLoggingMessage({ level: 'error', data: `Error starting relayer for chains ${relayChains.join( ', ' )}: ${error}`, }); throw error; } } ); async function main() { const transport = new StdioServerTransport(); await server.connect(transport); console.error('Hyperlane MCP server started. Listening for requests...'); } main().catch((error) => { console.error('Fatal error in main():', error); process.exit(1); });

Implementation Reference

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/Suryansh-23/hyperlane-mcp'

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