UNPKG

finnhub-mcp

Version:

A Model Context Protocol server for Finnhub financial data

425 lines (378 loc) 13 kB
const fs = require("fs"); const path = require("path"); const https = require("https"); const { spawn } = require("child_process"); const GITHUB_REPO = "SalZaki/finnhub-mcp"; const RELEASE_URL = `https://api.github.com/repos/${GITHUB_REPO}/releases/latest`; class Spinner { constructor(message = "Loading") { this.message = message; this.frames = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]; this.frameIndex = 0; this.interval = null; } start() { this.interval = setInterval(() => { process.stdout.write(`\r${this.frames[this.frameIndex]} ${this.message}`); this.frameIndex = (this.frameIndex + 1) % this.frames.length; }, 100); } stop(finalMessage) { if (this.interval) { clearInterval(this.interval); this.interval = null; } process.stdout.write(`\r${finalMessage || this.message}\n`); } updateMessage(message) { this.message = message; } } function httpsGet(url) { return new Promise((resolve, reject) => { https .get( url, { headers: { "User-Agent": "finnhub-mcp-installer", }, }, (res) => { let data = ""; res.on("data", (chunk) => (data += chunk)); res.on("end", () => { if (res.statusCode === 200) { try { resolve(JSON.parse(data)); } catch (parseError) { reject( new Error( `Failed to parse JSON response: ${parseError.message}`, ), ); } } else if (res.statusCode === 404) { reject( new Error( `Repository not found or no releases exist (HTTP 404)`, ), ); } else { reject(new Error(`HTTP ${res.statusCode}: ${data}`)); } }); }, ) .on("error", reject); }); } function downloadFileWithCurl(url, filePath, spinner) { return new Promise((resolve, reject) => { const curl = spawn("curl", [ "-L", "-o", filePath, "--silent", "--show-error", url, ]); curl.on("close", (code) => { if (code === 0) { resolve(); } else { reject(new Error(`curl failed with code ${code}`)); } }); curl.on("error", reject); }); } function downloadFileWithNode(url, filePath, spinner) { return new Promise((resolve, reject) => { const file = fs.createWriteStream(filePath); let downloadedBytes = 0; let totalBytes = 0; function handleRequest(requestUrl, redirectCount = 0) { if (redirectCount > 5) { return reject(new Error("Too many redirects")); } https .get( requestUrl, { headers: { "User-Agent": "finnhub-mcp-installer", }, }, (response) => { // Handle redirects if (response.statusCode === 301 || response.statusCode === 302) { file.destroy(); return handleRequest( response.headers.location, redirectCount + 1, ); } if (response.statusCode === 200) { totalBytes = parseInt( response.headers["content-length"] || "0", 10, ); response.on("data", (chunk) => { downloadedBytes += chunk.length; if (totalBytes > 0 && spinner) { const percent = Math.round( (downloadedBytes / totalBytes) * 100, ); spinner.updateMessage( `Downloading... ${percent}% (${formatBytes(downloadedBytes)}/${formatBytes(totalBytes)})`, ); } }); response.pipe(file); file.on("finish", () => { file.close(); resolve(); }); file.on("error", (err) => { if (fs.existsSync(filePath)) { fs.unlinkSync(filePath); } reject(err); }); } else { file.destroy(); if (fs.existsSync(filePath)) { fs.unlinkSync(filePath); } reject( new Error( `HTTP ${response.statusCode}: ${response.statusMessage}`, ), ); } }, ) .on("error", (err) => { file.destroy(); if (fs.existsSync(filePath)) { fs.unlinkSync(filePath); } reject(err); }); } handleRequest(url); }); } function formatBytes(bytes) { if (bytes === 0) return "0 B"; const k = 1024; const sizes = ["B", "KB", "MB", "GB"]; const i = Math.floor(Math.log(bytes) / Math.log(k)); return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + " " + sizes[i]; } async function downloadFile(url, filePath, spinner) { try { // Try curl first (handles redirects well) spinner.updateMessage("Downloading with curl..."); await downloadFileWithCurl(url, filePath, spinner); } catch (error) { spinner.updateMessage("Curl failed, trying with Node.js..."); // Fallback to Node.js implementation await downloadFileWithNode(url, filePath, spinner); } } async function downloadRelease() { const fetchSpinner = new Spinner("Fetching latest release..."); try { fetchSpinner.start(); const release = await httpsGet(RELEASE_URL); fetchSpinner.stop("✅ Release information fetched"); // Add better validation if (!release) { console.error("\n❌ No release data received"); console.log(`Please check: https://github.com/${GITHUB_REPO}/releases`); process.exit(1); } if (!release.assets || !Array.isArray(release.assets)) { console.error("\n❌ Release has no assets or invalid assets data"); console.log(`Release info:`, JSON.stringify(release, null, 2)); process.exit(1); } if (release.assets.length === 0) { console.error("\n❌ Release exists but has no assets"); console.log(`Release: ${release.name || release.tag_name || "Unknown"}`); console.log(`Published: ${release.published_at || "Unknown"}`); console.log( "The release may still be building. Please wait a few minutes and try again.", ); console.log( `Check the Actions tab: https://github.com/${GITHUB_REPO}/actions`, ); process.exit(1); } console.log(`\n📦 Release: ${release.name || release.tag_name}`); console.log(`📅 Published: ${release.published_at}`); console.log("\nAvailable assets:"); release.assets.forEach((asset) => { console.log(` - ${asset.name} (${formatBytes(asset.size)})`); }); const platform = process.platform; const arch = process.arch; console.log(`\n🔍 Looking for: ${platform}-${arch}`); // Find the appropriate asset for the current platform let asset; if (platform === "win32") { // Look for Windows assets (win-x64, win-arm64) if (arch === "x64") { asset = release.assets.find((a) => a.name.includes("win-x64")); } else if (arch === "arm64") { asset = release.assets.find((a) => a.name.includes("win-arm64")); } // Fallback to any Windows asset if (!asset) { asset = release.assets.find((a) => a.name.toLowerCase().includes("win"), ); } } else if (platform === "darwin") { // macOS - look for osx assets (your workflow uses 'osx' not 'darwin') if (arch === "x64") { asset = release.assets.find((a) => a.name.includes("osx-x64")); } else if (arch === "arm64") { asset = release.assets.find((a) => a.name.includes("osx-arm64")); } // Fallback to any macOS asset if (!asset) { asset = release.assets.find((a) => { const name = a.name.toLowerCase(); return ( name.includes("osx") || name.includes("macos") || name.includes("darwin") ); }); } } else if (platform === "linux") { // Linux assets (linux-x64, linux-arm64) if (arch === "x64") { asset = release.assets.find((a) => a.name.includes("linux-x64")); } else if (arch === "arm64") { asset = release.assets.find((a) => a.name.includes("linux-arm64")); } // Fallback to any Linux asset if (!asset) { asset = release.assets.find((a) => a.name.toLowerCase().includes("linux"), ); } } if (!asset) { console.error(`\n❌ No compatible release found for ${platform}-${arch}`); console.log( "Available assets:", release.assets.map((a) => a.name), ); console.log("\nSupported platforms:"); console.log(" - Windows: win-x64, win-arm64"); console.log(" - macOS: osx-x64, osx-arm64"); console.log(" - Linux: linux-x64, linux-arm64"); console.log("\nYour platform details:"); console.log(` - Platform: ${platform}`); console.log(` - Architecture: ${arch}`); console.log( "Please check that your GitHub release includes a binary for your platform.", ); process.exit(1); } const assetName = asset.name; const downloadUrl = asset.browser_download_url; const assetSize = asset.size; console.log(`\n✅ Selected: ${assetName} (${formatBytes(assetSize)})`); const binDir = path.join(__dirname, "..", "bin"); if (!fs.existsSync(binDir)) { fs.mkdirSync(binDir, { recursive: true }); } const executableName = platform === "win32" ? "finnhub-mcp-server.exe" : "finnhub-mcp-server"; const executablePath = path.join(binDir, executableName); const downloadSpinner = new Spinner(`Downloading ${assetName}...`); downloadSpinner.start(); await downloadFile(downloadUrl, executablePath, downloadSpinner); downloadSpinner.stop(`✅ Downloaded ${assetName}`); // Make executable on Unix systems if (platform !== "win32") { fs.chmodSync(executablePath, "755"); } // Download appsettings.json const appsettings = release.assets.find( (a) => a.name === "appsettings.json", ); if (appsettings) { const configPath = path.join(binDir, "appsettings.json"); const configSpinner = new Spinner("Downloading appsettings.json..."); configSpinner.start(); await downloadFile( appsettings.browser_download_url, configPath, configSpinner, ); configSpinner.stop("✅ Downloaded appsettings.json"); } else { console.warn("⚠️ No appsettings.json found in release assets."); } console.log("\n🎉 Installation complete!"); console.log(`📍 Installed to: ${executablePath}`); // Test the executable console.log("\n🔧 Testing installation..."); try { const testCmd = platform === "win32" ? executablePath : executablePath; const testProcess = spawn(testCmd, ["--version"], { stdio: "pipe" }); let output = ""; testProcess.stdout.on("data", (data) => { output += data.toString(); }); testProcess.on("close", (code) => { if (code === 0 && output.trim()) { console.log(`✅ Installation verified: ${output.trim()}`); } else { console.log( "⚠️ Installation complete but version test failed (this may be normal)", ); } }); // Timeout after 5 seconds setTimeout(() => { testProcess.kill(); console.log("✅ Installation appears successful"); }, 5000); } catch (testError) { console.log("✅ Installation complete (version test skipped)"); } } catch (error) { if (fetchSpinner.interval) { fetchSpinner.stop("❌ Installation failed"); } console.error("\n❌ Installation failed:", error.message); console.log("\nTroubleshooting:"); console.log("1. ✅ Check internet connection"); console.log( `2. ✅ Verify repository exists: https://github.com/${GITHUB_REPO}`, ); console.log( `3. ✅ Check releases page: https://github.com/${GITHUB_REPO}/releases`, ); console.log( `4. ✅ Check build status: https://github.com/${GITHUB_REPO}/actions`, ); console.log("5. ✅ Ensure curl is available on your system"); if (error.message.includes("404")) { console.log("\n🔍 Additional debugging:"); console.log("- Repository may be private or doesn't exist"); console.log("- No releases may be published yet"); console.log("- Release workflow may have failed"); } process.exit(1); } } downloadRelease();