Lighthouse MCP

by l3wi
import * as fs from "fs/promises"; import * as path from "path"; import { fileURLToPath } from "url"; import { scriptName, version } from "./index.js"; // Base Interfaces - Common properties interface BasePlatform { id: string; name: string; logoUrl: string; slug?: string; // Slug is optional as it might not be always present } interface BaseNetwork { id: string; name: string; logoUrl: string; } interface BaseAccount { id: string; name: string; } interface BaseAsset { id: string; symbol: string; name: string; logoUrl: string | null; // logoUrl can be null in some cases type: string; // Keeping as string for flexibility, can be made into a more specific enum if needed price?: number; // Price is optional as it's not always present in BaseAsset, but in derived interfaces } interface BaseSnapshot { id: string; timestamp: string; value: number; } interface BaseTimeRangePresets { "1d": string | null; "7d": string | null; "30d": string | null; "90d": string | null; } interface BaseGainerLoserItem { id: string; symbol: string; logoUrl: string; type: string; currAmount: number; currPrice: number; currUsdValue: number; prevAmount: number; prevPrice: number; prevUsdValue: number; diffUsdValue: number; } interface BaseChangeByTypeItem { type: string; currUsdValue: number; prevUsdValue: number; diffUsdValue: number; } // Interfaces for PortfolioLatestResponse - Lighthouse API /latest endpoint export interface PortfolioLatestResponse { id: string; status: string; usdValue: number; takenAt: string; finishedAt: string; accounts: { [accountId: string]: Account; }; networks: { [networkId: string]: Network; }; platforms: { [platformId: string]: Platform; }; positions: Position[]; nftCollections: { [collectionId: string]: NftCollection; }; } export interface Account extends BaseAccount { type: string; } export interface Network extends BaseNetwork {} export interface Platform extends BasePlatform { slug?: string; // Make slug optional here if it's not always present in this context } export interface Position { id: string; type: string; ref: string; usdValue: number; assets: LighthouseAsset[]; // Renamed to avoid conflict and clarify context accountId: string; networkId: string; platformId: string; customPosition: null; healthFactor: HealthFactor | null; } export interface LighthouseAsset extends BaseAsset { // Renamed to avoid conflict and clarify context logoUrl: string | null; context: string; amount: number; price: number; usdValue: number; ids: { [idName: string]: string; }; collectionId?: string; // Optional, only for NFT type } export interface HealthFactor { value: number; method: string; } export interface NftCollection { id: string; name: string; description: string; logoUrl: string; ids: { [idName: string]: string; }; } export interface UserResponse { user: { id: string; portfolios: { id: string; name: string; role: string; slug: string; }[]; }; } // Interfaces for Yield Data - Based on your first request enum AssetTypeEnum { // Renamed to avoid conflict with interface name STABLECOIN = "STABLECOIN", ETH_EQUIVALENT = "ETH_EQUIVALENT", POOL = "POOL", NATIVE = "NATIVE", } enum PoolItemTypeEnum { // Renamed to avoid conflict with interface name NATIVE = "NATIVE", POOL = "POOL", } export interface YieldAsset extends BaseAsset { // Using BaseAsset and extending it with price logoUrl: string; // logoUrl is not nullable here based on your example price: number; type: AssetTypeEnum; } interface SupplyElement { asset: YieldAsset; amount: number; } interface ReceiveElement { asset: YieldAsset; apy: number; type: PoolItemTypeEnum; } interface BorrowElement { asset: YieldAsset; amount: number; } interface PayElement { asset: YieldAsset; apy: number; type: PoolItemTypeEnum; } export interface Pool { // Renamed from YieldPool to Pool as it seems to represent the same concept platform: Platform; // Using Platform interface here, assuming platform info is consistent network: Network; // Using Network interface here, assuming network info is consistent account: BaseAccount; // Using BaseAccount, as 'type' is not present in your example name: string; supply: SupplyElement[]; receive: ReceiveElement[]; borrow: BorrowElement[]; pay: PayElement[]; } export interface YieldResponse { // Renamed from YieldResponse to ApiResponse pools: Pool[]; platforms: Platform[]; // Using Platform interface here } // Interfaces for PortfolioPerformanceResponse - Based on your second request export interface PortfolioPerformanceResponse { startsAt: string; endsAt: string; presets: TimeRangePresets; // Reusing TimeRangePresets usdValueChange: number; lastSnapshotUsdValue: number; snapshots: Snapshot[]; // Reusing Snapshot gainers: GainerLoserItem[]; // Reusing GainerLoserItem losers: GainerLoserItem[]; // Reusing GainerLoserItem changeByType: ChangeByTypeItem[]; // Reusing ChangeByTypeItem } export interface TimeRangePresets extends BaseTimeRangePresets {} export interface Snapshot extends BaseSnapshot {} export interface GainerLoserItem extends BaseGainerLoserItem {} export interface ChangeByTypeItem extends BaseChangeByTypeItem {} export class Lighthouse { private sessionCookie: string | null = null; private sessionFile: string; constructor(sessionFilePath?: string) { // If no session file path is provided, use the default location if (!sessionFilePath) { const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); this.sessionFile = path.join(__dirname, ".lighthouse_session"); } else { this.sessionFile = sessionFilePath; } } private async fetcher( url: string, options: RequestInit = {} ): Promise<Response> { const response = await fetch(url, { ...options, headers: { ...options.headers, Cookie: `lh_session=${this.sessionCookie}`, "User-Agent": `Lighthouse MCP/${version}`, }, }); return response; } /** * Initialize the session from the session file */ public async initialize(): Promise<void> { this.sessionCookie = await this.loadSession(); } /** * Get the current session cookie */ public getSessionCookie(): string | null { return this.sessionCookie; } /** * Check if the user is authenticated */ public isAuthenticated(): boolean { return this.sessionCookie !== null; } /** * Load session from file */ private async loadSession(): Promise<string | null> { try { const data = await fs.readFile(this.sessionFile, "utf-8"); return data.trim() || null; } catch (error) { return null; } } /** * Save session to file */ private async saveSession(cookie: string | null): Promise<void> { if (cookie) { await fs.writeFile(this.sessionFile, cookie, "utf-8"); } else { try { await fs.unlink(this.sessionFile); } catch (error) { // Ignore error if file doesn't exist } } } /** * Authenticate with Lighthouse using a transfer token URL */ public async authenticate( url: string ): Promise<{ success: boolean; message: string }> { try { // Extract token from URL const parsedUrl = new URL(url); const token = parsedUrl.searchParams.get("token"); if (!token) { return { success: false, message: "No token found in URL" }; } // Make the login request // Use the native fetch function to avoid cookie application const response = await fetch("https://lighthouse.one/v1/login", { method: "POST", headers: { "Content-Type": "application/json", "User-Agent": `Lighthouse MCP/${version}`, }, body: JSON.stringify({ type: "TRANSFER_TOKEN", token: token, }), }); if (!response.ok) { throw new Error(`Login failed with status ${response.status}`); } // Extract and store the session cookie const cookies = response.headers.get("set-cookie"); if (cookies) { const sessionCookieMatch = cookies.match(/lh_session=([^;]+)/); if (sessionCookieMatch) { this.sessionCookie = sessionCookieMatch[1]; // Save the session cookie to file await this.saveSession(this.sessionCookie); return { success: true, message: "Successfully authenticated with Lighthouse", }; } } throw new Error("No session cookie found in response"); } catch (error: any) { // Clear session cookie on error this.sessionCookie = null; await this.saveSession(null); return { success: false, message: `Authentication failed: ${error.message}`, }; } } /** * Logout and clear the session */ public async logout(): Promise<void> { this.sessionCookie = null; await this.saveSession(null); } /** * Get user data including portfolios */ public async getUserData(): Promise<UserResponse> { if (!this.sessionCookie) { throw new Error("Not authenticated. Please authenticate first."); } const response = await this.fetcher("https://lighthouse.one/v1/user"); if (!response.ok) { throw new Error(`API request failed with status ${response.status}`); } return await response.json(); } /** * List all portfolios */ public async listPortfolios(): Promise<string[]> { const userData = await this.getUserData(); return userData.user.portfolios.map((p) => p.name); } /** * Find a portfolio by name or partial match */ public async findPortfolio( portfolioName?: string ): Promise<{ slug: string; name: string }> { const userData = await this.getUserData(); if (userData.user.portfolios.length <= 0) { throw new Error("The user has no portfolios. Please create one."); } // If no portfolio name provided, return the first one if (!portfolioName) { return { slug: userData.user.portfolios[0].slug, name: userData.user.portfolios[0].name, }; } // Try exact match first const exactMatch = userData.user.portfolios.find( (p) => p.name.toLowerCase() === portfolioName.toLowerCase() ); if (exactMatch) { return { slug: exactMatch.slug, name: exactMatch.name }; } // Try partial match const partialMatch = userData.user.portfolios.find((p) => p.name.toLowerCase().includes(portfolioName.toLowerCase()) ); if (partialMatch) { return { slug: partialMatch.slug, name: partialMatch.name }; } // No match found throw new Error( `Portfolio "${portfolioName}" not found. Available portfolios: ${userData.user.portfolios .map((p) => p.name) .join(", ")}` ); } /** * Get portfolio data by slug */ public async getPortfolioData( portfolioSlug: string ): Promise<PortfolioLatestResponse> { if (!this.sessionCookie) { throw new Error("Not authenticated. Please authenticate first."); } const response = await this.fetcher( `https://lighthouse.one/v1/workspaces/${portfolioSlug}/snapshots/latest` ); if (!response.ok) { throw new Error(`API request failed with status ${response.status}`); } return await response.json(); } /** * Get yield data for a portfolio */ public async getYieldData(portfolioSlug: string): Promise<YieldResponse> { if (!this.sessionCookie) { throw new Error("Not authenticated. Please authenticate first."); } const response = await this.fetcher( `https://lighthouse.one/v1/workspaces/${portfolioSlug}/yields` ); if (!response.ok) { throw new Error(`API request failed with status ${response.status}`); } return await response.json(); } /** * Get performance data for a portfolio */ public async getPerformanceData( portfolioSlug: string, startDate: string ): Promise<PortfolioPerformanceResponse> { const response = await this.fetcher( `https://lighthouse.one/v1/workspaces/${portfolioSlug}/performance?startsAt=${startDate}` ); if (!response.ok) { throw new Error(`API request failed with status ${response.status}`); } return await response.json(); } }