Claude MCP Data Explorer
by tofunori
Verified
import { getAllDataFrames } from './data-loader.js';
import * as util from 'util';
// Interface for runScript arguments
interface RunScriptArgs {
script: string;
}
/**
* Safely executes JavaScript code with access to loaded data
*/
export async function runScript(args: RunScriptArgs): Promise<{ type: string, text: string }[]> {
const { script } = args;
if (!script) {
return [{ type: 'text', text: 'Error: script is required' }];
}
// Capture console output
let consoleOutput: string[] = [];
const originalConsoleLog = console.log;
const originalConsoleError = console.error;
const originalConsoleWarn = console.warn;
console.log = (...args) => {
consoleOutput.push(args.map(formatOutput).join(' '));
};
console.error = (...args) => {
consoleOutput.push(`ERROR: ${args.map(formatOutput).join(' ')}`);
};
console.warn = (...args) => {
consoleOutput.push(`WARNING: ${args.map(formatOutput).join(' ')}`);
};
try {
// Create a context with available libraries and data
const contextObject: Record<string, any> = {
// Make loaded data frames available to the script
...getAllDataFrames(),
// Add utilities
require: (moduleName: string) => {
try {
// Only allow specific modules for security
const allowedModules: { [key: string]: any } = {
'simple-statistics': require('simple-statistics'),
'papaparse': require('papaparse'),
};
if (moduleName in allowedModules) {
return allowedModules[moduleName];
} else {
throw new Error(`Module not allowed: ${moduleName}`);
}
} catch (error) {
throw new Error(`Error requiring module '${moduleName}': ${error}`);
}
},
// Add global variables and functions
console: {
log: console.log,
error: console.error,
warn: console.warn
},
Math,
Date,
JSON,
Object,
Array,
String,
Number,
Boolean,
Map,
Set,
Promise,
Error,
};
// Add Data Frame helper methods
for (const [name, data] of Object.entries(getAllDataFrames())) {
contextObject[name] = data;
// Add common DataFrame operations
if (Array.isArray(data) && data.length > 0) {
// Use method binding to ensure 'this' is preserved
contextObject[`${name}_describe`] = () => describeDataFrame(data);
contextObject[`${name}_columns`] = () => Object.keys(data[0] || {});
contextObject[`${name}_head`] = (n = 5) => data.slice(0, n);
contextObject[`${name}_tail`] = (n = 5) => data.slice(-n);
contextObject[`${name}_filter`] = (fn: (row: any) => boolean) => data.filter(fn);
contextObject[`${name}_map`] = (fn: (row: any) => any) => data.map(fn);
contextObject[`${name}_groupBy`] = (key: string) => {
const groups: Record<string, any[]> = {};
for (const row of data) {
const groupKey = String(row[key]);
if (!groups[groupKey]) {
groups[groupKey] = [];
}
groups[groupKey].push(row);
}
return groups;
};
}
}
// Create a secure function for execution
const AsyncFunction = Object.getPrototypeOf(async function(){}).constructor;
const secureFunction = new AsyncFunction(
...Object.keys(contextObject),
`"use strict";
try {
return (async () => {
${script}
return "Script executed successfully";
})();
} catch (error) {
throw error;
}`
);
// Execute the function with context
const result = await secureFunction(...Object.values(contextObject));
// Add result to console output if we got something back
if (result !== "Script executed successfully") {
consoleOutput.push(formatOutput(result));
}
// Clean up and return the result
return [{ type: 'text', text: consoleOutput.join('\n') }];
} catch (error: any) {
return [{ type: 'text', text: `Error executing script: ${error.message}\n\nConsole output:\n${consoleOutput.join('\n')}` }];
} finally {
// Restore original console methods
console.log = originalConsoleLog;
console.error = originalConsoleError;
console.warn = originalConsoleWarn;
}
}
/**
* Format any output value into a readable string
*/
function formatOutput(value: any): string {
try {
if (value === undefined) return 'undefined';
if (value === null) return 'null';
if (typeof value === 'object') {
return util.inspect(value, { depth: 2, colors: false, maxArrayLength: 100 });
}
return String(value);
} catch (error) {
return `[Error formatting output: ${error}]`;
}
}
/**
* Generate descriptive statistics for a DataFrame
*/
function describeDataFrame(data: any[]): Record<string, any> {
if (!data || data.length === 0) {
return { error: 'Empty dataset' };
}
const columns = Object.keys(data[0]);
const result: Record<string, any> = {};
for (const column of columns) {
const values = data.map(row => row[column]).filter(v => v !== null && v !== undefined);
if (values.length === 0) {
result[column] = { count: 0, missing: data.length };
continue;
}
if (typeof values[0] === 'number') {
try {
const numericValues = values.filter(v => typeof v === 'number' && !isNaN(v)) as number[];
if (numericValues.length === 0) continue;
result[column] = {
count: numericValues.length,
missing: data.length - numericValues.length,
min: Math.min(...numericValues),
max: Math.max(...numericValues),
mean: numericValues.reduce((a, b) => a + b, 0) / numericValues.length,
// More advanced stats could be added here
};
} catch (error) {
result[column] = { error: `Error calculating stats: ${error}` };
}
} else {
const uniqueValues = new Set(values);
result[column] = {
count: values.length,
missing: data.length - values.length,
unique: uniqueValues.size,
type: typeof values[0]
};
}
}
return result;
}