EDUCHAIN Agent Kit
- build
#!/usr/bin/env node
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import { CallToolRequestSchema, ErrorCode, ListResourcesRequestSchema, ListResourceTemplatesRequestSchema, ListToolsRequestSchema, McpError, ReadResourceRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
import { ethers } from 'ethers';
import * as subgraph from './subgraph.js';
import * as blockchain from './blockchain.js';
import * as swap from './swap.js';
import * as external_market from './external_market.js';
class SailFishSubgraphServer {
constructor() {
this.server = new Server({
name: 'educhain-agent-kit',
version: '1.0.0',
}, {
capabilities: {
resources: {},
tools: {},
},
});
this.setupResourceHandlers();
this.setupToolHandlers();
// Error handling
this.server.onerror = (error) => console.error('[MCP Error]', error);
process.on('SIGINT', () => __awaiter(this, void 0, void 0, function* () {
yield this.server.close();
process.exit(0);
}));
}
setupResourceHandlers() {
// List available resources
this.server.setRequestHandler(ListResourcesRequestSchema, () => __awaiter(this, void 0, void 0, function* () {
return ({
resources: [
{
uri: 'sailfish://overview',
name: 'SailFish DEX Overview',
mimeType: 'application/json',
description: 'Overview of SailFish DEX including TVL, volume, and other metrics',
},
],
});
}));
// List resource templates
this.server.setRequestHandler(ListResourceTemplatesRequestSchema, () => __awaiter(this, void 0, void 0, function* () {
return ({
resourceTemplates: [
{
uriTemplate: 'sailfish://token/{tokenId}',
name: 'Token Information',
mimeType: 'application/json',
description: 'Information about a specific token on SailFish DEX',
},
{
uriTemplate: 'sailfish://pool/{poolId}',
name: 'Pool Information',
mimeType: 'application/json',
description: 'Information about a specific liquidity pool on SailFish DEX',
},
],
});
}));
// Handle resource reading
this.server.setRequestHandler(ReadResourceRequestSchema, (request) => __awaiter(this, void 0, void 0, function* () {
try {
const { uri } = request.params;
let content = '';
// Handle overview resource
if (uri === 'sailfish://overview') {
const factory = yield subgraph.getFactory();
const ethPrice = yield subgraph.getEthPrice();
content = JSON.stringify({
totalValueLockedUSD: (factory === null || factory === void 0 ? void 0 : factory.totalValueLockedUSD) || '0',
totalVolumeUSD: (factory === null || factory === void 0 ? void 0 : factory.totalVolumeUSD) || '0',
txCount: (factory === null || factory === void 0 ? void 0 : factory.txCount) || '0',
poolCount: (factory === null || factory === void 0 ? void 0 : factory.poolCount) || '0',
ethPriceUSD: ethPrice || '0',
timestamp: new Date().toISOString(),
}, null, 2);
}
// Handle token resource
else if (uri.startsWith('sailfish://token/')) {
const tokenId = uri.replace('sailfish://token/', '');
const token = yield subgraph.getToken(tokenId);
if (!token) {
throw new McpError(ErrorCode.InvalidRequest, `Token with ID ${tokenId} not found`);
}
const tokenPrice = yield subgraph.getTokenPrice(tokenId);
content = JSON.stringify(Object.assign(Object.assign({}, token), { priceUSD: tokenPrice || 'Unknown', timestamp: new Date().toISOString() }), null, 2);
}
// Handle pool resource
else if (uri.startsWith('sailfish://pool/')) {
const poolId = uri.replace('sailfish://pool/', '');
const pool = yield subgraph.getPool(poolId);
if (!pool) {
throw new McpError(ErrorCode.InvalidRequest, `Pool with ID ${poolId} not found`);
}
content = JSON.stringify(Object.assign(Object.assign({}, pool), { timestamp: new Date().toISOString() }), null, 2);
}
else {
throw new McpError(ErrorCode.InvalidRequest, `Invalid URI format: ${uri}`);
}
return {
contents: [
{
uri: request.params.uri,
mimeType: 'application/json',
text: content,
},
],
};
}
catch (error) {
if (error instanceof McpError) {
throw error;
}
console.error('Error handling resource request:', error);
throw new McpError(ErrorCode.InternalError, `Failed to fetch data: ${error.message}`);
}
}));
}
setupToolHandlers() {
// List available tools
this.server.setRequestHandler(ListToolsRequestSchema, () => __awaiter(this, void 0, void 0, function* () {
return ({
tools: [
{
name: 'get_token_price',
description: 'Get the current price of a token on SailFish DEX',
inputSchema: {
type: 'object',
properties: {
tokenId: {
type: 'string',
description: 'Token address',
},
},
required: ['tokenId'],
},
},
{
name: 'get_token_info',
description: 'Get detailed information about a token on SailFish DEX',
inputSchema: {
type: 'object',
properties: {
tokenId: {
type: 'string',
description: 'Token address',
},
},
required: ['tokenId'],
},
},
{
name: 'get_pool_info',
description: 'Get detailed information about a liquidity pool on SailFish DEX',
inputSchema: {
type: 'object',
properties: {
poolId: {
type: 'string',
description: 'Pool address',
},
},
required: ['poolId'],
},
},
{
name: 'get_top_tokens',
description: 'Get a list of top tokens by TVL on SailFish DEX',
inputSchema: {
type: 'object',
properties: {
count: {
type: 'number',
description: 'Number of tokens to return (default: 10)',
},
},
required: [],
},
},
{
name: 'get_top_pools',
description: 'Get a list of top liquidity pools by TVL on SailFish DEX',
inputSchema: {
type: 'object',
properties: {
count: {
type: 'number',
description: 'Number of pools to return (default: 10)',
},
},
required: [],
},
},
{
name: 'get_total_tvl',
description: 'Get the total value locked (TVL) in SailFish DEX',
inputSchema: {
type: 'object',
properties: {},
required: [],
},
},
{
name: 'get_24h_volume',
description: 'Get the 24-hour trading volume on SailFish DEX',
inputSchema: {
type: 'object',
properties: {},
required: [],
},
},
{
name: 'get_token_historical_data',
description: 'Get historical data for a token on SailFish DEX',
inputSchema: {
type: 'object',
properties: {
tokenId: {
type: 'string',
description: 'Token address',
},
days: {
type: 'number',
description: 'Number of days of data to return (default: 7)',
},
},
required: ['tokenId'],
},
},
{
name: 'get_pool_historical_data',
description: 'Get historical data for a liquidity pool on SailFish DEX',
inputSchema: {
type: 'object',
properties: {
poolId: {
type: 'string',
description: 'Pool address',
},
days: {
type: 'number',
description: 'Number of days of data to return (default: 7)',
},
},
required: ['poolId'],
},
},
{
name: 'get_edu_balance',
description: 'Get the EDU balance of a wallet address',
inputSchema: {
type: 'object',
properties: {
walletAddress: {
type: 'string',
description: 'Wallet address to check',
},
},
required: ['walletAddress'],
},
},
{
name: 'get_token_balance',
description: 'Get the token balance of a wallet address with USD value using SailFish as price oracle',
inputSchema: {
type: 'object',
properties: {
tokenAddress: {
type: 'string',
description: 'Token contract address',
},
walletAddress: {
type: 'string',
description: 'Wallet address to check',
},
},
required: ['tokenAddress', 'walletAddress'],
},
},
{
name: 'get_multiple_token_balances',
description: 'Get multiple token balances for a wallet address with USD values using SailFish as price oracle',
inputSchema: {
type: 'object',
properties: {
tokenAddresses: {
type: 'array',
items: {
type: 'string',
},
description: 'List of token contract addresses',
},
walletAddress: {
type: 'string',
description: 'Wallet address to check',
},
},
required: ['tokenAddresses', 'walletAddress'],
},
},
{
name: 'get_nft_balance',
description: 'Get the NFT balance of a wallet address for a specific NFT collection',
inputSchema: {
type: 'object',
properties: {
nftAddress: {
type: 'string',
description: 'NFT contract address',
},
walletAddress: {
type: 'string',
description: 'Wallet address to check',
},
fetchTokenIds: {
type: 'boolean',
description: 'Whether to fetch token IDs (default: true)',
},
},
required: ['nftAddress', 'walletAddress'],
},
},
{
name: 'get_wallet_overview',
description: 'Get an overview of a wallet including EDU, tokens, and NFTs',
inputSchema: {
type: 'object',
properties: {
walletAddress: {
type: 'string',
description: 'Wallet address to check',
},
tokenAddresses: {
type: 'array',
items: {
type: 'string',
},
description: 'List of token contract addresses to check',
},
nftAddresses: {
type: 'array',
items: {
type: 'string',
},
description: 'List of NFT contract addresses to check',
},
},
required: ['walletAddress'],
},
},
{
name: 'set_rpc_url',
description: 'Set the RPC URL for blockchain interactions',
inputSchema: {
type: 'object',
properties: {
url: {
type: 'string',
description: 'RPC URL to use for blockchain interactions',
},
},
required: ['url'],
},
},
{
name: 'get_rpc_url',
description: 'Get the current RPC URL used for blockchain interactions',
inputSchema: {
type: 'object',
properties: {},
required: [],
},
},
{
name: 'send_edu',
description: 'Send EDU native token to another wallet address',
inputSchema: {
type: 'object',
properties: {
privateKey: {
type: 'string',
description: 'Private key of the sender wallet',
},
toAddress: {
type: 'string',
description: 'Recipient wallet address',
},
amount: {
type: 'string',
description: 'Amount of EDU to send',
},
},
required: ['privateKey', 'toAddress', 'amount'],
},
},
{
name: 'get_wallet_address_from_private_key',
description: 'Get wallet address from private key with proper checksum formatting',
inputSchema: {
type: 'object',
properties: {
privateKey: {
type: 'string',
description: 'Private key of the wallet',
},
},
required: ['privateKey'],
},
},
{
name: 'send_erc20_token',
description: 'Send ERC20 token to another wallet address',
inputSchema: {
type: 'object',
properties: {
privateKey: {
type: 'string',
description: 'Private key of the sender wallet',
},
tokenAddress: {
type: 'string',
description: 'Token contract address',
},
toAddress: {
type: 'string',
description: 'Recipient wallet address',
},
amount: {
type: 'string',
description: 'Amount of tokens to send',
},
confirm: {
type: 'boolean',
description: 'Confirm the transaction after verifying wallet address (default: true)',
},
},
required: ['privateKey', 'tokenAddress', 'toAddress', 'amount'],
},
},
{
name: 'get_swap_quote',
description: 'Get a quote for swapping tokens on SailFish DEX',
inputSchema: {
type: 'object',
properties: {
tokenIn: {
type: 'string',
description: 'Address of the input token',
},
tokenOut: {
type: 'string',
description: 'Address of the output token',
},
amountIn: {
type: 'string',
description: 'Amount of input token to swap',
},
fee: {
type: 'number',
description: 'Fee tier (100=0.01%, 500=0.05%, 3000=0.3%, 10000=1%)',
},
},
required: ['tokenIn', 'tokenOut', 'amountIn'],
},
},
{
name: 'swap_tokens',
description: 'Swap tokens on SailFish DEX (token to token)',
inputSchema: {
type: 'object',
properties: {
privateKey: {
type: 'string',
description: 'Private key of the sender wallet',
},
tokenIn: {
type: 'string',
description: 'Address of the input token',
},
tokenOut: {
type: 'string',
description: 'Address of the output token',
},
amountIn: {
type: 'string',
description: 'Amount of input token to swap',
},
slippagePercentage: {
type: 'number',
description: 'Slippage tolerance percentage (default: 0.5)',
},
fee: {
type: 'number',
description: 'Fee tier (100=0.01%, 500=0.05%, 3000=0.3%, 10000=1%)',
},
},
required: ['privateKey', 'tokenIn', 'tokenOut', 'amountIn'],
},
},
{
name: 'swap_edu_for_tokens',
description: 'Swap EDU for tokens on SailFish DEX',
inputSchema: {
type: 'object',
properties: {
privateKey: {
type: 'string',
description: 'Private key of the sender wallet',
},
tokenOut: {
type: 'string',
description: 'Address of the output token',
},
amountIn: {
type: 'string',
description: 'Amount of EDU to swap',
},
slippagePercentage: {
type: 'number',
description: 'Slippage tolerance percentage (default: 0.5)',
},
fee: {
type: 'number',
description: 'Fee tier (100=0.01%, 500=0.05%, 3000=0.3%, 10000=1%)',
},
},
required: ['privateKey', 'tokenOut', 'amountIn'],
},
},
{
name: 'swap_tokens_for_edu',
description: 'Swap tokens for EDU on SailFish DEX',
inputSchema: {
type: 'object',
properties: {
privateKey: {
type: 'string',
description: 'Private key of the sender wallet',
},
tokenIn: {
type: 'string',
description: 'Address of the input token',
},
amountIn: {
type: 'string',
description: 'Amount of tokens to swap',
},
slippagePercentage: {
type: 'number',
description: 'Slippage tolerance percentage (default: 0.5)',
},
fee: {
type: 'number',
description: 'Fee tier (100=0.01%, 500=0.05%, 3000=0.3%, 10000=1%)',
},
},
required: ['privateKey', 'tokenIn', 'amountIn'],
},
},
{
name: 'get_external_market_data',
description: 'Get external market data for EDU from centralized exchanges',
inputSchema: {
type: 'object',
properties: {},
required: [],
},
},
{
name: 'check_arbitrage_opportunities',
description: 'Check for arbitrage opportunities between centralized exchanges and SailFish DEX',
inputSchema: {
type: 'object',
properties: {
threshold: {
type: 'number',
description: 'Minimum price difference percentage to consider as an arbitrage opportunity (default: 1.0)',
},
},
required: [],
},
},
{
name: 'update_external_market_config',
description: 'Update the configuration for external market data API',
inputSchema: {
type: 'object',
properties: {
apiUrl: {
type: 'string',
description: 'API URL for external market data',
},
apiKey: {
type: 'string',
description: 'API key for external market data (if required)',
},
symbols: {
type: 'object',
properties: {
EDU: {
type: 'string',
description: 'Symbol for EDU token on the external API',
},
USD: {
type: 'string',
description: 'Symbol for USD on the external API',
},
},
description: 'Symbol mappings for the external API',
},
},
required: [],
},
},
{
name: 'get_external_market_config',
description: 'Get the current configuration for external market data API',
inputSchema: {
type: 'object',
properties: {},
required: [],
},
},
{
name: 'wrap_edu',
description: 'Wrap EDU to WEDU (Wrapped EDU)',
inputSchema: {
type: 'object',
properties: {
privateKey: {
type: 'string',
description: 'Private key of the wallet',
},
amount: {
type: 'string',
description: 'Amount of EDU to wrap',
},
},
required: ['privateKey', 'amount'],
},
},
{
name: 'unwrap_wedu',
description: 'Unwrap WEDU (Wrapped EDU) to EDU',
inputSchema: {
type: 'object',
properties: {
privateKey: {
type: 'string',
description: 'Private key of the wallet',
},
amount: {
type: 'string',
description: 'Amount of WEDU to unwrap',
},
},
required: ['privateKey', 'amount'],
},
},
],
});
}));
// Handle tool calls
this.server.setRequestHandler(CallToolRequestSchema, (request) => __awaiter(this, void 0, void 0, function* () {
try {
const { name, arguments: args = {} } = request.params;
switch (name) {
case 'get_token_price': {
if (!args.tokenId || typeof args.tokenId !== 'string') {
throw new McpError(ErrorCode.InvalidParams, 'Token ID is required');
}
const price = yield subgraph.getTokenPrice(args.tokenId);
return {
content: [
{
type: 'text',
text: JSON.stringify({ price: price || 'Unknown' }, null, 2),
},
],
};
}
case 'get_token_info': {
if (!args.tokenId || typeof args.tokenId !== 'string') {
throw new McpError(ErrorCode.InvalidParams, 'Token ID is required');
}
const token = yield subgraph.getToken(args.tokenId);
if (!token) {
throw new McpError(ErrorCode.InvalidRequest, `Token with ID ${args.tokenId} not found`);
}
const price = yield subgraph.getTokenPrice(args.tokenId);
return {
content: [
{
type: 'text',
text: JSON.stringify(Object.assign(Object.assign({}, token), { priceUSD: price || 'Unknown' }), null, 2),
},
],
};
}
case 'get_pool_info': {
if (!args.poolId || typeof args.poolId !== 'string') {
throw new McpError(ErrorCode.InvalidParams, 'Pool ID is required');
}
const pool = yield subgraph.getPool(args.poolId);
if (!pool) {
throw new McpError(ErrorCode.InvalidRequest, `Pool with ID ${args.poolId} not found`);
}
return {
content: [
{
type: 'text',
text: JSON.stringify(pool, null, 2),
},
],
};
}
case 'get_top_tokens': {
const count = typeof args.count === 'number' ? args.count : 10;
const tokens = yield subgraph.getTopTokens(count);
return {
content: [
{
type: 'text',
text: JSON.stringify(tokens, null, 2),
},
],
};
}
case 'get_top_pools': {
const count = typeof args.count === 'number' ? args.count : 10;
const pools = yield subgraph.getTopPools(count);
return {
content: [
{
type: 'text',
text: JSON.stringify(pools, null, 2),
},
],
};
}
case 'get_total_tvl': {
const tvl = yield subgraph.getTotalTVL();
return {
content: [
{
type: 'text',
text: JSON.stringify({ totalValueLockedUSD: tvl }, null, 2),
},
],
};
}
case 'get_24h_volume': {
const volume = yield subgraph.get24HVolume();
return {
content: [
{
type: 'text',
text: JSON.stringify({ volumeUSD: volume }, null, 2),
},
],
};
}
case 'get_token_historical_data': {
if (!args.tokenId || typeof args.tokenId !== 'string') {
throw new McpError(ErrorCode.InvalidParams, 'Token ID is required');
}
const days = typeof args.days === 'number' ? args.days : 7;
const data = yield subgraph.getTokenDayData(args.tokenId, days);
return {
content: [
{
type: 'text',
text: JSON.stringify(data, null, 2),
},
],
};
}
case 'get_pool_historical_data': {
if (!args.poolId || typeof args.poolId !== 'string') {
throw new McpError(ErrorCode.InvalidParams, 'Pool ID is required');
}
const days = typeof args.days === 'number' ? args.days : 7;
const data = yield subgraph.getPoolDayData(args.poolId, days);
return {
content: [
{
type: 'text',
text: JSON.stringify(data, null, 2),
},
],
};
}
case 'get_edu_balance': {
if (!args.walletAddress || typeof args.walletAddress !== 'string') {
throw new McpError(ErrorCode.InvalidParams, 'Wallet address is required');
}
const balance = yield blockchain.getEduBalance(args.walletAddress);
return {
content: [
{
type: 'text',
text: JSON.stringify(balance, null, 2),
},
],
};
}
case 'get_token_balance': {
if (!args.tokenAddress || typeof args.tokenAddress !== 'string') {
throw new McpError(ErrorCode.InvalidParams, 'Token address is required');
}
if (!args.walletAddress || typeof args.walletAddress !== 'string') {
throw new McpError(ErrorCode.InvalidParams, 'Wallet address is required');
}
const balance = yield blockchain.getTokenBalance(args.tokenAddress, args.walletAddress);
return {
content: [
{
type: 'text',
text: JSON.stringify(balance, null, 2),
},
],
};
}
case 'get_multiple_token_balances': {
if (!args.tokenAddresses || !Array.isArray(args.tokenAddresses)) {
throw new McpError(ErrorCode.InvalidParams, 'Token addresses array is required');
}
if (!args.walletAddress || typeof args.walletAddress !== 'string') {
throw new McpError(ErrorCode.InvalidParams, 'Wallet address is required');
}
const balances = yield blockchain.getMultipleTokenBalances(args.tokenAddresses, args.walletAddress);
return {
content: [
{
type: 'text',
text: JSON.stringify(balances, null, 2),
},
],
};
}
case 'get_nft_balance': {
if (!args.nftAddress || typeof args.nftAddress !== 'string') {
throw new McpError(ErrorCode.InvalidParams, 'NFT address is required');
}
if (!args.walletAddress || typeof args.walletAddress !== 'string') {
throw new McpError(ErrorCode.InvalidParams, 'Wallet address is required');
}
const fetchTokenIds = args.fetchTokenIds !== false;
const balance = yield blockchain.getERC721Balance(args.nftAddress, args.walletAddress, fetchTokenIds);
return {
content: [
{
type: 'text',
text: JSON.stringify(balance, null, 2),
},
],
};
}
case 'get_wallet_overview': {
if (!args.walletAddress || typeof args.walletAddress !== 'string') {
throw new McpError(ErrorCode.InvalidParams, 'Wallet address is required');
}
const tokenAddresses = Array.isArray(args.tokenAddresses) ? args.tokenAddresses : [];
const nftAddresses = Array.isArray(args.nftAddresses) ? args.nftAddresses : [];
const overview = yield blockchain.getWalletOverview(args.walletAddress, tokenAddresses, nftAddresses);
return {
content: [
{
type: 'text',
text: JSON.stringify(overview, null, 2),
},
],
};
}
case 'set_rpc_url': {
if (!args.url || typeof args.url !== 'string') {
throw new McpError(ErrorCode.InvalidParams, 'RPC URL is required');
}
blockchain.setRpcUrl(args.url);
return {
content: [
{
type: 'text',
text: JSON.stringify({ success: true, rpcUrl: args.url }, null, 2),
},
],
};
}
case 'get_rpc_url': {
const rpcUrl = blockchain.getRpcUrl();
return {
content: [
{
type: 'text',
text: JSON.stringify({ rpcUrl }, null, 2),
},
],
};
}
case 'get_wallet_address_from_private_key': {
if (!args.privateKey || typeof args.privateKey !== 'string') {
throw new McpError(ErrorCode.InvalidParams, 'Private key is required');
}
const walletAddress = blockchain.getWalletAddressFromPrivateKey(args.privateKey);
return {
content: [
{
type: 'text',
text: JSON.stringify({ walletAddress }, null, 2),
},
],
};
}
case 'send_edu': {
if (!args.privateKey || typeof args.privateKey !== 'string') {
throw new McpError(ErrorCode.InvalidParams, 'Private key is required');
}
if (!args.toAddress || typeof args.toAddress !== 'string') {
throw new McpError(ErrorCode.InvalidParams, 'Recipient address is required');
}
if (!args.amount || typeof args.amount !== 'string') {
throw new McpError(ErrorCode.InvalidParams, 'Amount is required');
}
// Get wallet address from private key for information
const fromAddress = blockchain.getWalletAddressFromPrivateKey(args.privateKey);
// Proceed with the transaction
const result = yield blockchain.sendEdu(args.privateKey, args.toAddress, args.amount);
// Add from address to the result for better context
const enhancedResult = Object.assign(Object.assign({}, result), { fromAddress });
return {
content: [
{
type: 'text',
text: JSON.stringify(enhancedResult, null, 2),
},
],
};
}
case 'send_erc20_token': {
if (!args.privateKey || typeof args.privateKey !== 'string') {
throw new McpError(ErrorCode.InvalidParams, 'Private key is required');
}
if (!args.tokenAddress || typeof args.tokenAddress !== 'string') {
throw new McpError(ErrorCode.InvalidParams, 'Token address is required');
}
if (!args.toAddress || typeof args.toAddress !== 'string') {
throw new McpError(ErrorCode.InvalidParams, 'Recipient address is required');
}
if (!args.amount || typeof args.amount !== 'string') {
throw new McpError(ErrorCode.InvalidParams, 'Amount is required');
}
// Get wallet address from private key for information
const fromAddress = blockchain.getWalletAddressFromPrivateKey(args.privateKey);
// Try to get token details for better context
let tokenSymbol = 'Unknown';
try {
const provider = blockchain.getProvider();
const tokenContract = new ethers.Contract(args.tokenAddress, ['function symbol() view returns (string)'], provider);
tokenSymbol = yield tokenContract.symbol();
}
catch (error) {
console.error('Error fetching token symbol:', error);
}
// Proceed with the transaction
const result = yield blockchain.sendErc20Token(args.privateKey, args.tokenAddress, args.toAddress, args.amount);
// Add from address and token symbol to the result for better context
const enhancedResult = Object.assign(Object.assign({}, result), { fromAddress,
tokenSymbol });
return {
content: [
{
type: 'text',
text: JSON.stringify(enhancedResult, null, 2),
},
],
};
}
case 'get_swap_quote': {
if (!args.tokenIn || typeof args.tokenIn !== 'string') {
throw new McpError(ErrorCode.InvalidParams, 'Input token address is required');
}
if (!args.tokenOut || typeof args.tokenOut !== 'string') {
throw new McpError(ErrorCode.InvalidParams, 'Output token address is required');
}
if (!args.amountIn || typeof args.amountIn !== 'string') {
throw new McpError(ErrorCode.InvalidParams, 'Input amount is required');
}
const slippagePercentage = typeof args.slippagePercentage === 'number' ? args.slippagePercentage : 0.5;
const quote = yield swap.getSwapQuote(args.tokenIn, args.tokenOut, args.amountIn, slippagePercentage);
return {
content: [
{
type: 'text',
text: JSON.stringify({
inputToken: {
address: args.tokenIn,
symbol: quote.tokenInSymbol,
decimals: quote.tokenInDecimals,
amount: args.amountIn,
rawAmount: ethers.parseUnits(args.amountIn, quote.tokenInDecimals).toString()
},
outputToken: {
address: args.tokenOut,
symbol: quote.tokenOutSymbol,
decimals: quote.tokenOutDecimals,
amount: quote.formattedAmountOut,
minimumAmount: quote.formattedMinimumAmountOut,
rawAmount: quote.amountOut,
rawMinimumAmount: quote.minimumAmountOut
},
exchangeRate: (Number(quote.formattedAmountOut) / Number(args.amountIn)).toString(),
priceImpact: quote.priceImpact.toFixed(2),
routeType: quote.route.type,
slippage: slippagePercentage.toString(),
note: "Amounts are formatted using the token's decimal places. Raw amounts are in wei units."
}, null, 2),
},
],
};
}
case 'swap_tokens': {
if (!args.privateKey || typeof args.privateKey !== 'string') {
throw new McpError(ErrorCode.InvalidParams, 'Private key is required');
}
if (!args.tokenIn || typeof args.tokenIn !== 'string') {
throw new McpError(ErrorCode.InvalidParams, 'Input token address is required');
}
if (!args.tokenOut || typeof args.tokenOut !== 'string') {
throw new McpError(ErrorCode.InvalidParams, 'Output token address is required');
}
if (!args.amountIn || typeof args.amountIn !== 'string') {
throw new McpError(ErrorCode.InvalidParams, 'Input amount is required');
}
const slippagePercentage = typeof args.slippagePercentage === 'number' ? args.slippagePercentage : 0.5;
const fee = typeof args.fee === 'number' ? args.fee : 3000; // Default to 0.3%
const result = yield swap.swapExactTokensForTokens(args.privateKey, args.tokenIn, args.tokenOut, args.amountIn, slippagePercentage);
return {
content: [
{
type: 'text',
text: JSON.stringify(result, null, 2),
},
],
};
}
case 'swap_edu_for_tokens': {
if (!args.privateKey || typeof args.privateKey !== 'string') {
throw new McpError(ErrorCode.InvalidParams, 'Private key is required');
}
if (!args.tokenOut || typeof args.tokenOut !== 'string') {
throw new McpError(ErrorCode.InvalidParams, 'Output token address is required');
}
if (!args.amountIn || typeof args.amountIn !== 'string') {
throw new McpError(ErrorCode.InvalidParams, 'Input amount is required');
}
const slippagePercentage = typeof args.slippagePercentage === 'number' ? args.slippagePercentage : 0.5;
const fee = typeof args.fee === 'number' ? args.fee : 3000; // Default to 0.3%
const result = yield swap.swapExactEDUForTokens(args.privateKey, args.tokenOut, args.amountIn, slippagePercentage);
return {
content: [
{
type: 'text',
text: JSON.stringify(result, null, 2),
},
],
};
}
case 'swap_tokens_for_edu': {
if (!args.privateKey || typeof args.privateKey !== 'string') {
throw new McpError(ErrorCode.InvalidParams, 'Private key is required');
}
if (!args.tokenIn || typeof args.tokenIn !== 'string') {
throw new McpError(ErrorCode.InvalidParams, 'Input token address is required');
}
if (!args.amountIn || typeof args.amountIn !== 'string') {
throw new McpError(ErrorCode.InvalidParams, 'Input amount is required');
}
const slippagePercentage = typeof args.slippagePercentage === 'number' ? args.slippagePercentage : 0.5;
const fee = typeof args.fee === 'number' ? args.fee : 3000; // Default to 0.3%
const result = yield swap.swapExactTokensForEDU(args.privateKey, args.tokenIn, args.amountIn, slippagePercentage);
return {
content: [
{
type: 'text',
text: JSON.stringify(result, null, 2),
},
],
};
}
case 'get_external_market_data': {
try {
const data = yield external_market.getExternalMarketData();
return {
content: [
{
type: 'text',
text: JSON.stringify(data, null, 2),
},
],
};
}
catch (error) {
console.error('Error getting external market data:', error);
return {
content: [
{
type: 'text',
text: JSON.stringify({
error: 'Failed to fetch external market data',
message: error.message,
note: 'You may need to update the external market API configuration'
}, null, 2),
},
],
isError: true,
};
}
}
case 'check_arbitrage_opportunities': {
try {
const threshold = typeof args.threshold === 'number' ? args.threshold : 1.0;
const opportunities = yield external_market.checkArbitrageOpportunities(threshold);
return {
content: [
{
type: 'text',
text: JSON.stringify(opportunities, null, 2),
},
],
};
}
catch (error) {
console.error('Error checking arbitrage opportunities:', error);
return {
content: [
{
type: 'text',
text: JSON.stringify({
error: 'Failed to check arbitrage opportunities',
message: error.message,
note: 'You may need to update the external market API configuration'
}, null, 2),
},
],
isError: true,
};
}
}
case 'update_external_market_config': {
try {
const newConfig = {};
if (typeof args.apiUrl === 'string') {
newConfig.apiUrl = args.apiUrl;
}
if (typeof args.apiKey === 'string') {
newConfig.apiKey = args.apiKey;
}
if (typeof args.symbols === 'object' && args.symbols !== null) {
newConfig.symbols = {};
if (typeof args.symbols.EDU === 'string') {
newConfig.symbols.EDU = args.symbols.EDU;
}
if (typeof args.symbols.USD === 'string') {
newConfig.symbols.USD = args.symbols.USD;
}
}
external_market.updateConfig(newConfig);
const currentConfig = external_market.getConfig();
return {
content: [
{
type: 'text',
text: JSON.stringify({
success: true,
message: 'External market API configuration updated',
config: currentConfig
}, null, 2),
},
],
};
}
catch (error) {
console.error('Error updating external market config:', error);
return {
content: [
{
type: 'text',
text: JSON.stringify({
error: 'Failed to update external market API configuration',
message: error.message
}, null, 2),
},
],
isError: true,
};
}
}
case 'get_external_market_config': {
try {
const config = external_market.getConfig();
return {
content: [
{
type: 'text',
text: JSON.stringify(config, null, 2),
},
],
};
}
catch (error) {
console.error('Error getting external market config:', error);
return {
content: [
{
type: 'text',
text: JSON.stringify({
error: 'Failed to get external market API configuration',
message: error.message
}, null, 2),
},
],
isError: true,
};
}
}
case 'wrap_edu': {
if (!args.privateKey || typeof args.privateKey !== 'string') {
throw new McpError(ErrorCode.InvalidParams, 'Private key is required');
}
if (!args.amount || typeof args.amount !== 'string') {
throw new McpError(ErrorCode.InvalidParams, 'Amount is required');
}
try {
const result = yield swap.wrapEDU(args.privateKey, args.amount);
return {
content: [
{
type: 'text',
text: JSON.stringify(Object.assign(Object.assign({}, result), { message: `Successfully wrapped ${args.amount} EDU to WEDU`, note: "WEDU (Wrapped EDU) is required for interacting with SailFish DEX. You can unwrap it back to EDU at any time." }), null, 2),
},
],
};
}
catch (error) {
console.error('Error wrapping EDU to WEDU:', error);
return {
content: [
{
type: 'text',
text: JSON.stringify({
error: 'Failed to wrap EDU to WEDU',
message: error.message
}, null, 2),
},
],
isError: true,
};
}
}
case 'unwrap_wedu': {
if (!args.privateKey || typeof args.privateKey !== 'string') {
throw new McpError(ErrorCode.InvalidParams, 'Private key is required');
}
if (!args.amount || typeof args.amount !== 'string') {
throw new McpError(ErrorCode.InvalidParams, 'Amount is required');
}
try {
const result = yield swap.unwrapWEDU(args.privateKey, args.amount);
return {
content: [
{
type: 'text',
text: JSON.stringify(Object.assign(Object.assign({}, result), { message: `Successfully unwrapped ${args.amount} WEDU to EDU` }), null, 2),
},
],
};
}
catch (error) {
console.error('Error unwrapping WEDU to EDU:', error);
return {
content: [
{
type: 'text',
text: JSON.stringify({
error: 'Failed to unwrap WEDU to EDU',
message: error.message
}, null, 2),
},
],
isError: true,
};
}
}
default:
throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${name}`);
}
}
catch (error) {
if (error instanceof McpError) {
throw error;
}
console.error('Error handling tool call:', error);
return {
content: [
{
type: 'text',
text: `Error: ${error.message}`,
},
],
isError: true,
};
}
}));
}
run() {
return __awaiter(this, void 0, void 0, function* () {
const transport = new StdioServerTransport();
yield this.server.connect(transport);
console.error('EDUCHAIN Agent Kit running on stdio');
});
}
}
const server = new SailFishSubgraphServer();
server.run().catch(console.error);
//# sourceMappingURL=index.js.map