#!/usr/bin/env node
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import {
CallToolRequestSchema,
ListToolsRequestSchema,
Tool,
} from '@modelcontextprotocol/sdk/types.js';
import { z } from 'zod';
import { config } from 'dotenv';
import { BridgeService } from './services/bridge.js';
import { QuoteService } from './services/quote.js';
import { StatusService } from './services/status.js';
import { BalanceService } from './services/balance.js';
import {
QuoteRequestSchema,
BridgeRequestSchema,
SupportedTokensRequestSchema,
BalanceQuerySchema,
createSuccessResponse,
createErrorResponse,
MCPToolResult,
SupportedChainId,
} from './types/index.js';
import { CHAIN_INFO, NATIVE_TOKENS } from './constants/index.js';
// Load environment variables
config();
class ValueRouterMCPServer {
private server: Server;
private bridgeService: BridgeService;
private quoteService: QuoteService;
private statusService: StatusService;
private balanceService: BalanceService;
constructor() {
this.server = new Server(
{
name: 'valuerouter-mcp-server',
version: '1.0.0',
},
{
capabilities: {
tools: {},
},
}
);
this.bridgeService = new BridgeService();
this.quoteService = new QuoteService();
this.statusService = new StatusService();
this.balanceService = new BalanceService();
this.setupHandlers();
}
private setupHandlers() {
this.server.setRequestHandler(ListToolsRequestSchema, async () => {
return {
tools: [
{
name: 'get_supported_chains',
description: 'Get all supported chains for USDC bridging',
inputSchema: {
type: 'object',
properties: {
includeTestnets: {
type: 'boolean',
description: 'Whether to include testnet chains',
default: false,
},
},
additionalProperties: false,
},
},
{
name: 'get_supported_tokens',
description: 'Get supported tokens for a specific chain or all chains',
inputSchema: {
type: 'object',
properties: {
chainId: {
oneOf: [
{ type: 'number' },
{ type: 'string' },
],
description: 'Chain ID to get tokens for (optional)',
},
includeTestnets: {
type: 'boolean',
description: 'Whether to include testnet tokens',
default: false,
},
},
additionalProperties: false,
},
},
{
name: 'get_bridge_quote',
description: 'Get a quote for bridging USDC or other tokens between chains',
inputSchema: {
type: 'object',
properties: {
fromChainId: {
oneOf: [
{ type: 'number' },
{ type: 'string' },
],
description: 'Source chain ID',
},
toChainId: {
oneOf: [
{ type: 'number' },
{ type: 'string' },
],
description: 'Destination chain ID',
},
fromToken: {
type: 'object',
properties: {
address: { type: 'string' },
chainId: { oneOf: [{ type: 'number' }, { type: 'string' }] },
symbol: { type: 'string' },
name: { type: 'string' },
decimals: { type: 'number' },
logoURI: { type: 'string' },
isNative: { type: 'boolean' },
},
required: ['address', 'chainId', 'symbol', 'name', 'decimals'],
},
toToken: {
type: 'object',
properties: {
address: { type: 'string' },
chainId: { oneOf: [{ type: 'number' }, { type: 'string' }] },
symbol: { type: 'string' },
name: { type: 'string' },
decimals: { type: 'number' },
logoURI: { type: 'string' },
isNative: { type: 'boolean' },
},
required: ['address', 'chainId', 'symbol', 'name', 'decimals'],
},
amount: {
type: 'string',
description: 'Amount to bridge in smallest unit (wei, lamports, etc.)',
},
slippageBps: {
type: 'number',
description: 'Slippage tolerance in basis points (100 = 1%)',
default: 100,
},
userAddress: {
type: 'string',
description: 'User address for better quote accuracy (optional)',
},
},
required: ['fromChainId', 'toChainId', 'fromToken', 'toToken', 'amount'],
additionalProperties: false,
},
},
{
name: 'execute_bridge',
description: 'Execute a bridge transaction (simulation only - returns transaction data)',
inputSchema: {
type: 'object',
properties: {
fromChainId: {
oneOf: [
{ type: 'number' },
{ type: 'string' },
],
description: 'Source chain ID',
},
toChainId: {
oneOf: [
{ type: 'number' },
{ type: 'string' },
],
description: 'Destination chain ID',
},
fromToken: {
type: 'object',
properties: {
address: { type: 'string' },
chainId: { oneOf: [{ type: 'number' }, { type: 'string' }] },
symbol: { type: 'string' },
name: { type: 'string' },
decimals: { type: 'number' },
logoURI: { type: 'string' },
isNative: { type: 'boolean' },
},
required: ['address', 'chainId', 'symbol', 'name', 'decimals'],
},
toToken: {
type: 'object',
properties: {
address: { type: 'string' },
chainId: { oneOf: [{ type: 'number' }, { type: 'string' }] },
symbol: { type: 'string' },
name: { type: 'string' },
decimals: { type: 'number' },
logoURI: { type: 'string' },
isNative: { type: 'boolean' },
},
required: ['address', 'chainId', 'symbol', 'name', 'decimals'],
},
amount: {
type: 'string',
description: 'Amount to bridge in smallest unit',
},
recipientAddress: {
type: 'string',
description: 'Recipient address on destination chain',
},
userAddress: {
type: 'string',
description: 'User address initiating the transaction',
},
slippageBps: {
type: 'number',
description: 'Slippage tolerance in basis points',
default: 100,
},
memo: {
type: 'string',
description: 'Memo for Cosmos chains (optional)',
},
},
required: ['fromChainId', 'toChainId', 'fromToken', 'toToken', 'amount', 'recipientAddress', 'userAddress'],
additionalProperties: false,
},
},
{
name: 'get_transaction_status',
description: 'Get the status of a bridge transaction',
inputSchema: {
type: 'object',
properties: {
transactionHash: {
type: 'string',
description: 'Transaction hash to check status for',
},
fromChainId: {
oneOf: [
{ type: 'number' },
{ type: 'string' },
],
description: 'Source chain ID',
},
toChainId: {
oneOf: [
{ type: 'number' },
{ type: 'string' },
],
description: 'Destination chain ID',
},
},
required: ['transactionHash', 'fromChainId', 'toChainId'],
additionalProperties: false,
},
},
{
name: 'get_user_balance',
description: 'Get user token balance on a specific chain',
inputSchema: {
type: 'object',
properties: {
chainId: {
oneOf: [
{ type: 'number' },
{ type: 'string' },
],
description: 'Chain ID to check balance on',
},
tokenAddress: {
type: 'string',
description: 'Token contract address',
},
userAddress: {
type: 'string',
description: 'User address to check balance for',
},
},
required: ['chainId', 'tokenAddress', 'userAddress'],
additionalProperties: false,
},
},
{
name: 'estimate_bridge_fees',
description: 'Estimate fees for a bridge transaction',
inputSchema: {
type: 'object',
properties: {
fromChainId: {
oneOf: [
{ type: 'number' },
{ type: 'string' },
],
description: 'Source chain ID',
},
toChainId: {
oneOf: [
{ type: 'number' },
{ type: 'string' },
],
description: 'Destination chain ID',
},
amount: {
type: 'string',
description: 'Amount to bridge',
},
tokenAddress: {
type: 'string',
description: 'Token address (optional, defaults to USDC)',
},
},
required: ['fromChainId', 'toChainId', 'amount'],
additionalProperties: false,
},
},
] as Tool[],
};
});
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
try {
switch (name) {
case 'get_supported_chains':
return await this.getSupportedChains(args);
case 'get_supported_tokens':
return await this.getSupportedTokens(args);
case 'get_bridge_quote':
return await this.getBridgeQuote(args);
case 'execute_bridge':
return await this.executeBridge(args);
case 'get_transaction_status':
return await this.getTransactionStatus(args);
case 'get_user_balance':
return await this.getUserBalance(args);
case 'estimate_bridge_fees':
return await this.estimateBridgeFees(args);
default:
return createErrorResponse(
`Unknown tool: ${name}`,
'UNKNOWN_TOOL'
);
}
} catch (error) {
return createErrorResponse(
error instanceof Error ? error.message : String(error),
'TOOL_ERROR'
);
}
});
}
private async getSupportedChains(args: any): Promise<MCPToolResult> {
const { includeTestnets = false } = args;
const chains = Object.values(CHAIN_INFO)
.filter(chain => includeTestnets || !chain.isTestnet)
.map(chain => ({
chainId: chain.chainId,
name: chain.name,
symbol: chain.symbol,
decimals: chain.decimals,
logoUrl: chain.logoUrl,
explorerUrl: chain.explorerUrl,
isTestnet: chain.isTestnet,
networkType: chain.networkType,
usdcAddress: chain.usdcAddress,
}));
return createSuccessResponse({
chains,
count: chains.length,
});
}
private async getSupportedTokens(args: any): Promise<MCPToolResult> {
try {
const request = SupportedTokensRequestSchema.parse(args);
const result = await this.quoteService.getSupportedTokens(request);
return createSuccessResponse(result);
} catch (error) {
return createErrorResponse(
error instanceof Error ? error.message : String(error),
'INVALID_REQUEST'
);
}
}
private async getBridgeQuote(args: any): Promise<MCPToolResult> {
try {
const request = QuoteRequestSchema.parse(args);
const result = await this.quoteService.getQuote(request);
return createSuccessResponse(result);
} catch (error) {
return createErrorResponse(
error instanceof Error ? error.message : String(error),
'QUOTE_ERROR'
);
}
}
private async executeBridge(args: any): Promise<MCPToolResult> {
try {
const request = BridgeRequestSchema.parse(args);
const result = await this.bridgeService.prepareBridgeTransaction(request);
// Add a warning that this is simulation only
return createSuccessResponse({
...result,
warning: 'This is a simulation only. To execute the transaction, use the returned transaction data with your wallet.',
simulationOnly: true,
});
} catch (error) {
return createErrorResponse(
error instanceof Error ? error.message : String(error),
'BRIDGE_ERROR'
);
}
}
private async getTransactionStatus(args: any): Promise<MCPToolResult> {
try {
const { transactionHash, fromChainId, toChainId } = args;
const result = await this.statusService.getTransactionStatus(
transactionHash,
fromChainId,
toChainId
);
return createSuccessResponse(result);
} catch (error) {
return createErrorResponse(
error instanceof Error ? error.message : String(error),
'STATUS_ERROR'
);
}
}
private async getUserBalance(args: any): Promise<MCPToolResult> {
try {
const request = BalanceQuerySchema.parse(args);
const result = await this.balanceService.getBalance(request);
return createSuccessResponse(result);
} catch (error) {
return createErrorResponse(
error instanceof Error ? error.message : String(error),
'BALANCE_ERROR'
);
}
}
private async estimateBridgeFees(args: any): Promise<MCPToolResult> {
try {
const { fromChainId, toChainId, amount, tokenAddress } = args;
const result = await this.quoteService.estimateFees(
fromChainId,
toChainId,
amount,
tokenAddress
);
return createSuccessResponse(result);
} catch (error) {
return createErrorResponse(
error instanceof Error ? error.message : String(error),
'FEE_ESTIMATION_ERROR'
);
}
}
async run() {
const transport = new StdioServerTransport();
await this.server.connect(transport);
}
}
// Start the server
const server = new ValueRouterMCPServer();
server.run().catch((error) => {
console.error('Server error:', error);
process.exit(1);
});