SingleStore MCP Server
- src
#!/usr/bin/env node
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import {
CallToolRequestSchema,
ErrorCode,
ListToolsRequestSchema,
McpError,
} from '@modelcontextprotocol/sdk/types.js';
import * as mysql from 'mysql2/promise';
import * as https from 'https';
import { randomBytes } from 'crypto';
// Define extended RowDataPacket interfaces for better type safety
interface TableRowDataPacket extends mysql.RowDataPacket {
TABLE_NAME: string;
}
interface ColumnRowDataPacket extends mysql.RowDataPacket {
Field: string;
Type: string;
Null: string;
Key: string;
Default?: string;
Extra?: string;
}
// Define ResultSetHeader interface - mysql2 doesn't export ResultSetHeader directly
interface ResultSetHeader {
affectedRows: number;
insertId: number;
warningStatus: number;
}
// Define TableOptions interface for better type safety
interface TableOptions {
is_reference?: boolean;
shard_key?: string[];
sort_key?: string[];
compression?: string;
auto_increment_start?: number;
}
interface ColumnGenerator {
type: 'sequence' | 'random' | 'values' | 'formula';
start?: number;
increment?: number;
end?: number;
values?: any[];
formula?: string;
}
interface Column {
name: string;
type: string;
nullable?: boolean;
default?: string | number | boolean;
auto_increment?: boolean;
}
interface CreateTableArguments {
table_name: string;
columns: Column[];
table_options?: TableOptions;
}
interface GenerateSyntheticDataArguments {
table: string;
count?: number;
batch_size?: number;
column_generators?: Record<string, ColumnGenerator>;
}
// Interface for SQL optimization recommendations
interface OptimizationRecommendation {
summary: {
total_runtime_ms: string;
compile_time_ms: string;
execution_time_ms: string;
bottlenecks: string[];
};
suggestions: Array<{
issue: string;
recommendation: string;
impact: 'high' | 'medium' | 'low';
}>;
optimizedQuery?: string;
}
// Fetch SingleStore CA bundle
async function fetchCABundle(): Promise<string> {
return new Promise((resolve, reject) => {
https.get('https://portal.singlestore.com/static/ca/singlestore_bundle.pem', (res) => {
let data = '';
res.on('data', (chunk) => data += chunk);
res.on('end', () => resolve(data));
res.on('error', (err) => reject(err));
}).on('error', (err) => reject(err));
});
}
class SingleStoreServer {
// Helper method to generate random values based on column type
private generateValueForColumn(columnType: string, isNullable: boolean): any {
// Handle NULL values for nullable columns (10% chance)
if (isNullable && Math.random() < 0.1) {
return null;
}
// Integer types
if (columnType.includes('int')) {
return Math.floor(Math.random() * 1000000);
}
// Decimal/numeric types
if (columnType.includes('decimal') || columnType.includes('numeric') ||
columnType.includes('float') || columnType.includes('double')) {
return parseFloat((Math.random() * 1000).toFixed(2));
}
// Date and time types
if (columnType.includes('date')) {
const start = new Date(2020, 0, 1);
const end = new Date();
return new Date(start.getTime() + Math.random() * (end.getTime() - start.getTime()))
.toISOString().split('T')[0];
}
if (columnType.includes('time')) {
const hours = Math.floor(Math.random() * 24);
const minutes = Math.floor(Math.random() * 60);
const seconds = Math.floor(Math.random() * 60);
return `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`;
}
if (columnType.includes('datetime') || columnType.includes('timestamp')) {
const start = new Date(2020, 0, 1);
const end = new Date();
return new Date(start.getTime() + Math.random() * (end.getTime() - start.getTime()))
.toISOString().slice(0, 19).replace('T', ' ');
}
// Boolean/bit types
if (columnType.includes('bool') || columnType.includes('bit(1)')) {
return Math.random() > 0.5 ? 1 : 0;
}
// Text types
if (columnType.includes('char') || columnType.includes('text')) {
// Extract the length for varchar/char if specified
let length = 10;
const matches = columnType.match(/\((\d+)\)/);
if (matches && matches[1]) {
length = Math.min(parseInt(matches[1], 10), 50); // Cap at 50 to avoid huge strings
}
return randomBytes(Math.ceil(length / 2))
.toString('hex')
.slice(0, length);
}
// JSON type
if (columnType.includes('json')) {
return JSON.stringify({
id: Math.floor(Math.random() * 1000),
name: `Item ${Math.floor(Math.random() * 100)}`,
value: Math.random() * 100
});
}
// Enum or set types
if (columnType.includes('enum') || columnType.includes('set')) {
// Extract values from enum/set definition: enum('value1','value2')
const matches = columnType.match(/'([^']+)'/g);
if (matches && matches.length > 0) {
const values = matches.map(m => m.replace(/'/g, ''));
return values[Math.floor(Math.random() * values.length)];
}
}
// Default fallback for unknown types
return 'unknown_type_data';
}
private async analyzeProfileData(profileData: any, originalQuery: string): Promise<OptimizationRecommendation> {
const result: OptimizationRecommendation = {
summary: {
total_runtime_ms: '0',
compile_time_ms: '0',
execution_time_ms: '0',
bottlenecks: []
},
suggestions: []
};
try {
// Parse the JSON string if it's not already an object
const profile = typeof profileData === 'string' ? JSON.parse(profileData) : profileData;
// Extract query_info
const queryInfo = profile.query_info || {};
// Set basic summary information
result.summary.total_runtime_ms = queryInfo.total_runtime_ms || '0';
// Extract compile time from compile_time_stats if available
if (queryInfo.compile_time_stats && queryInfo.compile_time_stats.total) {
result.summary.compile_time_ms = queryInfo.compile_time_stats.total;
// Calculate execution time (total - compile)
const totalTime = parseInt(result.summary.total_runtime_ms, 10);
const compileTime = parseInt(result.summary.compile_time_ms, 10);
result.summary.execution_time_ms = (totalTime - compileTime).toString();
}
// Analyze execution plan and operators
this.analyzeExecutionPlan(profile, result);
// Analyze table statistics and memory usage
this.analyzeMemoryAndStats(profile, result);
// Analyze network traffic and data movement
this.analyzeNetworkTraffic(profile, result);
// Analyze compilation time
this.analyzeCompilationTime(profile, result);
// Analyze partition skew
this.analyzePartitionSkew(profile, result);
// Identify bottlenecks
this.identifyBottlenecks(profile, result);
} catch (error) {
result.suggestions.push({
issue: 'Error analyzing profile data',
recommendation: 'The profile data could not be properly analyzed. Please check the query syntax.',
impact: 'high'
});
}
return result;
}
private analyzeExecutionPlan(profile: any, result: OptimizationRecommendation): void {
const textProfile = profile.query_info?.text_profile || '';
const lines = textProfile.split('\n');
// Look for full table scans
if (textProfile.includes('TableScan') && !textProfile.includes('IndexScan')) {
result.suggestions.push({
issue: 'Full table scan detected',
recommendation: 'Consider adding an index to the columns used in WHERE clauses to avoid scanning the entire table.',
impact: 'high'
});
}
// Check for hash joins with large tables
if (textProfile.includes('HashJoin')) {
const rowsMatch = textProfile.match(/actual_rows: (\d+)/);
if (rowsMatch && parseInt(rowsMatch[1], 10) > 10000) {
result.suggestions.push({
issue: 'Large hash join operation',
recommendation: 'For large tables, consider using appropriate indexes on join columns or partitioning data to reduce the size of hash tables.',
impact: 'medium'
});
}
}
}
private analyzeMemoryAndStats(profile: any, result: OptimizationRecommendation): void {
const textProfile = profile.query_info?.text_profile || '';
const lines = textProfile.split('\n');
// Check for high memory usage
for (const line of lines) {
const memoryMatch = line.match(/memory_usage: (\d+)/);
if (memoryMatch) {
const memoryUsage = parseInt(memoryMatch[1], 10);
if (memoryUsage > 100000) { // More than 100MB
result.suggestions.push({
issue: `High memory usage (${Math.round(memoryUsage / 1024)}MB)`,
recommendation: 'Consider adding appropriate indexes, breaking the query into smaller parts, or optimizing large in-memory operations.',
impact: 'high'
});
}
}
}
}
private analyzeNetworkTraffic(profile: any, result: OptimizationRecommendation): void {
const textProfile = profile.query_info?.text_profile || '';
const lines = textProfile.split('\n');
let totalNetworkTraffic = 0;
for (const line of lines) {
const trafficMatch = line.match(/network_traffic: (\d+(\.\d+)?)/);
if (trafficMatch) {
totalNetworkTraffic += parseFloat(trafficMatch[1]);
}
}
if (totalNetworkTraffic > 100000) { // More than 100MB
result.suggestions.push({
issue: `High network traffic (${Math.round(totalNetworkTraffic / 1024)}MB)`,
recommendation: 'Consider optimizing data movement between nodes by using appropriate shard keys and reducing the amount of data transferred.',
impact: 'high'
});
}
}
private analyzeCompilationTime(profile: any, result: OptimizationRecommendation): void {
const compileTimeStats = profile.query_info?.compile_time_stats || {};
const totalCompileTime = parseInt(compileTimeStats.total || '0', 10);
const totalRuntime = parseInt(profile.query_info?.total_runtime_ms || '0', 10);
if (totalRuntime > 0 && totalCompileTime > 0 && (totalCompileTime / totalRuntime) > 0.2) {
result.suggestions.push({
issue: `High compilation time (${totalCompileTime}ms, ${Math.round((totalCompileTime / totalRuntime) * 100)}% of total runtime)`,
recommendation: 'Consider parameterizing your queries for plan reuse or adjusting compilation-related variables.',
impact: 'medium'
});
}
}
private analyzePartitionSkew(profile: any, result: OptimizationRecommendation): void {
const textProfile = profile.query_info?.text_profile || '';
const lines = textProfile.split('\n');
for (const line of lines) {
const skewMatch = line.match(/max:(\d+) at partition_(\d+), average: (\d+)/);
if (skewMatch) {
const max = parseInt(skewMatch[1], 10);
const partition = skewMatch[2];
const avg = parseInt(skewMatch[3], 10);
if (max > avg * 3) {
result.suggestions.push({
issue: `Significant data skew detected in partition ${partition}`,
recommendation: 'Review your shard key choice and data distribution strategy to achieve more uniform load across partitions.',
impact: 'high'
});
}
}
}
}
private identifyBottlenecks(profile: any, result: OptimizationRecommendation): void {
const textProfile = profile.query_info?.text_profile || '';
const lines = textProfile.split('\n');
const execTimes: Array<{operation: string, time: number}> = [];
for (const line of lines) {
const execTimeMatch = line.match(/exec_time: (\d+)ms/);
if (execTimeMatch) {
const time = parseInt(execTimeMatch[1], 10);
const operationMatch = line.match(/^(\w+)/);
const operation = operationMatch ? operationMatch[1] : 'Unknown';
execTimes.push({ operation, time });
}
}
execTimes.sort((a, b) => b.time - a.time);
result.summary.bottlenecks = execTimes
.slice(0, 3)
.filter(item => item.time > 0)
.map(item => `${item.operation} (${item.time}ms)`);
}
private server: Server;
private connection: mysql.Connection | null = null;
private caBundle: string | null = null;
constructor() {
this.server = new Server(
{
name: 'singlestore-server',
version: '0.1.0',
},
{
capabilities: {
tools: {},
},
}
);
this.setupToolHandlers();
this.server.onerror = (error) => console.error('[MCP Error]', error);
process.on('SIGINT', async () => {
await this.cleanup();
process.exit(0);
});
}
private async ensureConnection() {
if (!this.connection) {
try {
if (!this.caBundle) {
this.caBundle = await fetchCABundle();
}
const config = {
host: process.env.SINGLESTORE_HOST,
user: process.env.SINGLESTORE_USER,
password: process.env.SINGLESTORE_PASSWORD,
database: process.env.SINGLESTORE_DATABASE,
port: parseInt(process.env.SINGLESTORE_PORT || '3306'),
ssl: {
ca: this.caBundle
}
};
this.connection = await mysql.createConnection(config);
} catch (error: unknown) {
const err = error as Error;
throw new McpError(
ErrorCode.InternalError,
`Database connection error: ${err.message}`
);
}
}
return this.connection;
}
private async cleanup() {
if (this.connection) {
await this.connection.end();
}
await this.server.close();
}
private setupToolHandlers() {
this.server.setRequestHandler(ListToolsRequestSchema, async () => ({
tools: [
{
name: 'generate_er_diagram',
description: 'Generate a Mermaid ER diagram of the database schema',
inputSchema: {
type: 'object',
properties: {},
required: [],
},
},
{
name: 'list_tables',
description: 'List all tables in the database',
inputSchema: {
type: 'object',
properties: {},
required: [],
},
},
{
name: 'query_table',
description: 'Execute a query on a table',
inputSchema: {
type: 'object',
properties: {
query: {
type: 'string',
description: 'SQL query to execute',
},
},
required: ['query'],
},
},
{
name: 'describe_table',
description: 'Get detailed information about a table',
inputSchema: {
type: 'object',
properties: {
table: {
type: 'string',
description: 'Name of the table to describe',
},
},
required: ['table'],
},
},
{
name: 'run_read_query',
description: 'Execute a read-only (SELECT) query on the database',
inputSchema: {
type: 'object',
properties: {
query: {
type: 'string',
description: 'SQL SELECT query to execute',
},
},
required: ['query'],
},
},
{
name: 'create_table',
description: 'Create a new table in the database with specified columns and constraints',
inputSchema: {
type: 'object',
properties: {
table_name: {
type: 'string',
description: 'Name of the table to create'
},
columns: {
type: 'array',
items: {
type: 'object',
properties: {
name: {
type: 'string',
description: 'Column name'
},
type: {
type: 'string',
description: 'Data type (e.g., INT, VARCHAR(255), etc.)'
},
nullable: {
type: 'boolean',
description: 'Whether the column can be NULL'
},
default: {
type: 'string',
description: 'Default value for the column'
},
auto_increment: {
type: 'boolean',
description: 'Whether the column should auto increment'
}
},
required: ['name', 'type']
},
description: 'List of columns to create'
},
table_options: {
type: 'object',
properties: {
shard_key: {
type: 'array',
items: { type: 'string' },
description: 'Columns to use as shard key'
},
sort_key: {
type: 'array',
items: { type: 'string' },
description: 'Columns to use as sort key'
},
is_reference: {
type: 'boolean',
description: 'Whether this is a reference table'
},
compression: {
type: 'string',
enum: ['SPARSE'],
description: 'Table compression type'
},
auto_increment_start: {
type: 'number',
description: 'Starting value for auto increment columns'
}
}
}
},
required: ['table_name', 'columns']
}
},
{
name: 'generate_synthetic_data',
description: 'Generate and insert synthetic data into an existing table',
inputSchema: {
type: 'object',
properties: {
table: {
type: 'string',
description: 'Name of the table to insert data into'
},
count: {
type: 'number',
description: 'Number of rows to generate and insert',
default: 100
},
batch_size: {
type: 'number',
description: 'Number of rows to insert in each batch',
default: 1000
},
column_generators: {
type: 'object',
description: 'Custom generators for specific columns (optional)',
additionalProperties: {
type: 'object',
properties: {
type: {
type: 'string',
enum: ['sequence', 'random', 'values', 'formula'],
description: 'Type of generator to use'
},
start: {
type: 'number',
description: 'Starting value for sequence generator'
},
end: {
type: 'number',
description: 'Ending value for random number generator'
},
values: {
type: 'array',
items: { type: 'string' },
description: 'Array of values to choose from for values generator'
},
formula: {
type: 'string',
description: 'SQL expression for formula generator'
}
}
}
}
},
required: ['table']
}
},
{
name: 'optimize_sql',
description: 'Analyze a SQL query using PROFILE and provide optimization recommendations',
inputSchema: {
type: 'object',
properties: {
query: {
type: 'string',
description: 'SQL query to analyze and optimize'
}
},
required: ['query']
}
}
],
}));
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
const conn = await this.ensureConnection();
switch (request.params.name) {
case 'generate_er_diagram': {
try {
// Get all tables
const [tables] = await conn.query(
'SELECT TABLE_NAME FROM information_schema.TABLES WHERE TABLE_SCHEMA = DATABASE()'
) as [TableRowDataPacket[], mysql.FieldPacket[]];
// Get foreign key relationships
const [relationships] = await conn.query(
`SELECT
TABLE_NAME,
COLUMN_NAME,
REFERENCED_TABLE_NAME,
REFERENCED_COLUMN_NAME
FROM information_schema.KEY_COLUMN_USAGE
WHERE
TABLE_SCHEMA = DATABASE()
AND REFERENCED_TABLE_NAME IS NOT NULL`
) as [mysql.RowDataPacket[], mysql.FieldPacket[]];
// Start building Mermaid diagram
let mermaidDiagram = 'erDiagram\n';
// Add tables and their columns
for (const table of tables) {
const [columns] = await conn.query(
'DESCRIBE ??',
[table.TABLE_NAME]
) as [ColumnRowDataPacket[], mysql.FieldPacket[]];
mermaidDiagram += `\n ${table.TABLE_NAME} {\n`;
for (const column of columns) {
const fieldType = column.Type.split('(')[0];
mermaidDiagram += ` ${fieldType} ${column.Field}${column.Key === 'PRI' ? ' PK' : ''}\n`;
}
mermaidDiagram += ' }\n';
}
// Add relationships
mermaidDiagram += `
Documents ||--o{ Document_Embeddings : "has embeddings"
Documents ||--o{ Chunk_Metadata : "is chunked into"
Documents ||--o{ ProcessingStatus : "has status"
Document_Embeddings ||--o| Chunk_Metadata : "belongs to chunk"
Entities ||--o{ Relationships : "has relationships"
Entities ||--o{ Relationships : "is referenced by"
Documents ||--o{ Relationships : "contains"`;
return {
content: [
{
type: 'text',
text: mermaidDiagram,
},
],
};
} catch (error: unknown) {
const err = error as Error;
throw new McpError(
ErrorCode.InternalError,
`ER diagram generation error: ${err.message}`
);
}
}
case 'list_tables': {
const [rows] = await conn.query('SHOW TABLES') as [mysql.RowDataPacket[], mysql.FieldPacket[]];
return {
content: [
{
type: 'text',
text: JSON.stringify(rows, null, 2),
},
],
};
}
case 'query_table': {
if (!request.params.arguments || typeof request.params.arguments.query !== 'string') {
throw new McpError(
ErrorCode.InvalidParams,
'Query parameter must be a string'
);
}
try {
const [rows] = await conn.query(request.params.arguments.query) as [mysql.RowDataPacket[], mysql.FieldPacket[]];
return {
content: [
{
type: 'text',
text: JSON.stringify(rows, null, 2),
},
],
};
} catch (error: unknown) {
const err = error as Error;
throw new McpError(
ErrorCode.InternalError,
`Query error: ${err.message}`
);
}
}
case 'describe_table': {
if (!request.params.arguments || typeof request.params.arguments.table !== 'string') {
throw new McpError(
ErrorCode.InvalidParams,
'Table parameter must be a string'
);
}
try {
// Get table schema
const [columns] = await conn.query(
'DESCRIBE ??',
[request.params.arguments.table]
) as [ColumnRowDataPacket[], mysql.FieldPacket[]];
// Get basic table statistics
const [stats] = await conn.query(
'SELECT COUNT(*) as total_rows FROM ??',
[request.params.arguments.table]
) as [mysql.RowDataPacket[], mysql.FieldPacket[]];
// Sample some data
const [sample] = await conn.query(
'SELECT * FROM ?? LIMIT 5',
[request.params.arguments.table]
) as [mysql.RowDataPacket[], mysql.FieldPacket[]];
const statistics = stats[0] as { total_rows: number };
return {
content: [
{
type: 'text',
text: JSON.stringify(
{
schema: columns,
statistics,
sample_data: sample,
},
null,
2
),
},
],
};
} catch (error: unknown) {
const err = error as Error;
throw new McpError(
ErrorCode.InternalError,
`Table description error: ${err.message}`
);
}
}
case 'create_table': {
if (!request.params.arguments || !request.params.arguments.table_name || !Array.isArray(request.params.arguments.columns)) {
throw new McpError(
ErrorCode.InvalidParams,
'Invalid parameters for create_table'
);
}
try {
// First convert to unknown, then to our expected type
const args = request.params.arguments as unknown as CreateTableArguments;
const { table_name, columns, table_options = {} as TableOptions } = args;
// Start building the CREATE TABLE statement
let sql = `CREATE ${(table_options as TableOptions).is_reference ? 'REFERENCE ' : ''}TABLE ${table_name} (\n`;
// Add columns
const columnDefs = columns.map(col => {
let def = ` ${col.name} ${col.type}`;
if (col.nullable === false) def += ' NOT NULL';
if (col.default !== undefined) def += ` DEFAULT ${col.default}`;
if (col.auto_increment) def += ' AUTO_INCREMENT';
return def;
});
// Add primary key if auto_increment is used
const autoIncrementCol = columns.find(col => col.auto_increment);
if (autoIncrementCol) {
columnDefs.push(` PRIMARY KEY (${autoIncrementCol.name})`);
}
// Add shard key if specified
if ((table_options as TableOptions).shard_key?.length) {
columnDefs.push(` SHARD KEY (${(table_options as TableOptions).shard_key.join(', ')})`);
}
// Add sort key if specified
if ((table_options as TableOptions).sort_key?.length) {
columnDefs.push(` SORT KEY (${(table_options as TableOptions).sort_key.join(', ')})`);
}
sql += columnDefs.join(',\n');
sql += '\n)';
// Add table options
const tableOptions = [];
if ((table_options as TableOptions).compression === 'SPARSE') {
tableOptions.push('COMPRESSION = SPARSE');
}
if ((table_options as TableOptions).auto_increment_start) {
tableOptions.push(`AUTO_INCREMENT = ${(table_options as TableOptions).auto_increment_start}`);
}
if (tableOptions.length) {
sql += ' ' + tableOptions.join(' ');
}
// Execute the CREATE TABLE statement
await conn.query(sql);
return {
content: [
{
type: 'text',
text: `Table ${table_name} created successfully`
}
]
};
} catch (error: unknown) {
const err = error as Error;
throw new McpError(
ErrorCode.InternalError,
`Failed to create table: ${err.message}`
);
}
}
case 'run_read_query': {
if (!request.params.arguments || typeof request.params.arguments.query !== 'string') {
throw new McpError(
ErrorCode.InvalidParams,
'Query parameter must be a string'
);
}
const query = request.params.arguments.query.trim().toLowerCase();
if (!query.startsWith('select ')) {
throw new McpError(
ErrorCode.InvalidParams,
'Only SELECT queries are allowed for this tool'
);
}
try {
const [rows] = await conn.query(request.params.arguments.query) as [mysql.RowDataPacket[], mysql.FieldPacket[]];
return {
content: [
{
type: 'text',
text: JSON.stringify(rows, null, 2),
},
],
};
} catch (error: unknown) {
const err = error as Error;
throw new McpError(
ErrorCode.InternalError,
`Query error: ${err.message}`
);
}
}
case 'generate_synthetic_data': {
if (!request.params.arguments || typeof request.params.arguments.table !== 'string') {
throw new McpError(
ErrorCode.InvalidParams,
'Table parameter must be a string'
);
}
// First convert to unknown, then to our expected type
const args = request.params.arguments as unknown as GenerateSyntheticDataArguments;
const table = args.table;
const count = Number(args.count || 100);
const batchSize = Math.min(Number(args.batch_size || 1000), 5000);
const columnGenerators = args.column_generators || {};
try {
// Get table schema to understand column types
const [columns] = await conn.query(
'DESCRIBE ??',
[table]
) as [ColumnRowDataPacket[], mysql.FieldPacket[]];
if (columns.length === 0) {
throw new McpError(
ErrorCode.InvalidParams,
`Table ${table} does not exist or has no columns`
);
}
// Identify auto-increment columns to exclude from insert
const autoIncrementColumns = new Set(
columns
.filter(col => col.Extra?.includes('auto_increment'))
.map(col => col.Field)
);
// Filter out auto-increment columns
const insertableColumns = columns.filter(col => !autoIncrementColumns.has(col.Field));
if (insertableColumns.length === 0) {
throw new McpError(
ErrorCode.InvalidParams,
`Table ${table} has only auto-increment columns, cannot insert data`
);
}
// Generate data in batches
let totalInserted = 0;
const startTime = Date.now();
for (let batchStart = 0; batchStart < count; batchStart += batchSize) {
const batchCount = Math.min(batchSize, count - batchStart);
const rows = [];
// Generate rows for this batch
for (let i = 0; i < batchCount; i++) {
const row: Record<string, any> = {};
for (const column of insertableColumns) {
const columnName = column.Field;
const columnType = column.Type.toLowerCase();
const isNullable = column.Null === 'YES';
// Check if we have a custom generator for this column
if (columnGenerators[columnName]) {
const generator = columnGenerators[columnName] as ColumnGenerator;
switch (generator.type) {
case 'sequence':
row[columnName] = (generator.start || 0) + batchStart + i;
break;
case 'random':
if (columnType.includes('int')) {
const min = generator.start || 0;
const max = generator.end || 1000000;
row[columnName] = Math.floor(Math.random() * (max - min + 1)) + min;
} else if (columnType.includes('float') || columnType.includes('double') || columnType.includes('decimal')) {
const min = generator.start || 0;
const max = generator.end || 1000;
row[columnName] = Math.random() * (max - min) + min;
}
break;
case 'values':
if (Array.isArray(generator.values) && generator.values.length > 0) {
const index = Math.floor(Math.random() * generator.values.length);
row[columnName] = generator.values[index];
}
break;
case 'formula':
// Formulas are applied during the INSERT statement
row[columnName] = null;
break;
default:
// Fall back to default generation
row[columnName] = this.generateValueForColumn(columnType, isNullable);
}
} else {
// Use default generation based on column type
row[columnName] = this.generateValueForColumn(columnType, isNullable);
}
}
rows.push(row);
}
// Prepare column names for the INSERT statement
const columnNames = insertableColumns.map(col => col.Field);
// Prepare placeholders for the VALUES clause
const placeholders = rows.map(() =>
`(${columnNames.map(() => '?').join(', ')})`
).join(', ');
// Flatten the values for the query
const values = rows.flatMap(row =>
columnNames.map(col => row[col])
);
// Execute the INSERT statement
const [result] = await conn.query(
`INSERT INTO ${table} (${columnNames.join(', ')}) VALUES ${placeholders}`,
values
) as [ResultSetHeader, mysql.FieldPacket[]];
totalInserted += result.affectedRows;
}
const duration = (Date.now() - startTime) / 1000;
return {
content: [
{
type: 'text',
text: JSON.stringify({
success: true,
table,
rows_inserted: totalInserted,
duration_seconds: duration,
rows_per_second: Math.round(totalInserted / duration)
}, null, 2)
}
]
};
} catch (error: unknown) {
const err = error as Error;
throw new McpError(
ErrorCode.InternalError,
`Failed to generate synthetic data: ${err.message}`
);
}
}
case 'optimize_sql': {
if (!request.params.arguments || typeof request.params.arguments.query !== 'string') {
throw new McpError(
ErrorCode.InvalidParams,
'Query parameter must be a string'
);
}
const query = request.params.arguments.query.trim();
try {
// Step 1: Run PROFILE on the query
await conn.query('SET profile_for_debug = ON');
await conn.query(`PROFILE ${query}`);
// Step 2: Get the profile data in JSON format
const [profileResult] = await conn.query('SHOW PROFILE JSON') as [mysql.RowDataPacket[], mysql.FieldPacket[]];
// Step 3: Analyze the profile data and generate recommendations
const recommendations = await this.analyzeProfileData(profileResult[0], query);
// Step 4: Return the analysis and recommendations
return {
content: [
{
type: 'text',
text: JSON.stringify({
original_query: query,
profile_summary: recommendations.summary,
recommendations: recommendations.suggestions,
optimized_query: recommendations.optimizedQuery || query
}, null, 2)
}
]
};
} catch (error: unknown) {
const err = error as Error;
throw new McpError(
ErrorCode.InternalError,
`Query optimization error: ${err.message}`
);
}
}
default:
throw new McpError(
ErrorCode.MethodNotFound,
`Unknown tool: ${request.params.name}`
);
}
});
}
async run() {
const transport = new StdioServerTransport();
await this.server.connect(transport);
console.error('SingleStore MCP server running on stdio');
}
}
const server = new SingleStoreServer();
server.run().catch(console.error);