Refactor code for extensability

Refactor code for extensability
This commit is contained in:
Karthik KK
2025-04-13 20:35:08 +12:00
parent 27e0aac8de
commit 2de75b8fa6
13 changed files with 770 additions and 8 deletions

BIN
.DS_Store vendored

Binary file not shown.

View File

@@ -16,6 +16,7 @@ echo "Building TypeScript..."
# Make JavaScript files executable
echo "Making JavaScript files executable..."
chmod +x dist/*.js
chmod +x dist/src/index.js
echo "Build completed successfully!"
echo "You can now run the server with: node dist/src/index.js /path/to/your/database.db"

View File

@@ -8,18 +8,19 @@
"bugs": "https://github.com/executeautomation/database-server/issues",
"type": "module",
"bin": {
"ea-database-server": "dist/index.js"
"ea-database-server": "dist/src/index.js"
},
"files": [
"dist"
],
"scripts": {
"build": "tsc && shx chmod +x dist/*.js",
"build": "tsc && shx chmod +x dist/src/index.js",
"prepare": "npm run build",
"watch": "tsc --watch",
"start": "node dist/index.js",
"dev": "tsc && node dist/index.js",
"example": "node examples/example.js"
"start": "node dist/src/index.js",
"dev": "tsc && node dist/src/index.js",
"example": "node examples/example.js",
"clean": "rimraf dist"
},
"dependencies": {
"@modelcontextprotocol/sdk": "1.9.0",
@@ -27,6 +28,7 @@
},
"devDependencies": {
"@types/sqlite3": "5.1.0",
"rimraf": "^5.0.5",
"shx": "0.4.0",
"typescript": "5.8.3"
}

BIN
src/.DS_Store vendored Normal file

Binary file not shown.

101
src/db/index.ts Normal file
View File

@@ -0,0 +1,101 @@
import sqlite3 from "sqlite3";
let db: sqlite3.Database;
let databasePath: string;
/**
* Initialize the SQLite database connection
* @param dbPath Path to the SQLite database file
*/
export function initDatabase(dbPath: string): Promise<void> {
databasePath = dbPath;
return new Promise((resolve, reject) => {
db = new sqlite3.Database(dbPath, (err) => {
if (err) {
reject(err);
} else {
resolve();
}
});
});
}
/**
* Execute a SQL query and get all results
* @param query SQL query to execute
* @param params Query parameters
* @returns Promise with query results
*/
export function dbAll(query: string, params: any[] = []): Promise<any[]> {
return new Promise((resolve, reject) => {
db.all(query, params, (err: Error | null, rows: any[]) => {
if (err) {
reject(err);
} else {
resolve(rows);
}
});
});
}
/**
* Execute a SQL query that modifies data
* @param query SQL query to execute
* @param params Query parameters
* @returns Promise with result info
*/
export function dbRun(query: string, params: any[] = []): Promise<{ changes: number, lastID: number }> {
return new Promise((resolve, reject) => {
db.run(query, params, function(this: sqlite3.RunResult, err: Error | null) {
if (err) {
reject(err);
} else {
resolve({ changes: this.changes, lastID: this.lastID });
}
});
});
}
/**
* Execute multiple SQL statements
* @param query SQL statements to execute
* @returns Promise that resolves when execution completes
*/
export function dbExec(query: string): Promise<void> {
return new Promise((resolve, reject) => {
db.exec(query, (err: Error | null) => {
if (err) {
reject(err);
} else {
resolve();
}
});
});
}
/**
* Close the database connection
*/
export function closeDatabase(): Promise<void> {
return new Promise((resolve, reject) => {
if (!db) {
resolve();
return;
}
db.close((err: Error | null) => {
if (err) {
reject(err);
} else {
resolve();
}
});
});
}
/**
* Get the current database path
*/
export function getDatabasePath(): string {
return databasePath;
}

View File

@@ -0,0 +1,66 @@
import { dbAll } from '../db/index.js';
import { getDatabasePath } from '../db/index.js';
/**
* Handle listing resources request
* @returns List of available resources
*/
export async function handleListResources() {
try {
const databasePath = getDatabasePath();
const resourceBaseUrl = new URL(`sqlite:///${databasePath}`);
const SCHEMA_PATH = "schema";
const result = await dbAll(
"SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'"
);
return {
resources: result.map((row: any) => ({
uri: new URL(`${row.name}/${SCHEMA_PATH}`, resourceBaseUrl).href,
mimeType: "application/json",
name: `"${row.name}" database schema`,
})),
};
} catch (error: any) {
throw new Error(`Error listing resources: ${error.message}`);
}
}
/**
* Handle reading a specific resource
* @param uri URI of the resource to read
* @returns Resource contents
*/
export async function handleReadResource(uri: string) {
try {
const resourceUrl = new URL(uri);
const SCHEMA_PATH = "schema";
const pathComponents = resourceUrl.pathname.split("/");
const schema = pathComponents.pop();
const tableName = pathComponents.pop();
if (schema !== SCHEMA_PATH) {
throw new Error("Invalid resource URI");
}
// Query to get column information for a table
const result = await dbAll(`PRAGMA table_info("${tableName}")`);
return {
contents: [
{
uri,
mimeType: "application/json",
text: JSON.stringify(result.map((column: any) => ({
column_name: column.name,
data_type: column.type
})), null, 2),
},
],
};
} catch (error: any) {
throw new Error(`Error reading resource: ${error.message}`);
}
}

View File

@@ -0,0 +1,170 @@
import { formatErrorResponse } from '../utils/formatUtils.js';
// Import all tool implementations
import { readQuery, writeQuery, exportQuery } from '../tools/queryTools.js';
import { createTable, alterTable, dropTable, listTables, describeTable } from '../tools/schemaTools.js';
import { appendInsight, listInsights } from '../tools/insightTools.js';
/**
* Handle listing available tools
* @returns List of available tools
*/
export function handleListTools() {
return {
tools: [
{
name: "read_query",
description: "Execute SELECT queries to read data from the database",
inputSchema: {
type: "object",
properties: {
query: { type: "string" },
},
required: ["query"],
},
},
{
name: "write_query",
description: "Execute INSERT, UPDATE, or DELETE queries",
inputSchema: {
type: "object",
properties: {
query: { type: "string" },
},
required: ["query"],
},
},
{
name: "create_table",
description: "Create new tables in the database",
inputSchema: {
type: "object",
properties: {
query: { type: "string" },
},
required: ["query"],
},
},
{
name: "alter_table",
description: "Modify existing table schema (add columns, rename tables, etc.)",
inputSchema: {
type: "object",
properties: {
query: { type: "string" },
},
required: ["query"],
},
},
{
name: "drop_table",
description: "Remove a table from the database with safety confirmation",
inputSchema: {
type: "object",
properties: {
table_name: { type: "string" },
confirm: { type: "boolean" },
},
required: ["table_name", "confirm"],
},
},
{
name: "export_query",
description: "Export query results to various formats (CSV, JSON)",
inputSchema: {
type: "object",
properties: {
query: { type: "string" },
format: { type: "string", enum: ["csv", "json"] },
},
required: ["query", "format"],
},
},
{
name: "list_tables",
description: "Get a list of all tables in the database",
inputSchema: {
type: "object",
properties: {},
},
},
{
name: "describe_table",
description: "View schema information for a specific table",
inputSchema: {
type: "object",
properties: {
table_name: { type: "string" },
},
required: ["table_name"],
},
},
{
name: "append_insight",
description: "Add a business insight to the memo",
inputSchema: {
type: "object",
properties: {
insight: { type: "string" },
},
required: ["insight"],
},
},
{
name: "list_insights",
description: "List all business insights in the memo",
inputSchema: {
type: "object",
properties: {},
},
},
],
};
}
/**
* Handle tool call requests
* @param name Name of the tool to call
* @param args Arguments for the tool
* @returns Tool execution result
*/
export async function handleToolCall(name: string, args: any) {
try {
switch (name) {
case "read_query":
return await readQuery(args.query);
case "write_query":
return await writeQuery(args.query);
case "create_table":
return await createTable(args.query);
case "alter_table":
return await alterTable(args.query);
case "drop_table":
return await dropTable(args.table_name, args.confirm);
case "export_query":
return await exportQuery(args.query, args.format);
case "list_tables":
return await listTables();
case "describe_table":
return await describeTable(args.table_name);
case "append_insight":
return await appendInsight(args.insight);
case "list_insights":
return await listInsights();
default:
throw new Error(`Unknown tool: ${name}`);
}
} catch (error: any) {
return formatErrorResponse(error);
}
}

92
src/index.ts Normal file
View File

@@ -0,0 +1,92 @@
#!/usr/bin/env node
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import {
CallToolRequestSchema,
ListResourcesRequestSchema,
ListToolsRequestSchema,
ReadResourceRequestSchema,
} from "@modelcontextprotocol/sdk/types.js";
// Import database utils
import { initDatabase, closeDatabase } from './db/index.js';
// Import handlers
import { handleListResources, handleReadResource } from './handlers/resourceHandlers.js';
import { handleListTools, handleToolCall } from './handlers/toolHandlers.js';
// Configure the server
const server = new Server(
{
name: "executeautomation/database-server",
version: "1.0.0",
},
{
capabilities: {
resources: {},
tools: {},
},
},
);
// Parse command line arguments
const args = process.argv.slice(2);
if (args.length === 0) {
console.error("Please provide a database file path as a command-line argument");
process.exit(1);
}
const databasePath = args[0];
// Set up request handlers
server.setRequestHandler(ListResourcesRequestSchema, async () => {
return await handleListResources();
});
server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
return await handleReadResource(request.params.uri);
});
server.setRequestHandler(ListToolsRequestSchema, async () => {
return handleListTools();
});
server.setRequestHandler(CallToolRequestSchema, async (request) => {
return await handleToolCall(request.params.name, request.params.arguments);
});
// Handle shutdown gracefully
process.on('SIGINT', async () => {
console.log('Shutting down gracefully...');
await closeDatabase();
process.exit(0);
});
process.on('SIGTERM', async () => {
console.log('Shutting down gracefully...');
await closeDatabase();
process.exit(0);
});
/**
* Start the server
*/
async function runServer() {
try {
console.log(`Initializing database: ${databasePath}`);
await initDatabase(databasePath);
console.log('Starting MCP server...');
const transport = new StdioServerTransport();
await server.connect(transport);
console.log('Server running. Press Ctrl+C to exit.');
} catch (error) {
console.error("Failed to initialize:", error);
process.exit(1);
}
}
// Start the server
runServer().catch(console.error);

64
src/tools/insightTools.ts Normal file
View File

@@ -0,0 +1,64 @@
import { dbAll, dbExec, dbRun } from '../db/index.js';
import { formatSuccessResponse } from '../utils/formatUtils.js';
/**
* Add a business insight to the memo
* @param insight Business insight text
* @returns Result of the operation
*/
export async function appendInsight(insight: string) {
try {
if (!insight) {
throw new Error("Insight text is required");
}
// Create insights table if it doesn't exist
await dbExec(`
CREATE TABLE IF NOT EXISTS mcp_insights (
id INTEGER PRIMARY KEY AUTOINCREMENT,
insight TEXT NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
`);
// Insert the insight
await dbRun(
"INSERT INTO mcp_insights (insight) VALUES (?)",
[insight]
);
return formatSuccessResponse({ success: true, message: "Insight added" });
} catch (error: any) {
throw new Error(`Error adding insight: ${error.message}`);
}
}
/**
* List all insights in the memo
* @returns Array of insights
*/
export async function listInsights() {
try {
// Check if insights table exists
const tableExists = await dbAll(
"SELECT name FROM sqlite_master WHERE type='table' AND name = 'mcp_insights'"
);
if (tableExists.length === 0) {
// Create table if it doesn't exist
await dbExec(`
CREATE TABLE IF NOT EXISTS mcp_insights (
id INTEGER PRIMARY KEY AUTOINCREMENT,
insight TEXT NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
`);
return formatSuccessResponse([]);
}
const insights = await dbAll("SELECT * FROM mcp_insights ORDER BY created_at DESC");
return formatSuccessResponse(insights);
} catch (error: any) {
throw new Error(`Error listing insights: ${error.message}`);
}
}

77
src/tools/queryTools.ts Normal file
View File

@@ -0,0 +1,77 @@
import { dbAll, dbRun, dbExec } from '../db/index.js';
import { formatErrorResponse, formatSuccessResponse, convertToCSV } from '../utils/formatUtils.js';
/**
* Execute a read-only SQL query
* @param query SQL query to execute
* @returns Query results
*/
export async function readQuery(query: string) {
try {
if (!query.trim().toLowerCase().startsWith("select")) {
throw new Error("Only SELECT queries are allowed with read_query");
}
const result = await dbAll(query);
return formatSuccessResponse(result);
} catch (error: any) {
throw new Error(`SQL Error: ${error.message}`);
}
}
/**
* Execute a data modification SQL query
* @param query SQL query to execute
* @returns Information about affected rows
*/
export async function writeQuery(query: string) {
try {
const lowerQuery = query.trim().toLowerCase();
if (lowerQuery.startsWith("select")) {
throw new Error("Use read_query for SELECT operations");
}
if (!(lowerQuery.startsWith("insert") || lowerQuery.startsWith("update") || lowerQuery.startsWith("delete"))) {
throw new Error("Only INSERT, UPDATE, or DELETE operations are allowed with write_query");
}
const result = await dbRun(query);
return formatSuccessResponse({ affected_rows: result.changes });
} catch (error: any) {
throw new Error(`SQL Error: ${error.message}`);
}
}
/**
* Export query results to CSV or JSON format
* @param query SQL query to execute
* @param format Output format (csv or json)
* @returns Formatted query results
*/
export async function exportQuery(query: string, format: string) {
try {
if (!query.trim().toLowerCase().startsWith("select")) {
throw new Error("Only SELECT queries are allowed with export_query");
}
const result = await dbAll(query);
if (format === "csv") {
const csvData = convertToCSV(result);
return {
content: [{
type: "text",
text: csvData
}],
isError: false,
};
} else if (format === "json") {
return formatSuccessResponse(result);
} else {
throw new Error("Unsupported export format. Use 'csv' or 'json'");
}
} catch (error: any) {
throw new Error(`Export Error: ${error.message}`);
}
}

128
src/tools/schemaTools.ts Normal file
View File

@@ -0,0 +1,128 @@
import { dbAll, dbExec } from '../db/index.js';
import { formatSuccessResponse } from '../utils/formatUtils.js';
/**
* Create a new table in the database
* @param query CREATE TABLE SQL statement
* @returns Result of the operation
*/
export async function createTable(query: string) {
try {
if (!query.trim().toLowerCase().startsWith("create table")) {
throw new Error("Only CREATE TABLE statements are allowed");
}
await dbExec(query);
return formatSuccessResponse({ success: true, message: "Table created successfully" });
} catch (error: any) {
throw new Error(`SQL Error: ${error.message}`);
}
}
/**
* Alter an existing table schema
* @param query ALTER TABLE SQL statement
* @returns Result of the operation
*/
export async function alterTable(query: string) {
try {
if (!query.trim().toLowerCase().startsWith("alter table")) {
throw new Error("Only ALTER TABLE statements are allowed");
}
await dbExec(query);
return formatSuccessResponse({ success: true, message: "Table altered successfully" });
} catch (error: any) {
throw new Error(`SQL Error: ${error.message}`);
}
}
/**
* Drop a table from the database
* @param tableName Name of the table to drop
* @param confirm Safety confirmation flag
* @returns Result of the operation
*/
export async function dropTable(tableName: string, confirm: boolean) {
try {
if (!tableName) {
throw new Error("Table name is required");
}
if (!confirm) {
return formatSuccessResponse({
success: false,
message: "Safety confirmation required. Set confirm=true to proceed with dropping the table."
});
}
// Check if table exists
const tableExists = await dbAll(
"SELECT name FROM sqlite_master WHERE type='table' AND name = ?",
[tableName]
);
if (tableExists.length === 0) {
throw new Error(`Table '${tableName}' does not exist`);
}
// Drop the table
await dbExec(`DROP TABLE "${tableName}"`);
return formatSuccessResponse({
success: true,
message: `Table '${tableName}' dropped successfully`
});
} catch (error: any) {
throw new Error(`Error dropping table: ${error.message}`);
}
}
/**
* List all tables in the database
* @returns Array of table names
*/
export async function listTables() {
try {
const tables = await dbAll(
"SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'"
);
return formatSuccessResponse(tables.map((t) => t.name));
} catch (error: any) {
throw new Error(`Error listing tables: ${error.message}`);
}
}
/**
* Get schema information for a specific table
* @param tableName Name of the table to describe
* @returns Column definitions for the table
*/
export async function describeTable(tableName: string) {
try {
if (!tableName) {
throw new Error("Table name is required");
}
// Check if table exists
const tableExists = await dbAll(
"SELECT name FROM sqlite_master WHERE type='table' AND name = ?",
[tableName]
);
if (tableExists.length === 0) {
throw new Error(`Table '${tableName}' does not exist`);
}
const columns = await dbAll(`PRAGMA table_info("${tableName}")`);
return formatSuccessResponse(columns.map((col) => ({
name: col.name,
type: col.type,
notnull: !!col.notnull,
default_value: col.dflt_value,
primary_key: !!col.pk
})));
} catch (error: any) {
throw new Error(`Error describing table: ${error.message}`);
}
}

61
src/utils/formatUtils.ts Normal file
View File

@@ -0,0 +1,61 @@
/**
* Convert data to CSV format
* @param data Array of objects to convert to CSV
* @returns CSV formatted string
*/
export function convertToCSV(data: any[]): string {
if (data.length === 0) return '';
// Get headers
const headers = Object.keys(data[0]);
// Create CSV header row
let csv = headers.join(',') + '\n';
// Add data rows
data.forEach(row => {
const values = headers.map(header => {
const val = row[header];
// Handle strings with commas, quotes, etc.
if (typeof val === 'string') {
return `"${val.replace(/"/g, '""')}"`;
}
// Use empty string for null/undefined
return val === null || val === undefined ? '' : val;
});
csv += values.join(',') + '\n';
});
return csv;
}
/**
* Format error response
* @param error Error object or message
* @returns Formatted error response object
*/
export function formatErrorResponse(error: Error | string): { content: Array<{type: string, text: string}>, isError: boolean } {
const message = error instanceof Error ? error.message : error;
return {
content: [{
type: "text",
text: JSON.stringify({ error: message }, null, 2)
}],
isError: true
};
}
/**
* Format success response
* @param data Data to format
* @returns Formatted success response object
*/
export function formatSuccessResponse(data: any): { content: Array<{type: string, text: string}>, isError: boolean } {
return {
content: [{
type: "text",
text: JSON.stringify(data, null, 2)
}],
isError: false
};
}

View File

@@ -12,7 +12,7 @@
"declaration": false
},
"include": [
"./**/*.ts"
"./src/**/*.ts"
],
"exclude": [
"node_modules",