Lighthouse MCP
by l3wi
#!/usr/bin/env node
import { FastMCP } from "fastmcp";
import { z } from "zod";
import {
Account,
Lighthouse,
Position,
LighthouseAsset,
} from "./lighthouse.js";
import { formatNumber, formatPercentage } from "./utils.js";
export const version = "0.0.6";
export const scriptName = "Lighthouse MCP";
const server = new FastMCP({
name: "Lighthouse MCP",
version: version,
});
server.addTool({
name: "authenticate",
description: "Authenticate with Lighthouse using a transfer token URL",
parameters: z.object({
url: z.string().url(),
}),
execute: async (args) => {
try {
const result = await lighthouse.authenticate(args.url);
if (result.success) {
return {
content: [
{
type: "text",
text: result.message,
},
],
};
} else {
return {
content: [{ type: "text", text: result.message }],
};
}
} catch (error: any) {
return {
content: [
{ type: "text", text: `Authentication failed: ${error.message}` },
],
};
}
},
});
server.addTool({
name: "listLighthousePortfolios",
description:
"List all Lighthouse portfolios, their total portfolio value, the wallets within each portfolio and their total value",
parameters: z.object({}),
execute: async () => {
const portfolios = await lighthouse.getUserData();
const porfolioData = await Promise.all(
portfolios.user.portfolios.map(async (portfolio) => {
return await lighthouse.getPortfolioData(portfolio.slug);
})
);
//Sum the portfolios
const totalPortfolioValue = porfolioData.reduce(
(acc, data) => acc + data.usdValue,
0
);
/// Format the porfolio data
const formattedPorfolioData = porfolioData.map((data, i) => {
return `# ${i + 1}. ${
portfolios.user.portfolios[i].name
}\n\n## Total Portfolio Value: $${data.usdValue.toLocaleString()}\n\n## Wallets (${
Object.keys(data.accounts).length
}):\n${Object.entries(data.accounts)
.map(([accountId, account]) => `- ${account.name} (${account.type})`)
.join("\n")}`;
});
return {
content: [
{
type: "text",
text: `# Lighthouse Portfolios\n\n${formattedPorfolioData.join(
"\n"
)}\n\n## Total Portfolio Value: $${totalPortfolioValue.toLocaleString()}`,
},
],
};
},
});
// Tool to fetch and format Lighthouse portfolio data
server.addTool({
name: "getLighthousePortfolio",
description:
"Fetch and display a detailed summary of a Lighthouse portfolio with breakdown by asset types and major holdings.",
parameters: z.object({
portfolio: z
.string()
.optional()
.describe(
"Optional portfolio name to select a specific portfolio to display a summary for"
),
}),
execute: async (args) => {
try {
if (!lighthouse.isAuthenticated()) {
return {
content: [
{
type: "text",
text: "No session cookie available. Please authenticate first.",
},
],
};
}
// Find the portfolio
const portfolio = await lighthouse.findPortfolio(args.portfolio);
// Get the portfolio data
const data = await lighthouse.getPortfolioData(portfolio.slug);
// Calculate total USD value
const totalUsdValue = data.usdValue;
// Get wallets/accounts
const wallets = Object.values(data.accounts).map((account: Account) => ({
id: account.id,
name: account.name,
type: account.type,
}));
// Calculate asset type breakdown
const assetTypeMap = new Map<string, number>();
data.positions.forEach((position: Position) => {
position.assets.forEach((asset: LighthouseAsset) => {
const currentValue = assetTypeMap.get(asset.type) || 0;
assetTypeMap.set(asset.type, currentValue + asset.usdValue);
});
});
// Convert to array and sort by value
const assetTypeBreakdown = Array.from(assetTypeMap.entries())
.map(([type, value]) => ({
type,
value,
percentage: (value / totalUsdValue) * 100,
}))
.sort((a, b) => b.value - a.value);
// Get major assets (>= $1000)
const majorAssets = data.positions
.flatMap((position) =>
position.assets
.filter((asset) => asset.usdValue >= 1000)
.map((asset) => ({
name: asset.name,
symbol: asset.symbol,
value: asset.usdValue,
amount: asset.amount,
}))
)
.sort((a, b) => b.value - a.value);
// Format the response
const assetTypeTable = `
| Asset Type | Net Value | % of Portfolio |
|------------|-----------|----------------|
${assetTypeBreakdown
.map(
(item) =>
`| ${item.type} | $${formatNumber(item.value)} | ${formatPercentage(
item.percentage
)}% |`
)
.join("\n")}
`;
const assetsTable = `
| Asset | Value | Amount |
|-------|-------|--------|
${majorAssets
.map(
(asset) =>
`| ${asset.name} (${asset.symbol}) | $${formatNumber(
asset.value
)} | ${formatNumber(asset.amount)} |`
)
.join("\n")}
`;
return {
content: [
{
type: "text",
text: `# Lighthouse Portfolio Summary: ${
portfolio.name
}\n\n## Total Portfolio Value: $${formatNumber(
totalUsdValue
)}\n\n## Wallets (${wallets.length}):\n${wallets
.map((w) => `- ${w.name} (${w.type})`)
.join(
"\n"
)}\n\n## Asset Type Breakdown:\n${assetTypeTable}\n\n## Major Holdings (>= $1,000):\n${assetsTable}`,
},
],
};
} catch (error: any) {
return {
content: [
{
type: "text",
text: `Failed to fetch Lighthouse portfolio: ${error.message}`,
},
],
};
}
},
});
server.addTool({
name: "getLighthouseYieldData",
description: "Get yield data for a Lighthouse portfolio",
parameters: z.object({
portfolio: z
.string()
.optional()
.describe(
"Optional portfolio name to select a specific portfolio to display a summary for"
),
}),
execute: async (args) => {
if (!lighthouse.isAuthenticated()) {
return {
content: [
{
type: "text",
text: "Not authenticated. Please authenticate first.",
},
],
};
}
const portfolio = args.portfolio
? await lighthouse.findPortfolio(args.portfolio)
: await lighthouse.findPortfolio();
const yieldData = await lighthouse.getYieldData(portfolio.slug);
// Calulate USD values
// Iterate through each pool
// Iterate through each supply, receive, borrow, pay
// Calculate the USD value of each receive & pay
// Sum the USD values of each receive & pay
// Return an array of pools with the USD values
const formattedYieldData = yieldData.pools.map((pool) => {
return {
...pool,
receiveUSD: pool.supply.map((supply, index) => {
return {
asset: supply.asset.symbol,
assetUSD: supply.amount * supply.asset.price,
apy: pool.receive[index].apy,
receiveUSD:
(pool.receive[index].apy / 100) *
supply.amount *
supply.asset.price,
};
}),
payUSD: pool.borrow.map((borrow, index) => {
return {
asset: borrow.asset.symbol,
assetUSD: borrow.amount * borrow.asset.price,
apy: pool.pay[index].apy,
payUSD:
(pool.pay[index].apy / 100) * borrow.amount * borrow.asset.price,
};
}),
};
});
const formattedYieldDataWithUsdValues = formattedYieldData.map((pool) => {
return {
...pool,
netYieldUSD:
pool.receiveUSD.reduce(
(acc, receive) => acc + receive.receiveUSD,
0
) - pool.payUSD.reduce((acc, pay) => acc + pay.payUSD, 0),
};
});
// Format the yield data
const responseFormattedYieldData = formattedYieldDataWithUsdValues
.map((pool) => {
return `# ${pool.platform.name} (${
pool.network.name
}) - Annual Yield: $${formatNumber(pool.netYieldUSD)} \n${
pool.receiveUSD.length > 0
? `## Receive: \n${pool.receiveUSD
.map(
(supply) =>
`${supply.asset} - $${formatNumber(
supply.receiveUSD
)} per year`
)
.join("\n")}`
: ""
}${
pool.payUSD.length > 0
? `## Pay: \n${pool.payUSD
.map(
(pay) =>
`${pay.asset} - $${formatNumber(pay.payUSD)} per year`
)
.join("\n")}`
: ""
}`;
})
.join("\n\n");
const totalSupplyUSD = formattedYieldDataWithUsdValues.reduce(
(acc, pool) => {
return (
acc +
pool.receiveUSD.reduce((acc, receive) => {
return acc + receive.assetUSD;
}, 0)
);
},
0
);
const totalBorrowUSD = formattedYieldDataWithUsdValues.reduce(
(acc, pool) => {
return acc + pool.payUSD.reduce((acc, pay) => acc + pay.assetUSD, 0);
},
0
);
const totalYieldUSD = formattedYieldDataWithUsdValues.reduce(
(acc, pool) => {
return acc + pool.netYieldUSD;
},
0
);
return {
content: [
{
type: "text",
text:
`${portfolio.name} \n Total Yield: $${formatNumber(totalYieldUSD)}
## Total Supplied: $${formatNumber(totalSupplyUSD)}
## Total Borrowed: $${formatNumber(totalBorrowUSD)}
## Avg APY: ${formatPercentage(
totalYieldUSD / totalSupplyUSD
)}% \n\n
` + responseFormattedYieldData,
},
],
};
},
});
server.addTool({
name: "getLighthousePerformanceData",
description: "Get performance data for a Lighthouse portfolio",
parameters: z.object({
portfolio: z.string().optional().describe("Optional portfolio name"),
startDate: z
.string()
.optional()
.describe("Optional start date. Formatted as YYYY-MM-DD"),
}),
execute: async (args) => {
const portfolio = args.portfolio
? await lighthouse.findPortfolio(args.portfolio)
: await lighthouse.findPortfolio();
const startDate = args.startDate
? new Date(args.startDate)
: new Date(Date.now() - 30 * 24 * 60 * 60 * 1000);
const performanceData = await lighthouse.getPerformanceData(
portfolio.slug,
startDate.toISOString().split("T")[0]
);
return {
content: [
{
type: "text",
text: `# ${portfolio.name} Performance Data
Timeframe: ${performanceData.startsAt} - ${performanceData.endsAt}
Period Return: ${formatNumber(
performanceData.usdValueChange
)} (${formatPercentage(
(performanceData.usdValueChange /
performanceData.lastSnapshotUsdValue) *
100
)})
----
Performance by asset type:
${performanceData.changeByType
.sort((a, b) => b.diffUsdValue - a.diffUsdValue)
.map((asset) => {
return `- ${asset.type}: ${formatNumber(
asset.diffUsdValue
)} (${formatPercentage(
asset.prevUsdValue / asset.currUsdValue
)}%)`;
})
.join("\n")}
----
Top 5 Gainers:
${performanceData.gainers
.sort((a, b) => b.diffUsdValue - a.diffUsdValue)
.slice(0, 5)
.map((gainer) => {
return `- ${gainer.symbol}: ${formatNumber(
gainer.diffUsdValue
)} (${formatPercentage(
gainer.diffUsdValue / gainer.prevUsdValue
)}%)`;
})
.join("\n")}
----
Top 5 Losers:
${performanceData.losers
.sort((a, b) => a.diffUsdValue - b.diffUsdValue)
.slice(0, 5)
.map((loser) => {
return `- ${loser.symbol}: ${formatNumber(
loser.diffUsdValue
)} (${formatPercentage(
loser.diffUsdValue / loser.prevUsdValue
)}%)`;
})
.join("\n")}
`,
},
],
};
},
});
// Create and initialize the Lighthouse client
const lighthouse = new Lighthouse();
// Initialize the Lighthouse client before starting the server
(async () => {
await lighthouse.initialize();
server.start({
transportType: "stdio",
});
})();