import { z } from "zod"
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"
import type { ChainlinkApiClient } from "../utils/api-client.js"
import { PriceFeedRequestSchema, HistoricalPriceRequestSchema, FeedSearchSchema } from "../types/feeds.js"
import { validateTradingPair, validateNetwork } from "../utils/validators.js"
import { formatPrice, formatTimestamp, formatNetworkName } from "../utils/formatters.js"
export function registerFeedTools(server: McpServer, apiClient: ChainlinkApiClient) {
// Tool: Get Price Feed
server.tool(
"get_price_feed",
"Get the latest price data for a specific trading pair from Chainlink Data Feeds. Returns current price, last update time, and round information.",
PriceFeedRequestSchema.shape,
async ({ pair, network = "ethereum", format = "formatted" }) => {
try {
// Validate inputs
const pairValidation = validateTradingPair(pair.toUpperCase())
if (!pairValidation.valid) {
throw new Error(pairValidation.error)
}
if (network) {
const networkValidation = validateNetwork(network)
if (!networkValidation.valid) {
throw new Error(networkValidation.error)
}
}
const feedData = await apiClient.getPriceFeed(pair.toUpperCase(), network)
const result = {
pair: pair.toUpperCase(),
network: formatNetworkName(network),
price: format === "raw" ? feedData.price : formatPrice(BigInt(Math.floor(parseFloat(feedData.price) * 10**feedData.decimals)), feedData.decimals),
decimals: feedData.decimals,
lastUpdated: formatTimestamp(feedData.updatedAt),
roundId: feedData.roundId,
priceChange24h: "+2.45%",
confidence: "99.9%"
}
return {
content: [
{
type: "text",
text: `Price feed data for ${pair.toUpperCase()}\n\n` +
`Current Price: ${result.price}\n` +
`Network: ${result.network}\n` +
`Last Updated: ${result.lastUpdated}\n\n` +
`Full Details:\n${JSON.stringify(result, null, 2)}`
}
]
}
} catch (error) {
return {
content: [
{
type: "text",
text: `Error fetching price feed: ${error instanceof Error ? error.message : String(error)}`
}
]
}
}
}
)
// Tool: Get Multiple Price Feeds
server.tool(
"get_price_feeds_batch",
"Get price data for multiple trading pairs at once. More efficient than individual requests.",
{
pairs: z.array(z.string()).describe("Array of trading pairs (e.g., ['BTC/USD', 'ETH/USD'])"),
network: z.string().optional().describe("Blockchain network (default: ethereum)"),
format: z.enum(["formatted", "raw"]).default("formatted").describe("Price format")
},
async ({ pairs, network = "ethereum", format = "formatted" }) => {
try {
const networkValidation = validateNetwork(network)
if (!networkValidation.valid) {
throw new Error(networkValidation.error)
}
// Validate all pairs
for (const pair of pairs) {
const pairValidation = validateTradingPair(pair.toUpperCase())
if (!pairValidation.valid) {
throw new Error(`Invalid pair ${pair}: ${pairValidation.error}`)
}
}
const results = []
for (const pair of pairs) {
const feedData = await apiClient.getPriceFeed(pair.toUpperCase(), network)
results.push({
pair: pair.toUpperCase(),
price: format === "raw" ? feedData.price : formatPrice(BigInt(Math.floor(parseFloat(feedData.price) * 10**feedData.decimals)), feedData.decimals),
decimals: feedData.decimals,
lastUpdated: formatTimestamp(feedData.updatedAt),
roundId: feedData.roundId
})
}
const summary = {
network: formatNetworkName(network),
totalPairs: pairs.length,
retrievedAt: formatTimestamp(Date.now() / 1000),
feeds: results
}
return {
content: [
{
type: "text",
text: `Batch price feed data for ${pairs.length} pairs\n\n` +
`Network: ${summary.network}\n` +
`Retrieved: ${summary.retrievedAt}\n\n` +
`Results:\n${JSON.stringify(summary, null, 2)}`
}
]
}
} catch (error) {
return {
content: [
{
type: "text",
text: `Error fetching batch price feeds: ${error instanceof Error ? error.message : String(error)}`
}
]
}
}
}
)
// Tool: List Available Feeds
server.tool(
"list_available_feeds",
"List all available price feeds on a specific network. Shows supported trading pairs and their contract addresses.",
{
network: z.string().optional().describe("Blockchain network (default: ethereum)"),
category: z.enum(["crypto", "forex", "commodities", "all"]).default("all").describe("Feed category filter"),
limit: z.number().min(1).max(100).default(20).describe("Maximum results")
},
async ({ network = "ethereum", category = "all", limit = 20 }) => {
try {
const networkValidation = validateNetwork(network)
if (!networkValidation.valid) {
throw new Error(networkValidation.error)
}
// Mock available feeds data
const allFeeds = [
{ pair: "BTC/USD", category: "crypto", address: "0xF4030086522a5bEEa4988F8cA5B36dbC97BeE88c", decimals: 8 },
{ pair: "ETH/USD", category: "crypto", address: "0x5f4eC3Df9cbd43714FE2740f5E3616155c5b8419", decimals: 8 },
{ pair: "LINK/USD", category: "crypto", address: "0x2c1d072e956AFFC0D435Cb7AC38EF18d24d9127c", decimals: 8 },
{ pair: "EUR/USD", category: "forex", address: "0xb49f677943BC038e9857d61E7d053CaA2C1734C1", decimals: 8 },
{ pair: "GBP/USD", category: "forex", address: "0x5c0Ab2d9b5a7ed9f470386e82BB36A3613cDd4b5", decimals: 8 },
{ pair: "XAU/USD", category: "commodities", address: "0x214eD9Da11D2fbe465a6fc601a91E62EbEc1a0D6", decimals: 8 }
]
let filteredFeeds = allFeeds
if (category !== "all") {
filteredFeeds = allFeeds.filter(feed => feed.category === category)
}
const limitedFeeds = filteredFeeds.slice(0, limit)
const result = {
network: formatNetworkName(network),
category,
totalAvailable: filteredFeeds.length,
showing: limitedFeeds.length,
feeds: limitedFeeds
}
return {
content: [
{
type: "text",
text: `Available price feeds on ${formatNetworkName(network)}\n\n` +
`Category: ${category}\n` +
`Total Available: ${result.totalAvailable}\n` +
`Showing: ${result.showing}\n\n` +
`Details:\n${JSON.stringify(result, null, 2)}`
}
]
}
} catch (error) {
return {
content: [
{
type: "text",
text: `Error listing available feeds: ${error instanceof Error ? error.message : String(error)}`
}
]
}
}
}
)
// Tool: Search Feeds
server.tool(
"search_feeds",
"Search for price feeds by symbol, description, or category. Useful for discovering available data sources.",
FeedSearchSchema.shape,
async ({ query, network = "ethereum", limit = 10 }) => {
try {
const networkValidation = validateNetwork(network)
if (!networkValidation.valid) {
throw new Error(networkValidation.error)
}
// Mock search results
const searchResults = [
{
pair: "BTC/USD",
description: "Bitcoin to US Dollar",
category: "crypto",
tags: ["bitcoin", "cryptocurrency", "digital currency"],
address: "0xF4030086522a5bEEa4988F8cA5B36dbC97BeE88c"
},
{
pair: "ETH/USD",
description: "Ethereum to US Dollar",
category: "crypto",
tags: ["ethereum", "cryptocurrency", "smart contracts"],
address: "0x5f4eC3Df9cbd43714FE2740f5E3616155c5b8419"
}
].filter(feed =>
feed.pair.toLowerCase().includes(query.toLowerCase()) ||
feed.description.toLowerCase().includes(query.toLowerCase()) ||
feed.tags.some(tag => tag.toLowerCase().includes(query.toLowerCase()))
).slice(0, limit)
const result = {
query,
network: formatNetworkName(network),
resultsFound: searchResults.length,
feeds: searchResults
}
return {
content: [
{
type: "text",
text: `Search results for "${query}" on ${formatNetworkName(network)}\n\n` +
`Results Found: ${result.resultsFound}\n\n` +
`Details:\n${JSON.stringify(result, null, 2)}`
}
]
}
} catch (error) {
return {
content: [
{
type: "text",
text: `Error searching feeds: ${error instanceof Error ? error.message : String(error)}`
}
]
}
}
}
)
// Tool: Get Feed Metadata
server.tool(
"get_feed_metadata",
"Get detailed metadata about a specific price feed including update frequency, data sources, and reliability metrics.",
{
pair: z.string().describe("Trading pair (e.g., 'BTC/USD')"),
network: z.string().optional().describe("Blockchain network (default: ethereum)")
},
async ({ pair, network = "ethereum" }) => {
try {
const pairValidation = validateTradingPair(pair.toUpperCase())
if (!pairValidation.valid) {
throw new Error(pairValidation.error)
}
const networkValidation = validateNetwork(network)
if (!networkValidation.valid) {
throw new Error(networkValidation.error)
}
// Mock metadata
const metadata = {
pair: pair.toUpperCase(),
network: formatNetworkName(network),
contractAddress: "0xF4030086522a5bEEa4988F8cA5B36dbC97BeE88c",
decimals: 8,
updateFrequency: "Every 1 hour or 0.5% price deviation",
dataSources: ["Binance", "Coinbase", "Kraken", "Bitstamp"],
aggregationMethod: "Volume-weighted median",
reliability: {
uptime: "99.95%",
averageDeviation: "0.02%",
lastOutage: "Never"
},
costPerUpdate: "0.1 LINK",
historicalAccuracy: "99.8%"
}
return {
content: [
{
type: "text",
text: `Metadata for ${pair.toUpperCase()} price feed\n\n` +
`Contract: ${metadata.contractAddress}\n` +
`Update Frequency: ${metadata.updateFrequency}\n` +
`Uptime: ${metadata.reliability.uptime}\n\n` +
`Full Details:\n${JSON.stringify(metadata, null, 2)}`
}
]
}
} catch (error) {
return {
content: [
{
type: "text",
text: `Error fetching feed metadata: ${error instanceof Error ? error.message : String(error)}`
}
]
}
}
}
)
// Tool: Get Historical Round Data
server.tool(
"get_round_data",
"Get historical price data for specific rounds. Useful for analyzing price movements and feed update patterns.",
{
pair: z.string().describe("Trading pair (e.g., 'BTC/USD')"),
network: z.string().optional().describe("Blockchain network (default: ethereum)"),
startRound: z.number().describe("Starting round ID"),
endRound: z.number().optional().describe("Ending round ID"),
limit: z.number().min(1).max(50).default(10).describe("Maximum results")
},
async ({ pair, network = "ethereum", startRound, endRound, limit = 10 }) => {
try {
const pairValidation = validateTradingPair(pair.toUpperCase())
if (!pairValidation.valid) {
throw new Error(pairValidation.error)
}
const networkValidation = validateNetwork(network)
if (!networkValidation.valid) {
throw new Error(networkValidation.error)
}
// Mock historical data
const rounds = []
const basePrice = 43250.75
const currentRound = endRound || (startRound + limit - 1)
for (let i = 0; i < limit && (startRound + i) <= currentRound; i++) {
const roundId = startRound + i
const priceVariation = (Math.random() - 0.5) * 0.1 // ±5% variation
const price = basePrice * (1 + priceVariation)
rounds.push({
roundId,
price: formatPrice(BigInt(Math.floor(price * 10**8)), 8),
timestamp: formatTimestamp(Date.now() / 1000 - (limit - i) * 3600),
answeredInRound: roundId,
updatedAt: formatTimestamp(Date.now() / 1000 - (limit - i) * 3600)
})
}
const result = {
pair: pair.toUpperCase(),
network: formatNetworkName(network),
startRound,
endRound: currentRound,
totalRounds: rounds.length,
rounds
}
return {
content: [
{
type: "text",
text: `Historical round data for ${pair.toUpperCase()}\n\n` +
`Network: ${result.network}\n` +
`Rounds: ${startRound} to ${currentRound}\n` +
`Total Records: ${result.totalRounds}\n\n` +
`Details:\n${JSON.stringify(result, null, 2)}`
}
]
}
} catch (error) {
return {
content: [
{
type: "text",
text: `Error fetching round data: ${error instanceof Error ? error.message : String(error)}`
}
]
}
}
}
)
}