finnhub-mcp
Version:
A Model Context Protocol server for Finnhub financial data
425 lines (378 loc) • 13 kB
JavaScript
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();