hubble-mcp-tool
Version:
MCP server for Hubble
110 lines (109 loc) • 4.08 kB
JavaScript
import { ErrorCode, McpError } from "@modelcontextprotocol/sdk/types.js";
import axios from "axios";
import fs from "fs/promises";
import { QUICKCHART_BASE_URL } from "../config/constants.js";
import { VALID_CHART_TYPES } from "../types/index.js";
import { ErrorHandler } from "../utils/errors.js";
/**
* Service for handling chart generation and operations
*/
export class ChartService {
/**
* Validates that the chart type is supported
*/
static validateChartType(type) {
if (!VALID_CHART_TYPES.includes(type)) {
throw new McpError(ErrorCode.InvalidParams, `Invalid chart type. Must be one of: ${VALID_CHART_TYPES.join(", ")}`);
}
}
/**
* Generates a chart configuration object from input arguments
*/
static generateChartConfig(args) {
const { type, labels, datasets, title, options = {} } = args;
this.validateChartType(type);
const config = {
type: type,
data: {
labels: labels || [],
datasets: datasets.map((dataset) => {
// Extract known properties
const { label, data, backgroundColor, borderColor, borderWidth, additionalConfig = {} } = dataset;
// Create dataset with known properties
const chartDataset = {
label: label || "",
data: data,
backgroundColor: backgroundColor,
borderColor: borderColor,
};
// Add borderWidth if provided
if (borderWidth !== undefined) {
chartDataset.borderWidth = borderWidth;
}
// Add any additional properties from additionalConfig
return {
...chartDataset,
...additionalConfig,
};
}),
},
options: {
...options,
...(title && {
title: {
display: true,
text: title,
},
}),
},
};
// Special handling for specific chart types
switch (type) {
case "radialGauge":
case "speedometer":
if (!datasets?.[0]?.data?.[0]) {
throw ErrorHandler.createInvalidParamsError(`${type} requires a single numeric value`);
}
config.options = {
...config.options,
plugins: {
datalabels: {
display: true,
formatter: (value) => value,
},
},
};
break;
case "scatter":
case "bubble":
datasets.forEach((dataset) => {
if (!Array.isArray(dataset.data[0])) {
throw ErrorHandler.createInvalidParamsError(`${type} requires data points in [x, y${type === "bubble" ? ", r" : ""}] format`);
}
});
break;
}
return config;
}
/**
* Generates a QuickChart URL from a chart configuration
*/
static async generateChartUrl(config) {
const encodedConfig = encodeURIComponent(JSON.stringify(config));
return `${QUICKCHART_BASE_URL}&c=${encodedConfig}`;
}
/**
* Downloads a chart image to a local file
*/
static async downloadChart(config, outputPath) {
try {
const url = await this.generateChartUrl(config);
const response = await axios.get(url, { responseType: "arraybuffer" });
await fs.writeFile(outputPath, response.data);
return outputPath;
}
catch (error) {
throw ErrorHandler.handleError(error, "Failed to download chart");
}
}
}