diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..1b81cb5 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,84 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project Overview + +MCP (Model Context Protocol) database server that exposes database access as tools over stdio transport. Supports SQLite, SQL Server, PostgreSQL, and MySQL. Published as `@executeautomation/database-server` on npm. + +## Build & Dev Commands + +```bash +npm run build # Compile TypeScript and chmod the entry point +npm run dev # Build + run the server (needs DB args, see below) +npm run watch # Incremental TypeScript compilation on file changes +npm run clean # Remove dist/ directory +npm run start # Run compiled server (needs DB args) +``` + +There is no test framework or linter configured in this project. + +### Running the server locally + +```bash +# SQLite +node dist/src/index.js /path/to/database.db + +# SQL Server +node dist/src/index.js --sqlserver --server --database [--user --password

] + +# PostgreSQL +node dist/src/index.js --postgresql --host --database [--user --password

] + +# MySQL +node dist/src/index.js --mysql --host --database [--user --password

--port ] + +# MySQL with AWS IAM +node dist/src/index.js --mysql --aws-iam-auth --host --database --user --aws-region +``` + +## Architecture + +### Entry Points + +- **`src/index.ts`** - Main entry point. Parses CLI args, creates the MCP `Server`, wires up request handlers, and initializes the database connection. Compiled to `dist/src/index.js`. +- **`index.ts` (root)** - Legacy standalone SQLite-only implementation. **Not compiled** by tsconfig (which only includes `./src/**/*.ts`). Kept for reference but not used. + +### Layered Structure + +``` +src/index.ts (CLI parsing + MCP server setup) + -> src/handlers/ (MCP request routing) + -> toolHandlers.ts - routes CallToolRequest to tool implementations + -> resourceHandlers.ts - handles ListResources/ReadResource for table schemas + -> src/tools/ (business logic for each MCP tool) + -> queryTools.ts - read_query, write_query, export_query + -> schemaTools.ts - create_table, alter_table, drop_table, list_tables, describe_table + -> insightTools.ts - append_insight, list_insights + -> src/db/ (database abstraction layer) + -> adapter.ts - DbAdapter interface + createDbAdapter() factory + -> index.ts - singleton module holding the active adapter; exports dbAll/dbRun/dbExec + -> sqlite-adapter.ts, sqlserver-adapter.ts, postgresql-adapter.ts, mysql-adapter.ts + -> src/utils/ + -> formatUtils.ts - CSV conversion, formatErrorResponse, formatSuccessResponse +``` + +### Database Adapter Pattern + +All database backends implement the `DbAdapter` interface (`src/db/adapter.ts`): `init()`, `close()`, `all()`, `run()`, `exec()`, `getMetadata()`, `getListTablesQuery()`, `getDescribeTableQuery()`. The factory function `createDbAdapter(type, connectionInfo)` instantiates the correct adapter. + +`src/db/index.ts` acts as a singleton: it holds the active `DbAdapter` instance and re-exports convenience functions (`dbAll`, `dbRun`, `dbExec`, etc.) that all tool implementations call. The adapter is set once during `initDatabase()`. + +## Key Conventions + +- **ESM modules**: `"type": "module"` in package.json with `"module": "NodeNext"` in tsconfig. All TypeScript imports must use `.js` extension (e.g., `import { foo } from './bar.js'`). +- **Logging to stderr**: stdout is reserved for the MCP stdio transport. The custom `logger` in `src/index.ts` routes all output through `console.error`. +- **TypeScript strict mode**: `"strict": true` in tsconfig, targeting ES2020. +- **CLI arg parsing**: Manual parsing in `src/index.ts` with no external library. Database type is determined by flag (`--sqlserver`, `--postgresql`, `--mysql`), defaulting to SQLite. +- **`global.d.ts`**: Custom type declarations for the `sqlite3` module at the project root. + +## Adding a New Database Backend + +1. Create `src/db/-adapter.ts` implementing the `DbAdapter` interface. +2. Import it in `src/db/adapter.ts` and add a case to `createDbAdapter()`. +3. Add CLI arg parsing for the new backend in `src/index.ts`. diff --git a/src/handlers/toolHandlers.ts b/src/handlers/toolHandlers.ts index 463bcc3..35bba4a 100644 --- a/src/handlers/toolHandlers.ts +++ b/src/handlers/toolHandlers.ts @@ -14,7 +14,7 @@ export function handleListTools() { tools: [ { name: "read_query", - description: "Execute SELECT queries to read data from the database", + description: "Execute read-only queries (SELECT, EXPLAIN, WITH, PRAGMA, SHOW, DESCRIBE) to read data from the database", inputSchema: { type: "object", properties: { @@ -70,7 +70,7 @@ export function handleListTools() { }, { name: "export_query", - description: "Export query results to various formats (CSV, JSON)", + description: "Export read-only query results (SELECT, EXPLAIN, WITH, PRAGMA, SHOW, DESCRIBE) to various formats (CSV, JSON)", inputSchema: { type: "object", properties: { diff --git a/src/tools/queryTools.ts b/src/tools/queryTools.ts index 8bb465a..baf848b 100644 --- a/src/tools/queryTools.ts +++ b/src/tools/queryTools.ts @@ -1,15 +1,26 @@ import { dbAll, dbRun, dbExec } from '../db/index.js'; import { formatErrorResponse, formatSuccessResponse, convertToCSV } from '../utils/formatUtils.js'; +const ALLOWED_READ_PREFIXES = ["select", "explain", "with", "pragma", "show", "describe", "desc"]; + +function isReadOnlyQuery(query: string): boolean { + const normalized = query.trim().toLowerCase(); + return ALLOWED_READ_PREFIXES.some(prefix => normalized.startsWith(prefix)); +} + /** - * Execute a read-only SQL query + * Execute a read-only SQL query. + * Supports SELECT, EXPLAIN, WITH (CTEs), PRAGMA, SHOW, and DESCRIBE/DESC statements. * @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"); + if (!isReadOnlyQuery(query)) { + throw new Error( + "Only read-only queries are allowed with read_query. " + + "Supported statements: SELECT, EXPLAIN, WITH, PRAGMA, SHOW, DESCRIBE/DESC" + ); } const result = await dbAll(query); @@ -51,8 +62,11 @@ export async function writeQuery(query: string) { */ 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"); + if (!isReadOnlyQuery(query)) { + throw new Error( + "Only read-only queries are allowed with export_query. " + + "Supported statements: SELECT, EXPLAIN, WITH, PRAGMA, SHOW, DESCRIBE/DESC" + ); } const result = await dbAll(query);