Skip to main content
Glama

Morpho API MCP Server

index.ts47.2 kB
#!/usr/bin/env node import { Server } from "@modelcontextprotocol/sdk/server/index.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js"; import axios from 'axios'; import { z } from 'zod'; // Define the base URL for the Morpho API const MORPHO_API_BASE = 'https://blue-api.morpho.org/graphql'; // Helper function to transform string numbers to numbers const stringToNumber = (val: string | number | null): number => { if (val === null) return 0; return typeof val === 'string' ? Number(val) : val; }; // Define Zod schemas for data validation // Asset Schema const AssetSchema = z.object({ address: z.string(), symbol: z.string(), decimals: z.number(), }).nullable().transform(val => val || { address: '', symbol: '', decimals: 0 }); // Chain Schema const ChainSchema = z.object({ id: z.number(), network: z.string(), currency: z.string() }); // Yield Schema const YieldSchema = z.object({ apr: z.number().nullable() }); // Asset with Price Schema const AssetWithPriceSchema = z.object({ symbol: z.string(), address: z.string(), priceUsd: z.number().nullable(), chain: ChainSchema, yield: YieldSchema.nullable() }); // Market Position Schema const MarketPositionSchema = z.object({ supplyShares: z.union([z.string(), z.number()]).transform(stringToNumber).optional(), supplyAssets: z.union([z.string(), z.number()]).transform(stringToNumber), supplyAssetsUsd: z.union([z.string(), z.number()]).transform(stringToNumber), borrowShares: z.union([z.string(), z.number()]).transform(stringToNumber).optional(), borrowAssets: z.union([z.string(), z.number()]).transform(stringToNumber), borrowAssetsUsd: z.union([z.string(), z.number()]).transform(stringToNumber), collateral: z.union([z.string(), z.number()]).transform(stringToNumber).optional(), collateralUsd: z.union([z.string(), z.number()]).transform(stringToNumber).optional(), market: z.object({ uniqueKey: z.string(), loanAsset: AssetSchema, collateralAsset: AssetSchema, }), user: z.object({ address: z.string() }) }); // Market Schema const MarketSchema = z.object({ uniqueKey: z.string(), lltv: z.union([z.string(), z.number()]).transform(stringToNumber), oracleAddress: z.string(), irmAddress: z.string(), whitelisted: z.boolean().optional(), loanAsset: AssetSchema, collateralAsset: AssetSchema, state: z.object({ borrowApy: z.number(), borrowAssets: z.union([z.string(), z.number()]).transform(stringToNumber), borrowAssetsUsd: z.number().nullable().transform(val => val ?? 0), supplyApy: z.number(), supplyAssets: z.union([z.string(), z.number()]).transform(stringToNumber), supplyAssetsUsd: z.number().nullable().transform(val => val ?? 0), fee: z.number(), utilization: z.number(), }), }); // Combined items schema const MarketsItemsSchema = z.object({ items: z.array(MarketSchema), }); // Assets Response Schema const AssetsResponseSchema = z.object({ data: z.object({ assets: z.object({ pageInfo: z.object({ count: z.number(), countTotal: z.number(), }), items: z.array(AssetWithPriceSchema) }) }) }); // Market Positions Response Schema const MarketPositionsResponseSchema = z.object({ data: z.object({ marketPositions: z.object({ pageInfo: z.object({ count: z.number(), countTotal: z.number(), }), items: z.array(MarketPositionSchema) }) }) }); // Morpho API Response Schema const MorphoApiResponseSchema = z.object({ data: z.object({ markets: MarketsItemsSchema }) }); // Define tool names as constants to avoid typos const GET_MARKETS_TOOL = 'get_markets'; const GET_WHITELISTED_MARKETS_TOOL = 'get_whitelisted_markets'; const GET_ASSET_PRICE_TOOL = 'get_asset_price'; const GET_MARKET_POSITIONS_TOOL = 'get_market_positions'; // Define parameter types for tools type PaginationParams = { first?: number; skip?: number; }; type OrderDirection = 'Asc' | 'Desc'; type MarketOrderField = | 'Lltv' | 'BorrowApy' | 'SupplyApy' | 'BorrowAssets' | 'SupplyAssets' | 'BorrowAssetsUsd' | 'SupplyAssetsUsd' | 'Fee' | 'Utilization'; type MarketFilterParams = { whitelisted?: boolean; collateralAssetAddress?: string; loanAssetAddress?: string; uniqueKey_in?: string[]; }; type MarketQueryParams = PaginationParams & { orderBy?: MarketOrderField; orderDirection?: OrderDirection; where?: MarketFilterParams; }; type AssetPriceParams = PaginationParams & { symbol: string; chainId?: number; }; type MarketPositionsParams = PaginationParams & { marketUniqueKey: string; orderBy?: 'SupplyShares' | 'BorrowShares' | 'SupplyAssets' | 'BorrowAssets'; orderDirection?: OrderDirection; }; // Add PageInfo schema const PageInfoSchema = z.object({ count: z.number(), countTotal: z.number(), }); // Update response schemas to include pagination info const MarketsResponseSchema = z.object({ data: z.object({ markets: z.object({ pageInfo: PageInfoSchema, items: z.array(MarketSchema), }) }) }); // TimeseriesPoint Schema const TimeseriesPointSchema = z.object({ x: z.number(), y: z.number().nullable(), }); // Oracle Feed Schema const OracleFeedSchema = z.object({ address: z.string(), description: z.string(), vendor: z.string(), pair: z.string(), }); // Oracle Data Schema const OracleDataSchema = z.discriminatedUnion('type', [ z.object({ type: z.literal('MorphoChainlinkOracle'), baseFeedOne: OracleFeedSchema, vault: z.string(), }), z.object({ type: z.literal('MorphoChainlinkOracleV2'), baseFeedOne: OracleFeedSchema, }), ]); // Oracle Schema const OracleSchema = z.object({ address: z.string(), type: z.string(), data: OracleDataSchema, }); // Transaction Schema const TransactionSchema = z.object({ blockNumber: z.number(), hash: z.string(), type: z.string(), timestamp: z.number(), user: z.object({ address: z.string(), }), data: z.object({ seizedAssets: z.union([z.string(), z.number()]).transform(stringToNumber).optional(), repaidAssets: z.union([z.string(), z.number()]).transform(stringToNumber).optional(), seizedAssetsUsd: z.union([z.string(), z.number()]).transform(stringToNumber).optional(), repaidAssetsUsd: z.union([z.string(), z.number()]).transform(stringToNumber).optional(), badDebtAssetsUsd: z.union([z.string(), z.number()]).transform(stringToNumber).optional(), liquidator: z.string().optional(), market: z.object({ uniqueKey: z.string(), }).optional(), }).optional(), }); // Vault Position Schema const VaultPositionSchema = z.object({ vault: z.object({ address: z.string(), name: z.string(), }), assets: z.union([z.string(), z.number()]).transform(stringToNumber), assetsUsd: z.union([z.string(), z.number()]).transform(stringToNumber), shares: z.union([z.string(), z.number()]).transform(stringToNumber), }); // Account Overview Schema const AccountOverviewSchema = z.object({ address: z.string(), marketPositions: z.array(MarketPositionSchema), vaultPositions: z.array(VaultPositionSchema), transactions: z.array(TransactionSchema), }); // Response Schemas const HistoricalApyResponseSchema = z.object({ data: z.object({ marketByUniqueKey: z.object({ uniqueKey: z.string(), historicalState: z.object({ supplyApy: z.array(TimeseriesPointSchema), borrowApy: z.array(TimeseriesPointSchema), }), }), }), }); const OracleDetailsResponseSchema = z.object({ data: z.object({ marketByUniqueKey: z.object({ oracle: OracleSchema, }), }), }); const AccountOverviewResponseSchema = z.object({ data: z.object({ userByAddress: AccountOverviewSchema, }), }); const LiquidationsResponseSchema = z.object({ data: z.object({ transactions: z.object({ pageInfo: PageInfoSchema, items: z.array(TransactionSchema), }), }), }); // Additional tool constants const GET_HISTORICAL_APY_TOOL = 'get_historical_apy'; const GET_ORACLE_DETAILS_TOOL = 'get_oracle_details'; const GET_ACCOUNT_OVERVIEW_TOOL = 'get_account_overview'; const GET_LIQUIDATIONS_TOOL = 'get_liquidations'; // Additional parameter types type TimeseriesInterval = 'HOUR' | 'DAY' | 'WEEK' | 'MONTH'; type TimeseriesParams = { startTimestamp: number; endTimestamp: number; interval: TimeseriesInterval; }; type HistoricalApyParams = { marketUniqueKey: string; chainId?: number; } & TimeseriesParams; type OracleDetailsParams = { marketUniqueKey: string; chainId?: number; }; type AccountOverviewParams = { address: string; chainId?: number; }; type LiquidationsParams = PaginationParams & { marketUniqueKeys?: string[]; startTimestamp?: number; endTimestamp?: number; orderBy?: 'Timestamp' | 'SeizedAssetsUsd' | 'RepaidAssetsUsd'; orderDirection?: OrderDirection; }; // Warning Schema const WarningSchema = z.object({ type: z.enum(['unrecognized_deposit_asset', 'unrecognized_vault_curator', 'not_whitelisted']), level: z.enum(['YELLOW', 'RED']) }); // Flow Cap Schema const FlowCapSchema = z.object({ market: z.object({ uniqueKey: z.string() }), maxIn: z.union([z.string(), z.number()]).transform(stringToNumber), maxOut: z.union([z.string(), z.number()]).transform(stringToNumber) }); // Public Allocator Config Schema const PublicAllocatorConfigSchema = z.object({ fee: z.number(), flowCaps: z.array(FlowCapSchema) }); // Pending Cap Schema const PendingCapSchema = z.object({ validAt: z.number(), supplyCap: z.union([z.string(), z.number()]).transform(stringToNumber), market: z.object({ uniqueKey: z.string() }) }); // Update VaultSchema const VaultSchema = z.object({ address: z.string(), symbol: z.string(), name: z.string(), creationBlockNumber: z.number(), creationTimestamp: z.number(), creatorAddress: z.string(), whitelisted: z.boolean(), asset: z.object({ id: z.string(), address: z.string(), decimals: z.number() }), chain: z.object({ id: z.number(), network: z.string() }), state: z.object({ id: z.string(), apy: z.number().nullable(), netApy: z.number().nullable(), totalAssets: z.union([z.string(), z.number()]).transform(stringToNumber), totalAssetsUsd: z.union([z.string(), z.number(), z.null()]).transform(val => val === null ? 0 : stringToNumber(val)), fee: z.number(), timelock: z.number() }), warnings: z.array(WarningSchema).optional(), pendingCaps: z.array(PendingCapSchema).optional(), allocators: z.array(z.object({ address: z.string() })).optional(), publicAllocatorConfig: PublicAllocatorConfigSchema.optional() }); // Vault Allocation Schema const VaultAllocationSchema = z.object({ market: MarketSchema, supplyCap: z.union([z.string(), z.number()]).transform(stringToNumber), supplyAssets: z.union([z.string(), z.number()]).transform(stringToNumber), supplyAssetsUsd: z.union([z.string(), z.number()]).transform(stringToNumber) }); // Vault Reallocate Schema const VaultReallocateSchema = z.object({ id: z.string(), timestamp: z.number(), hash: z.string(), blockNumber: z.number(), caller: z.string(), shares: z.union([z.string(), z.number()]).transform(stringToNumber), assets: z.union([z.string(), z.number()]).transform(stringToNumber), type: z.string(), vault: z.object({ id: z.string(), chain: z.object({ id: z.number() }) }), market: MarketSchema }); // Update VaultsResponseSchema const VaultsResponseSchema = z.object({ data: z.object({ vaults: z.object({ pageInfo: PageInfoSchema, items: z.array(VaultSchema) }) }) }); // Add new response schemas const VaultAllocationResponseSchema = z.object({ data: z.object({ vaultByAddress: z.object({ address: z.string(), state: z.object({ allocation: z.array(VaultAllocationSchema) }) }) }) }); const VaultReallocatesResponseSchema = z.object({ data: z.object({ vaultReallocates: z.object({ items: z.array(VaultReallocateSchema), pageInfo: PageInfoSchema }) }) }); // Add new tool constants const GET_VAULT_ALLOCATION_TOOL = 'get_vault_allocation'; const GET_VAULT_REALLOCATES_TOOL = 'get_vault_reallocates'; // Add new parameter types type VaultAllocationParams = { address: string; chainId?: number; }; type VaultReallocateParams = PaginationParams & { vaultAddress: string; orderBy?: 'Timestamp'; orderDirection?: OrderDirection; }; // Add new tool constants const GET_VAULTS_TOOL = 'get_vaults'; const GET_VAULT_POSITIONS_TOOL = 'get_vault_positions'; const GET_VAULT_TRANSACTIONS_TOOL = 'get_vault_transactions'; const GET_VAULT_APY_HISTORY_TOOL = 'get_vault_apy_history'; // Add new parameter types type VaultQueryParams = PaginationParams & { orderBy?: 'TotalAssetsUsd' | 'Apy' | 'NetApy'; orderDirection?: OrderDirection; where?: { asset_in?: string[]; address_in?: string[]; }; }; type VaultPositionsParams = PaginationParams & { vaultAddress: string; orderBy?: 'Shares' | 'Assets' | 'AssetsUsd'; orderDirection?: OrderDirection; }; type VaultTransactionsParams = PaginationParams & { orderBy?: 'Timestamp'; orderDirection?: OrderDirection; type_in?: ('MetaMorphoFee' | 'MetaMorphoWithdraw' | 'MetaMorphoDeposit')[]; }; type VaultApyHistoryParams = { address: string; options: { startTimestamp: number; endTimestamp: number; interval: 'HOUR' | 'DAY' | 'WEEK' | 'MONTH'; }; }; // Add new response schemas const VaultPositionsResponseSchema = z.object({ data: z.object({ vaultPositions: z.object({ items: z.array(z.object({ shares: z.union([z.string(), z.number()]).transform(stringToNumber), assets: z.union([z.string(), z.number()]).transform(stringToNumber), assetsUsd: z.union([z.string(), z.number()]).transform(stringToNumber), user: z.object({ address: z.string() }) })) }) }) }); const VaultTransactionsResponseSchema = z.object({ data: z.object({ transactions: z.object({ items: z.array(z.object({ hash: z.string(), timestamp: z.number(), type: z.string(), chain: z.object({ id: z.number(), network: z.string() }), user: z.object({ address: z.string() }), data: z.object({ shares: z.union([z.string(), z.number()]).transform(stringToNumber).optional(), assets: z.union([z.string(), z.number()]).transform(stringToNumber).optional(), vault: z.object({ address: z.string() }).optional() }).optional() })) }) }) }); const VaultApyHistoryResponseSchema = z.object({ data: z.object({ vaultByAddress: z.object({ address: z.string(), historicalState: z.object({ apy: z.array(TimeseriesPointSchema), netApy: z.array(TimeseriesPointSchema) }) }) }) }); // Create server instance with capabilities to handle tools const server = new Server( { name: "morpho-api-server", version: "1.0.0" }, { capabilities: { tools: {} } } ); // Implementation for listing available tools server.setRequestHandler(ListToolsRequestSchema, async () => { return { tools: [ { name: GET_MARKETS_TOOL, description: 'Retrieves markets from Morpho with pagination, ordering, and filtering support.', inputSchema: { type: 'object', properties: { first: { type: 'number', description: 'Number of items to return (default: 100)' }, skip: { type: 'number', description: 'Number of items to skip' }, orderBy: { type: 'string', enum: ['Lltv', 'BorrowApy', 'SupplyApy', 'BorrowAssets', 'SupplyAssets', 'BorrowAssetsUsd', 'SupplyAssetsUsd', 'Fee', 'Utilization'], description: 'Field to order by' }, orderDirection: { type: 'string', enum: ['Asc', 'Desc'], description: 'Order direction' }, where: { type: 'object', properties: { whitelisted: { type: 'boolean' }, collateralAssetAddress: { type: 'string' }, loanAssetAddress: { type: 'string' }, uniqueKey_in: { type: 'array', items: { type: 'string' } } } } } }, }, { name: GET_WHITELISTED_MARKETS_TOOL, description: 'Retrieves only whitelisted markets from Morpho.', inputSchema: { type: 'object', properties: {}, // No input parameters for this tool }, }, { name: GET_ASSET_PRICE_TOOL, description: 'Get current price and yield information for specific assets.', inputSchema: { type: 'object', properties: { symbol: { type: 'string', description: 'Asset symbol (e.g. "sDAI")' }, chainId: { type: 'number', description: 'Chain ID (default: 1 for Ethereum)' }, first: { type: 'number', description: 'Number of items to return' }, skip: { type: 'number', description: 'Number of items to skip' } }, required: ['symbol'] }, }, { name: GET_MARKET_POSITIONS_TOOL, description: 'Get positions overview for specific markets with pagination and ordering.', inputSchema: { type: 'object', properties: { marketUniqueKey: { type: 'string', description: 'Unique key of the market' }, first: { type: 'number', description: 'Number of positions to return (default: 30)' }, skip: { type: 'number', description: 'Number of positions to skip' }, orderBy: { type: 'string', enum: ['SupplyShares', 'BorrowShares', 'SupplyAssets', 'BorrowAssets'], description: 'Field to order by' }, orderDirection: { type: 'string', enum: ['Asc', 'Desc'], description: 'Order direction' } }, required: ['marketUniqueKey'] }, }, { name: GET_HISTORICAL_APY_TOOL, description: 'Get historical APY data for a specific market.', inputSchema: { type: 'object', properties: { marketUniqueKey: { type: 'string' }, chainId: { type: 'number' }, startTimestamp: { type: 'number' }, endTimestamp: { type: 'number' }, interval: { type: 'string', enum: ['HOUR', 'DAY', 'WEEK', 'MONTH'] } }, required: ['marketUniqueKey', 'startTimestamp', 'endTimestamp', 'interval'] }, }, { name: GET_ORACLE_DETAILS_TOOL, description: 'Get oracle details for a specific market.', inputSchema: { type: 'object', properties: { marketUniqueKey: { type: 'string' }, chainId: { type: 'number' } }, required: ['marketUniqueKey'] }, }, { name: GET_ACCOUNT_OVERVIEW_TOOL, description: 'Get account overview including positions and transactions.', inputSchema: { type: 'object', properties: { address: { type: 'string' }, chainId: { type: 'number' } }, required: ['address'] }, }, { name: GET_LIQUIDATIONS_TOOL, description: 'Get liquidation events with filtering and pagination.', inputSchema: { type: 'object', properties: { marketUniqueKeys: { type: 'array', items: { type: 'string' } }, startTimestamp: { type: 'number' }, endTimestamp: { type: 'number' }, first: { type: 'number' }, skip: { type: 'number' }, orderBy: { type: 'string', enum: ['Timestamp', 'SeizedAssetsUsd', 'RepaidAssetsUsd'] }, orderDirection: { type: 'string', enum: ['Asc', 'Desc'] } } }, }, { name: GET_VAULT_ALLOCATION_TOOL, description: 'Get vault allocation for a specific market.', inputSchema: { type: 'object', properties: { address: { type: 'string' }, chainId: { type: 'number' } }, required: ['address'] }, }, { name: GET_VAULT_REALLOCATES_TOOL, description: 'Get vault reallocates for a specific vault.', inputSchema: { type: 'object', properties: { vaultAddress: { type: 'string' }, first: { type: 'number' }, skip: { type: 'number' }, orderBy: { type: 'string', enum: ['Timestamp'] }, orderDirection: { type: 'string', enum: ['Asc', 'Desc'] } }, required: ['vaultAddress'] }, }, { name: GET_VAULTS_TOOL, description: 'Retrieves all vaults with their current states.', inputSchema: { type: 'object', properties: { first: { type: 'number' }, skip: { type: 'number' }, orderBy: { type: 'string', enum: ['TotalAssetsUsd', 'Apy', 'NetApy'] }, orderDirection: { type: 'string', enum: ['Asc', 'Desc'] } } } }, { name: GET_VAULT_POSITIONS_TOOL, description: 'Get positions for a specific vault.', inputSchema: { type: 'object', properties: { vaultAddress: { type: 'string' }, first: { type: 'number' }, skip: { type: 'number' }, orderBy: { type: 'string', enum: ['Shares', 'Assets', 'AssetsUsd'] }, orderDirection: { type: 'string', enum: ['Asc', 'Desc'] } }, required: ['vaultAddress'] } }, { name: GET_VAULT_TRANSACTIONS_TOOL, description: 'Get latest vault transactions.', inputSchema: { type: 'object', properties: { first: { type: 'number' }, skip: { type: 'number' }, orderBy: { type: 'string', enum: ['Timestamp'] }, orderDirection: { type: 'string', enum: ['Asc', 'Desc'] }, type_in: { type: 'array', items: { type: 'string', enum: ['MetaMorphoFee', 'MetaMorphoWithdraw', 'MetaMorphoDeposit'] } } } } }, { name: GET_VAULT_APY_HISTORY_TOOL, description: 'Get historical APY data for a vault.', inputSchema: { type: 'object', properties: { address: { type: 'string' }, options: { type: 'object', properties: { startTimestamp: { type: 'number' }, endTimestamp: { type: 'number' }, interval: { type: 'string', enum: ['HOUR', 'DAY', 'WEEK', 'MONTH'] } }, required: ['startTimestamp', 'endTimestamp', 'interval'] } }, required: ['address', 'options'] } } ], }; }); // Helper function to build GraphQL query parameters function buildQueryParams(params: PaginationParams & { orderBy?: string, orderDirection?: OrderDirection, where?: Record<string, any> } = {}): string { const queryParts: string[] = []; if (params.first !== undefined) queryParts.push(`first: ${params.first}`); if (params.skip !== undefined) queryParts.push(`skip: ${params.skip}`); if (params.orderBy) queryParts.push(`orderBy: ${params.orderBy}`); if (params.orderDirection) queryParts.push(`orderDirection: ${params.orderDirection}`); if (params.where && Object.keys(params.where).length > 0) { const whereStr = JSON.stringify(params.where).replace(/"([^"]+)":/g, '$1:'); queryParts.push(`where: ${whereStr}`); } return queryParts.length > 0 ? `(${queryParts.join(', ')})` : ''; } // Implementation to handle tool execution requests server.setRequestHandler(CallToolRequestSchema, async (request) => { const { name, params = {} } = request.params; if (name === GET_MARKETS_TOOL) { try { const queryParams = buildQueryParams(params as MarketQueryParams); const query = ` query { markets${queryParams} { pageInfo { count countTotal } items { uniqueKey lltv oracleAddress irmAddress loanAsset { address symbol decimals } collateralAsset { address symbol decimals } state { borrowApy borrowAssets borrowAssetsUsd supplyApy supplyAssets supplyAssetsUsd fee utilization } } } } `; const response = await axios.post(MORPHO_API_BASE, { query }); const validatedData = MarketsResponseSchema.parse(response.data); return { content: [ { type: 'text', text: JSON.stringify(validatedData.data.markets, null, 2), }, ], }; } catch (error: any) { console.error('Error calling Morpho API:', error.message); return { isError: true, content: [{ type: 'text', text: `Error retrieving markets: ${error.message}` }], }; } } if (name === GET_WHITELISTED_MARKETS_TOOL) { try { const query = ` query { markets(where:{whitelisted: true}) { items { whitelisted uniqueKey lltv oracleAddress irmAddress loanAsset { address symbol decimals } collateralAsset { address symbol decimals } state { borrowApy borrowAssets borrowAssetsUsd supplyApy supplyAssets supplyAssetsUsd fee utilization } } } } `; const response = await axios.post(MORPHO_API_BASE, { query }); const validatedData = MorphoApiResponseSchema.parse(response.data); return { content: [ { type: 'text', text: JSON.stringify(validatedData.data.markets.items, null, 2), }, ], }; } catch (error: any) { console.error('Error calling Morpho API:', error.message); return { isError: true, content: [{ type: 'text', text: `Error retrieving whitelisted markets: ${error.message}` }], }; } } if (name === GET_ASSET_PRICE_TOOL) { try { const { symbol, chainId = 1, ...paginationParams } = params as AssetPriceParams; const queryParams = buildQueryParams({ ...paginationParams, where: { symbol_in: [symbol], chainId } }); const query = ` query { assets${queryParams} { pageInfo { count countTotal } items { symbol address priceUsd chain { id network currency } yield { apr } } } } `; const response = await axios.post(MORPHO_API_BASE, { query }); const validatedData = AssetsResponseSchema.parse(response.data); return { content: [ { type: 'text', text: JSON.stringify(validatedData.data.assets, null, 2), }, ], }; } catch (error: any) { console.error('Error calling Morpho API:', error.message); return { isError: true, content: [{ type: 'text', text: `Error retrieving asset price: ${error.message}` }], }; } } if (name === GET_MARKET_POSITIONS_TOOL) { try { const { marketUniqueKey, ...queryParams } = params as MarketPositionsParams; const finalParams = buildQueryParams({ ...queryParams, where: { marketUniqueKey_in: [marketUniqueKey] } }); const query = ` query { marketPositions${finalParams} { pageInfo { count countTotal } items { supplyShares supplyAssets supplyAssetsUsd borrowShares borrowAssets borrowAssetsUsd collateral collateralUsd market { uniqueKey loanAsset { address symbol } collateralAsset { address symbol } } user { address } } } } `; const response = await axios.post(MORPHO_API_BASE, { query }); const validatedData = MarketPositionsResponseSchema.parse(response.data); return { content: [ { type: 'text', text: JSON.stringify(validatedData.data.marketPositions, null, 2), }, ], }; } catch (error: any) { console.error('Error calling Morpho API:', error.message); return { isError: true, content: [{ type: 'text', text: `Error retrieving market positions: ${error.message}` }], }; } } if (name === GET_HISTORICAL_APY_TOOL) { try { const { marketUniqueKey, chainId = 1, startTimestamp, endTimestamp, interval } = params as HistoricalApyParams; const query = ` query MarketApys { marketByUniqueKey( uniqueKey: "${marketUniqueKey}" chainId: ${chainId} ) { uniqueKey historicalState { supplyApy(options: { startTimestamp: ${startTimestamp} endTimestamp: ${endTimestamp} interval: ${interval} }) { x y } borrowApy(options: { startTimestamp: ${startTimestamp} endTimestamp: ${endTimestamp} interval: ${interval} }) { x y } } } }`; const response = await axios.post(MORPHO_API_BASE, { query }); const validatedData = HistoricalApyResponseSchema.parse(response.data); return { content: [{ type: 'text', text: JSON.stringify(validatedData.data.marketByUniqueKey, null, 2) }], }; } catch (error: any) { return { isError: true, content: [{ type: 'text', text: `Error retrieving historical APY: ${error.message}` }], }; } } if (name === GET_ORACLE_DETAILS_TOOL) { try { const { marketUniqueKey, chainId = 1 } = params as OracleDetailsParams; const query = ` query { marketByUniqueKey( uniqueKey: "${marketUniqueKey}" chainId: ${chainId} ) { oracle { address type data { ... on MorphoChainlinkOracleData { baseFeedOne { address description vendor pair } vault } ... on MorphoChainlinkOracleV2Data { baseFeedOne { address description vendor pair } } } } } }`; const response = await axios.post(MORPHO_API_BASE, { query }); const validatedData = OracleDetailsResponseSchema.parse(response.data); return { content: [{ type: 'text', text: JSON.stringify(validatedData.data.marketByUniqueKey, null, 2) }], }; } catch (error: any) { return { isError: true, content: [{ type: 'text', text: `Error retrieving oracle details: ${error.message}` }], }; } } if (name === GET_ACCOUNT_OVERVIEW_TOOL) { try { const { address, chainId = 1 } = params as AccountOverviewParams; const query = ` query { userByAddress( chainId: ${chainId} address: "${address}" ) { address marketPositions { market { uniqueKey } borrowAssets borrowAssetsUsd supplyAssets supplyAssetsUsd } vaultPositions { vault { address name } assets assetsUsd shares } transactions { hash timestamp type } } }`; const response = await axios.post(MORPHO_API_BASE, { query }); const validatedData = AccountOverviewResponseSchema.parse(response.data); return { content: [{ type: 'text', text: JSON.stringify(validatedData.data.userByAddress, null, 2) }], }; } catch (error: any) { return { isError: true, content: [{ type: 'text', text: `Error retrieving account overview: ${error.message}` }], }; } } if (name === GET_LIQUIDATIONS_TOOL) { try { const liquidationParams = params as LiquidationsParams; const where: Record<string, any> = { type_in: ['MarketLiquidation'] }; if (liquidationParams.marketUniqueKeys?.length) { where.marketUniqueKey_in = liquidationParams.marketUniqueKeys; } if (liquidationParams.startTimestamp) { where.timestamp_gte = liquidationParams.startTimestamp; } if (liquidationParams.endTimestamp) { where.timestamp_lte = liquidationParams.endTimestamp; } const queryParams = buildQueryParams({ first: liquidationParams.first, skip: liquidationParams.skip, orderBy: liquidationParams.orderBy, orderDirection: liquidationParams.orderDirection, where }); const query = ` query { transactions${queryParams} { pageInfo { count countTotal } items { blockNumber hash type timestamp user { address } data { ... on MarketLiquidationTransactionData { seizedAssets repaidAssets seizedAssetsUsd repaidAssetsUsd badDebtAssetsUsd liquidator market { uniqueKey } } } } } }`; const response = await axios.post(MORPHO_API_BASE, { query }); const validatedData = LiquidationsResponseSchema.parse(response.data); return { content: [{ type: 'text', text: JSON.stringify(validatedData.data.transactions, null, 2) }], }; } catch (error: any) { return { isError: true, content: [{ type: 'text', text: `Error retrieving liquidations: ${error.message}` }], }; } } if (name === GET_VAULT_ALLOCATION_TOOL) { try { const { address, chainId = 1 } = params as VaultAllocationParams; const query = ` query { vaultByAddress( chainId: ${chainId} address: "${address}" ) { address state { allocation { market { uniqueKey } supplyCap supplyAssets supplyAssetsUsd } } } }`; const response = await axios.post(MORPHO_API_BASE, { query }); const validatedData = VaultAllocationResponseSchema.parse(response.data); return { content: [{ type: 'text', text: JSON.stringify(validatedData.data.vaultByAddress, null, 2) }], }; } catch (error: any) { return { isError: true, content: [{ type: 'text', text: `Error retrieving vault allocation: ${error.message}` }], }; } } if (name === GET_VAULT_REALLOCATES_TOOL) { try { const { vaultAddress, first, skip, orderBy = 'Timestamp', orderDirection = 'Asc' } = params as VaultReallocateParams; const queryParams = buildQueryParams({ first, skip, orderBy, orderDirection, where: { vaultAddress_in: [vaultAddress] } }); const query = ` query { vaultReallocates${queryParams} { pageInfo { count countTotal } items { id timestamp hash blockNumber caller shares assets type vault { id chain { id } } market { uniqueKey } } } }`; const response = await axios.post(MORPHO_API_BASE, { query }); const validatedData = VaultReallocatesResponseSchema.parse(response.data); return { content: [{ type: 'text', text: JSON.stringify(validatedData.data.vaultReallocates, null, 2) }], }; } catch (error: any) { return { isError: true, content: [{ type: 'text', text: `Error retrieving vault reallocates: ${error.message}` }], }; } } if (name === GET_VAULTS_TOOL) { try { const queryParams = buildQueryParams(params as VaultQueryParams); const query = ` query { vaults${queryParams} { pageInfo { count countTotal } items { address symbol name creationBlockNumber creationTimestamp creatorAddress whitelisted asset { id address decimals } chain { id network } state { id apy netApy totalAssets totalAssetsUsd fee timelock } warnings { type level } } } }`; const response = await axios.post(MORPHO_API_BASE, { query }); const validatedData = VaultsResponseSchema.parse(response.data); return { content: [{ type: 'text', text: JSON.stringify(validatedData.data.vaults, null, 2) }] }; } catch (error: any) { return { isError: true, content: [{ type: 'text', text: `Error retrieving vaults: ${error.message}` }] }; } } if (name === GET_VAULT_POSITIONS_TOOL) { try { const { vaultAddress, ...rest } = params as VaultPositionsParams; const queryParams = buildQueryParams({ ...rest, where: { vaultAddress_in: [vaultAddress] } }); const query = ` query { vaultPositions${queryParams} { items { shares assets assetsUsd user { address } } } }`; const response = await axios.post(MORPHO_API_BASE, { query }); const validatedData = VaultPositionsResponseSchema.parse(response.data); return { content: [{ type: 'text', text: JSON.stringify(validatedData.data.vaultPositions, null, 2) }] }; } catch (error: any) { return { isError: true, content: [{ type: 'text', text: `Error retrieving vault positions: ${error.message}` }] }; } } if (name === GET_VAULT_TRANSACTIONS_TOOL) { try { const { type_in, ...rest } = params as VaultTransactionsParams; const queryParams = buildQueryParams({ ...rest, where: { type_in } }); const query = ` query { transactions${queryParams} { items { hash timestamp type chain { id network } user { address } data { ... on VaultTransactionData { shares assets vault { address } } } } } }`; const response = await axios.post(MORPHO_API_BASE, { query }); const validatedData = VaultTransactionsResponseSchema.parse(response.data); return { content: [{ type: 'text', text: JSON.stringify(validatedData.data.transactions, null, 2) }] }; } catch (error: any) { return { isError: true, content: [{ type: 'text', text: `Error retrieving vault transactions: ${error.message}` }] }; } } if (name === GET_VAULT_APY_HISTORY_TOOL) { try { const { address, options } = params as VaultApyHistoryParams; const query = ` query { vaultByAddress(address: "${address}") { address historicalState { apy(options: { startTimestamp: ${options.startTimestamp} endTimestamp: ${options.endTimestamp} interval: ${options.interval} }) { x y } netApy(options: { startTimestamp: ${options.startTimestamp} endTimestamp: ${options.endTimestamp} interval: ${options.interval} }) { x y } } } }`; const response = await axios.post(MORPHO_API_BASE, { query }); const validatedData = VaultApyHistoryResponseSchema.parse(response.data); return { content: [{ type: 'text', text: JSON.stringify(validatedData.data.vaultByAddress, null, 2) }] }; } catch (error: any) { return { isError: true, content: [{ type: 'text', text: `Error retrieving vault APY history: ${error.message}` }] }; } } // Tool not found throw new Error(`Tool not found: ${name}`); }); // Set up the transport for the server const transport = new StdioServerTransport(); // Start the server async function main() { await server.connect(transport); console.error("Morpho API MCP Server running on stdio"); } main().catch((error) => { console.error("Fatal error in main():", error); process.exit(1); });

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/crazyrabbitLTC/mcp-morpho-server'

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