Initial Commit

This commit is contained in:
Karthik KK
2025-04-13 11:49:08 +12:00
parent 3bd57460b5
commit 27e0aac8de
10 changed files with 3605 additions and 0 deletions

BIN
.DS_Store vendored Normal file

Binary file not shown.

26
Dockerfile Normal file
View File

@@ -0,0 +1,26 @@
FROM node:20-alpine
# Add metadata
LABEL maintainer="ExecuteAutomation <info@executeautomation.com>"
LABEL description="ExecuteAutomation Database Server - A Model Context Protocol server for SQLite"
LABEL version="1.0.0"
WORKDIR /app
# Copy package.json and package-lock.json
COPY package*.json ./
# Install dependencies
RUN npm ci
# Copy source code
COPY . .
# Build the TypeScript code
RUN npm run build
# Set the entrypoint
ENTRYPOINT ["node", "dist/index.js"]
# Default command (to be overridden by the user)
CMD [""]

21
build.sh Executable file
View File

@@ -0,0 +1,21 @@
#!/bin/bash
# Clean dist directory
rm -rf dist
mkdir -p dist
# Install dependencies if node_modules doesn't exist
if [ ! -d "node_modules" ]; then
echo "Installing dependencies..."
npm install
fi
# Build TypeScript code
echo "Building TypeScript..."
./node_modules/.bin/tsc
# Make JavaScript files executable
echo "Making JavaScript files executable..."
chmod +x dist/*.js
echo "Build completed successfully!"

63
examples/example.js Normal file
View File

@@ -0,0 +1,63 @@
// Example of using the SQLite MCP Server
// First, start the server with a SQLite database file:
// npx @modelcontextprotocol/server-sqlite ./example.db
// Then, use the Claude Desktop app with the following config:
/*
{
"mcpServers": {
"sqlite": {
"command": "npx",
"args": [
"-y",
"@modelcontextprotocol/server-sqlite",
"./example.db"
]
}
}
}
*/
// Example prompts to use with Claude:
/*
1. Create a table:
"Create a 'users' table with columns for id, name, email, and created_at"
2. Insert data:
"Insert a few sample users into the users table"
3. Query data:
"Show me all users in the database"
4. Describe the schema:
"What tables exist in the database and what are their structures?"
5. Add an insight:
"Analyze the user data and add an insight about the user distribution"
*/
// SQLite setup
// 1. Create table example
/*
CREATE TABLE users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
email TEXT UNIQUE NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
*/
// 2. Insert data example
/*
INSERT INTO users (name, email) VALUES
('John Doe', 'john@example.com'),
('Jane Smith', 'jane@example.com'),
('Bob Johnson', 'bob@example.com')
*/
// 3. Query example
/*
SELECT * FROM users
*/

34
global.d.ts vendored Normal file
View File

@@ -0,0 +1,34 @@
declare module 'sqlite3' {
export interface RunResult {
lastID: number;
changes: number;
}
export interface Database {
all(sql: string, params: any[], callback: (err: Error | null, rows: any[]) => void): void;
all(sql: string, callback: (err: Error | null, rows: any[]) => void): void;
get(sql: string, params: any[], callback: (err: Error | null, row: any) => void): void;
get(sql: string, callback: (err: Error | null, row: any) => void): void;
run(sql: string, params: any[], callback?: (err: Error | null) => void): this;
run(sql: string, callback?: (err: Error | null) => void): this;
exec(sql: string, callback?: (err: Error | null) => void): this;
close(callback?: (err: Error | null) => void): void;
}
export class Statement {
bind(params: any[]): this;
reset(): this;
finalize(callback?: (err: Error | null) => void): void;
}
export function verbose(): any;
export class Database {
constructor(filename: string, mode?: number, callback?: (err: Error | null) => void);
constructor(filename: string, callback?: (err: Error | null) => void);
}
}

549
index.ts Normal file
View File

@@ -0,0 +1,549 @@
#!/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 sqlite3 from "sqlite3";
// 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];
// Create a resource base URL for SQLite
const resourceBaseUrl = new URL(`sqlite:///${databasePath}`);
const SCHEMA_PATH = "schema";
// Initialize SQLite database connection
let db: sqlite3.Database;
function initDatabase(): Promise<void> {
return new Promise((resolve, reject) => {
db = new sqlite3.Database(databasePath, (err) => {
if (err) {
reject(err);
} else {
resolve();
}
});
});
}
// Helper function to run a query and get all results
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);
}
});
});
}
// Helper function to run a query that doesn't return results
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 });
}
});
});
}
// Helper function to run multiple statements
function dbExec(query: string): Promise<void> {
return new Promise((resolve, reject) => {
db.exec(query, (err: Error | null) => {
if (err) {
reject(err);
} else {
resolve();
}
});
});
}
// List all available database resources (tables)
server.setRequestHandler(ListResourcesRequestSchema, async () => {
// Query to get all table names
const result = await dbAll(
"SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'"
);
return {
resources: result.map((row) => ({
uri: new URL(`${row.name}/${SCHEMA_PATH}`, resourceBaseUrl).href,
mimeType: "application/json",
name: `"${row.name}" database schema`,
})),
};
});
// Get schema information for a specific table
server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
const resourceUrl = new URL(request.params.uri);
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: request.params.uri,
mimeType: "application/json",
text: JSON.stringify(result.map((column) => ({
column_name: column.name,
data_type: column.type
})), null, 2),
},
],
};
});
// List available tools
server.setRequestHandler(ListToolsRequestSchema, async () => {
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"],
},
},
],
};
});
// Helper function to convert data to CSV format
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;
}
// Handle tool calls
server.setRequestHandler(CallToolRequestSchema, async (request) => {
switch (request.params.name) {
case "read_query": {
const query = request.params.arguments?.query as string;
if (!query.trim().toLowerCase().startsWith("select")) {
throw new Error("Only SELECT queries are allowed with read_query");
}
try {
const result = await dbAll(query);
return {
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
isError: false,
};
} catch (error: any) {
throw new Error(`SQL Error: ${error.message}`);
}
}
case "write_query": {
const query = request.params.arguments?.query as string;
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");
}
try {
const result = await dbRun(query);
return {
content: [{
type: "text",
text: JSON.stringify({ affected_rows: result.changes }, null, 2)
}],
isError: false,
};
} catch (error: any) {
throw new Error(`SQL Error: ${error.message}`);
}
}
case "create_table": {
const query = request.params.arguments?.query as string;
if (!query.trim().toLowerCase().startsWith("create table")) {
throw new Error("Only CREATE TABLE statements are allowed");
}
try {
await dbExec(query);
return {
content: [{
type: "text",
text: JSON.stringify({ success: true, message: "Table created successfully" }, null, 2)
}],
isError: false,
};
} catch (error: any) {
throw new Error(`SQL Error: ${error.message}`);
}
}
case "alter_table": {
const query = request.params.arguments?.query as string;
if (!query.trim().toLowerCase().startsWith("alter table")) {
throw new Error("Only ALTER TABLE statements are allowed");
}
try {
await dbExec(query);
return {
content: [{
type: "text",
text: JSON.stringify({ success: true, message: "Table altered successfully" }, null, 2)
}],
isError: false,
};
} catch (error: any) {
throw new Error(`SQL Error: ${error.message}`);
}
}
case "drop_table": {
const tableName = request.params.arguments?.table_name as string;
const confirm = request.params.arguments?.confirm as boolean;
if (!tableName) {
throw new Error("Table name is required");
}
if (!confirm) {
return {
content: [{
type: "text",
text: JSON.stringify({
success: false,
message: "Safety confirmation required. Set confirm=true to proceed with dropping the table."
}, null, 2)
}],
isError: false,
};
}
try {
// 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 {
content: [{
type: "text",
text: JSON.stringify({ success: true, message: `Table '${tableName}' dropped successfully` }, null, 2)
}],
isError: false,
};
} catch (error: any) {
throw new Error(`Error dropping table: ${error.message}`);
}
}
case "export_query": {
const query = request.params.arguments?.query as string;
const format = request.params.arguments?.format as string;
if (!query.trim().toLowerCase().startsWith("select")) {
throw new Error("Only SELECT queries are allowed with export_query");
}
try {
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 {
content: [{
type: "text",
text: JSON.stringify(result, null, 2)
}],
isError: false,
};
} else {
throw new Error("Unsupported export format. Use 'csv' or 'json'");
}
} catch (error: any) {
throw new Error(`Export Error: ${error.message}`);
}
}
case "list_tables": {
try {
const tables = await dbAll(
"SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'"
);
return {
content: [{
type: "text",
text: JSON.stringify(tables.map((t) => t.name), null, 2)
}],
isError: false,
};
} catch (error: any) {
throw new Error(`Error listing tables: ${error.message}`);
}
}
case "describe_table": {
const tableName = request.params.arguments?.table_name as string;
if (!tableName) {
throw new Error("Table name is required");
}
try {
// 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 {
content: [{
type: "text",
text: JSON.stringify(columns.map((col) => ({
name: col.name,
type: col.type,
notnull: !!col.notnull,
default_value: col.dflt_value,
primary_key: !!col.pk
})), null, 2)
}],
isError: false,
};
} catch (error: any) {
throw new Error(`Error describing table: ${error.message}`);
}
}
case "append_insight": {
const insight = request.params.arguments?.insight as string;
if (!insight) {
throw new Error("Insight text is required");
}
try {
// 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 {
content: [{
type: "text",
text: JSON.stringify({ success: true, message: "Insight added" }, null, 2)
}],
isError: false,
};
} catch (error: any) {
throw new Error(`Error adding insight: ${error.message}`);
}
}
default:
throw new Error(`Unknown tool: ${request.params.name}`);
}
});
async function runServer() {
try {
await initDatabase();
const transport = new StdioServerTransport();
await server.connect(transport);
} catch (error) {
console.error("Failed to initialize:", error);
process.exit(1);
}
}
runServer().catch(console.error);

2656
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

33
package.json Normal file
View File

@@ -0,0 +1,33 @@
{
"name": "@executeautomation/database-server",
"version": "1.0.0",
"description": "MCP server for interacting with SQLite databases by ExecuteAutomation",
"license": "MIT",
"author": "ExecuteAutomation (https://executeautomation.com)",
"homepage": "https://executeautomation.com",
"bugs": "https://github.com/executeautomation/database-server/issues",
"type": "module",
"bin": {
"ea-database-server": "dist/index.js"
},
"files": [
"dist"
],
"scripts": {
"build": "tsc && shx chmod +x dist/*.js",
"prepare": "npm run build",
"watch": "tsc --watch",
"start": "node dist/index.js",
"dev": "tsc && node dist/index.js",
"example": "node examples/example.js"
},
"dependencies": {
"@modelcontextprotocol/sdk": "1.9.0",
"sqlite3": "5.1.7"
},
"devDependencies": {
"@types/sqlite3": "5.1.0",
"shx": "0.4.0",
"typescript": "5.8.3"
}
}

202
readme.md Normal file
View File

@@ -0,0 +1,202 @@
# ExecuteAutomation Database Server
A Model Context Protocol server that provides access to SQLite databases. This server enables LLMs like Claude to inspect database schemas and execute SQL queries.
## Components
### Tools
The server offers nine core tools:
#### Query Tools
- **read_query**
- Execute SELECT queries to read data from the database
- Input:
- `query` (string): The SELECT SQL query to execute
- Returns: Query results as array of objects
- **write_query**
- Execute INSERT, UPDATE, or DELETE queries
- Input:
- `query` (string): The SQL modification query
- Returns: `{ affected_rows: number }`
#### Schema Management Tools
- **create_table**
- Create new tables in the database
- Input:
- `query` (string): CREATE TABLE SQL statement
- Returns: Confirmation of table creation
- **alter_table**
- Modify existing table schema (add columns, rename tables, etc.)
- Input:
- `query` (string): ALTER TABLE SQL statement
- Returns: Confirmation of table alteration
- **drop_table**
- Remove a table from the database with safety confirmation
- Input:
- `table_name` (string): Name of the table to drop
- `confirm` (boolean): Safety confirmation flag (must be true to actually drop)
- Returns: Confirmation message or safety warning
#### Schema Information Tools
- **list_tables**
- Get a list of all tables in the database
- No input required
- Returns: Array of table names
- **describe_table**
- View schema information for a specific table
- Input:
- `table_name` (string): Name of table to describe
- Returns: Array of column definitions with names and types
#### Data Export Tools
- **export_query**
- Export query results to various formats
- Input:
- `query` (string): The SELECT SQL query to execute
- `format` (string): Output format - either "csv" or "json"
- Returns: Query results formatted as CSV or JSON
#### Insights
- **append_insight**
- Add a business insight to the memo
- Input:
- `insight` (string): Business insight discovered from data analysis
- Returns: Confirmation message
### Resources
The server provides schema information for each table in the database:
- **Table Schemas** (`sqlite:///<path>/<table>/schema`)
- JSON schema information for each table
- Includes column names and data types
- Automatically discovered from database metadata
## Usage with Claude Desktop
To use this server with the Claude Desktop app, add the following configuration to the "mcpServers" section of your `claude_desktop_config.json`:
### Docker
```json
{
"mcpServers": {
"database": {
"command": "docker",
"args": [
"run",
"-i",
"--rm",
"executeautomation/database-server",
"/path/to/database.db"]
}
}
}
```
### NPX (Published Package)
```json
{
"mcpServers": {
"database": {
"command": "npx",
"args": [
"-y",
"@executeautomation/database-server",
"/path/to/database.db"
]
}
}
}
```
### Local Development
For local development and debugging, you can point directly to your compiled JavaScript file:
```json
{
"mcpServers": {
"database": {
"command": "node",
"args": [
"/path/to/your/project/dist/index.js",
"/path/to/your/database.db"
]
}
}
}
```
Example with full paths:
```json
{
"mcpServers": {
"database": {
"command": "node",
"args": [
"/Users/username/projects/database-server/dist/index.js",
"/Users/username/databases/mydata.db"
]
}
}
}
```
The Claude Desktop config file is typically located at:
- macOS: `~/Library/Application Support/Claude/claude_desktop_config.json`
- Windows: `%APPDATA%\Claude\claude_desktop_config.json`
- Linux: `~/.config/Claude/claude_desktop_config.json`
Replace all paths with your actual file paths.
## Building and Development
### Quick Build
Use the included build script to install dependencies and build the project:
```sh
chmod +x build.sh
./build.sh
```
### Manual Build Steps
```sh
# Install dependencies
npm install
# Build the TypeScript
npm run build
```
### Docker Build
```sh
docker build -t executeautomation/database-server -f Dockerfile .
```
## Troubleshooting
If you encounter connection issues:
1. Make sure the database file path is correct and accessible
2. Verify that the compiled JavaScript file exists at the specified path
3. Check the Claude Desktop logs for detailed error messages
4. Restart Claude Desktop after making configuration changes
## License
This MCP server is licensed under the MIT License. This means you are free to use, modify, and distribute the software, subject to the terms and conditions of the MIT License.
## About ExecuteAutomation
This server is maintained by ExecuteAutomation. Visit [executeautomation.com](https://executeautomation.com) for more tools and resources.

21
tsconfig.json Normal file
View File

@@ -0,0 +1,21 @@
{
"compilerOptions": {
"target": "ES2020",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"strict": true,
"skipLibCheck": true,
"outDir": "./dist",
"rootDir": ".",
"declaration": false
},
"include": [
"./**/*.ts"
],
"exclude": [
"node_modules",
"dist"
]
}