diff --git a/docs/docs/database-tools.md b/docs/docs/database-tools.md
index 5b8b21a..0b7152f 100644
--- a/docs/docs/database-tools.md
+++ b/docs/docs/database-tools.md
@@ -13,6 +13,9 @@ The MCP Database Server provides a set of tools that Claude can use to interact
| `drop_table` | Remove a table from the database | `table_name`: Name of table
`confirm`: Safety flag (must be true) |
| `list_tables` | Get a list of all tables | None |
| `describe_table` | View schema information for a table | `table_name`: Name of table |
+| `list_indexes` | List all indexes in the database | None |
+| `list_table_indexes` | List indexes for a specific table | `table_name`: Name of table |
+| `describe_index` | Get detailed information about an index | `index_name`: Name of index
`table_name`: Table name (optional) |
| `export_query` | Export query results as CSV/JSON | `query`: SQL SELECT statement
`format`: "csv" or "json" |
| `append_insight` | Add a business insight to memo | `insight`: Text of insight |
| `list_insights` | List all business insights | None |
@@ -49,6 +52,28 @@ Create a new table called "CustomerFeedback" with columns for customer ID, ratin
Claude will use the `create_table` tool to define the new table.
+### Index Management
+
+To analyze database indexes for performance optimization:
+
+```
+Show me all indexes in the database
+```
+
+Claude will use the `list_indexes` tool to display all available indexes.
+
+```
+What indexes exist on the Users table?
+```
+
+Claude will use the `list_table_indexes` tool to show indexes specific to that table.
+
+```
+Give me detailed information about the idx_users_email index
+```
+
+Claude will use the `describe_index` tool to provide comprehensive index details.
+
### Exporting Data
To export query results:
diff --git a/docs/docs/index-management.md b/docs/docs/index-management.md
new file mode 100644
index 0000000..5b9f5b6
--- /dev/null
+++ b/docs/docs/index-management.md
@@ -0,0 +1,188 @@
+# Index Management Guide
+
+Database indexes are crucial for query performance optimization. The MCP Database Server provides comprehensive tools to analyze, inspect, and understand your database indexes across all supported database types (SQLite, PostgreSQL, MySQL, and SQL Server).
+
+## Overview
+
+Index management tools help you:
+- **Identify existing indexes** across your database
+- **Analyze table-specific indexes** for optimization opportunities
+- **Get detailed index information** including columns, uniqueness, and type
+- **Optimize query performance** by understanding index usage
+
+## Available Index Tools
+
+### list_indexes
+Lists all indexes in the database with basic information.
+
+**Parameters:** None
+
+**Example Usage:**
+```
+Show me all indexes in the database
+```
+
+**Sample Output:**
+```json
+{
+ "success": true,
+ "data": [
+ {
+ "name": "idx_users_email",
+ "table_name": "users",
+ "columns": ["email"],
+ "unique": true,
+ "type": "btree"
+ },
+ {
+ "name": "idx_orders_customer_id",
+ "table_name": "orders",
+ "columns": ["customer_id"],
+ "unique": false,
+ "type": "btree"
+ }
+ ]
+}
+```
+
+### list_table_indexes
+Lists all indexes for a specific table.
+
+**Parameters:**
+- `table_name` (required): Name of the table to analyze
+
+**Example Usage:**
+```
+What indexes exist on the users table?
+```
+
+**Sample Output:**
+```json
+{
+ "success": true,
+ "data": [
+ {
+ "name": "PRIMARY",
+ "table_name": "users",
+ "columns": ["id"],
+ "unique": true,
+ "type": "btree",
+ "primary": true
+ },
+ {
+ "name": "idx_users_email",
+ "table_name": "users",
+ "columns": ["email"],
+ "unique": true,
+ "type": "btree"
+ }
+ ]
+}
+```
+
+### describe_index
+Provides detailed information about a specific index.
+
+**Parameters:**
+- `index_name` (required): Name of the index to describe
+- `table_name` (optional): Table name for disambiguation
+
+**Example Usage:**
+```
+Give me detailed information about the idx_users_email index
+```
+
+**Sample Output:**
+```json
+{
+ "success": true,
+ "data": {
+ "name": "idx_users_email",
+ "table_name": "users",
+ "unique": true,
+ "type": "btree",
+ "columns": [
+ {
+ "name": "email",
+ "position": 1,
+ "is_descending": false,
+ "type": "key"
+ }
+ ],
+ "definition": "CREATE UNIQUE INDEX idx_users_email ON users (email)"
+ }
+}
+```
+
+## Common Use Cases
+
+### Performance Analysis
+```
+List all indexes and identify which tables might need optimization
+```
+
+### Index Audit
+```
+Show me all unique indexes in the database to understand data constraints
+```
+
+### Query Optimization
+```
+What indexes are available on the orders table for the customer_id column?
+```
+
+### Schema Documentation
+```
+Describe the primary key index on the products table
+```
+
+## Database-Specific Features
+
+### SQLite
+- Supports basic index information and PRAGMA index_info
+- Automatically excludes system indexes (sqlite_*)
+
+### PostgreSQL
+- Provides index method information (btree, hash, gin, gist)
+- Includes partial index conditions when available
+- Shows primary key and unique constraint details
+
+### MySQL
+- Displays index types (BTREE, HASH, FULLTEXT)
+- Shows column positions in composite indexes
+- Includes key length information
+
+### SQL Server
+- Provides clustered vs non-clustered index information
+- Shows included columns for covering indexes
+- Displays index usage statistics when available
+
+## Best Practices
+
+1. **Regular Index Review**: Use `list_indexes` periodically to audit your database indexes
+
+2. **Table-Specific Analysis**: Use `list_table_indexes` when optimizing queries for specific tables
+
+3. **Detailed Investigation**: Use `describe_index` to understand complex composite indexes
+
+4. **Performance Monitoring**: Combine index information with query analysis to identify optimization opportunities
+
+5. **Cross-Database Compatibility**: Index tools work consistently across all supported database types
+
+## Troubleshooting
+
+### Common Issues
+
+**Index Not Found**
+- Verify the index name spelling
+- Check if the index exists using `list_indexes`
+- Ensure you have proper database permissions
+
+**Empty Results**
+- Confirm the table exists using `list_tables`
+- Check if the table has any indexes defined
+- Verify database connection is active
+
+**Permission Errors**
+- Ensure your database user has SELECT permissions on system tables
+- Check database-specific permission requirements for metadata access
\ No newline at end of file
diff --git a/docs/sidebars.ts b/docs/sidebars.ts
index 1e6ae94..928e808 100644
--- a/docs/sidebars.ts
+++ b/docs/sidebars.ts
@@ -36,6 +36,7 @@ const sidebars: SidebarsConfig = {
label: 'Usage',
items: [
'database-tools',
+ 'index-management',
'usage-examples',
],
},
diff --git a/src/db/adapter.ts b/src/db/adapter.ts
index 3ae6dc5..8e8bf6f 100644
--- a/src/db/adapter.ts
+++ b/src/db/adapter.ts
@@ -48,6 +48,19 @@ export interface DbAdapter {
* @param tableName Table name
*/
getDescribeTableQuery(tableName: string): string;
+
+ /**
+ * Get database-specific query for listing indexes
+ * @param tableName Optional table name to filter indexes
+ */
+ getListIndexesQuery(tableName?: string): string;
+
+ /**
+ * Get database-specific query for describing an index
+ * @param indexName Index name
+ * @param tableName Optional table name for disambiguation
+ */
+ getDescribeIndexQuery(indexName: string, tableName?: string): string;
}
// Import adapters using dynamic imports
diff --git a/src/db/index.ts b/src/db/index.ts
index f883f2b..3f3059b 100644
--- a/src/db/index.ts
+++ b/src/db/index.ts
@@ -102,4 +102,27 @@ export function getDescribeTableQuery(tableName: string): string {
throw new Error("Database not initialized");
}
return dbAdapter.getDescribeTableQuery(tableName);
-}
\ No newline at end of file
+}
+
+/**
+ * Get database-specific query for listing indexes
+ * @param tableName Optional table name to filter indexes
+ */
+export function getListIndexesQuery(tableName?: string): string {
+ if (!dbAdapter) {
+ throw new Error("Database not initialized");
+ }
+ return dbAdapter.getListIndexesQuery(tableName);
+}
+
+/**
+ * Get database-specific query for describing an index
+ * @param indexName Index name
+ * @param tableName Optional table name for disambiguation
+ */
+export function getDescribeIndexQuery(indexName: string, tableName?: string): string {
+ if (!dbAdapter) {
+ throw new Error("Database not initialized");
+ }
+ return dbAdapter.getDescribeIndexQuery(indexName, tableName);
+}
\ No newline at end of file
diff --git a/src/db/mysql-adapter.ts b/src/db/mysql-adapter.ts
index 77c2305..5172efa 100644
--- a/src/db/mysql-adapter.ts
+++ b/src/db/mysql-adapter.ts
@@ -213,4 +213,64 @@ export class MysqlAdapter implements DbAdapter {
getDescribeTableQuery(tableName: string): string {
return `DESCRIBE \`${tableName}\``;
}
-}
\ No newline at end of file
+
+ /**
+ * Get database-specific query for listing indexes
+ * @param tableName Optional table name to filter indexes
+ */
+ getListIndexesQuery(tableName?: string): string {
+ let query = `
+ SELECT
+ INDEX_NAME as name,
+ TABLE_NAME as table_name,
+ COLUMN_NAME as column_name,
+ NON_UNIQUE as is_unique,
+ INDEX_TYPE as type
+ FROM
+ information_schema.STATISTICS
+ WHERE
+ TABLE_SCHEMA = DATABASE()
+ `;
+
+ if (tableName) {
+ query += ` AND TABLE_NAME = ?`;
+ }
+
+ query += ` ORDER BY TABLE_NAME, INDEX_NAME, SEQ_IN_INDEX`;
+ return query;
+ }
+
+ /**
+ * Get database-specific query for describing an index
+ * @param indexName Index name
+ * @param tableName Optional table name for disambiguation
+ */
+ getDescribeIndexQuery(indexName: string, tableName?: string): string {
+ let query = `
+ SELECT
+ s.INDEX_NAME as name,
+ s.TABLE_NAME as table_name,
+ s.COLUMN_NAME as column_name,
+ s.NON_UNIQUE as is_unique,
+ s.INDEX_TYPE as type,
+ s.SEQ_IN_INDEX as column_position,
+ s.CARDINALITY,
+ s.SUB_PART,
+ s.NULLABLE,
+ s.INDEX_COMMENT as comment,
+ CASE WHEN s.NON_UNIQUE = 0 THEN 1 ELSE 0 END as unique_constraint
+ FROM
+ information_schema.STATISTICS s
+ WHERE
+ s.TABLE_SCHEMA = DATABASE()
+ AND s.INDEX_NAME = ?
+ `;
+
+ if (tableName) {
+ query += ` AND s.TABLE_NAME = ?`;
+ }
+
+ query += ` ORDER BY s.SEQ_IN_INDEX`;
+ return query;
+ }
+}
\ No newline at end of file
diff --git a/src/db/postgresql-adapter.ts b/src/db/postgresql-adapter.ts
index 6e95cf1..4384740 100644
--- a/src/db/postgresql-adapter.ts
+++ b/src/db/postgresql-adapter.ts
@@ -1,5 +1,5 @@
import { DbAdapter } from "./adapter.js";
-import pg from 'pg';
+import pg from "pg";
/**
* PostgreSQL database adapter implementation
@@ -22,7 +22,7 @@ export class PostgresqlAdapter implements DbAdapter {
}) {
this.host = connectionInfo.host;
this.database = connectionInfo.database;
-
+
// Create PostgreSQL connection config
this.config = {
host: connectionInfo.host,
@@ -41,22 +41,28 @@ export class PostgresqlAdapter implements DbAdapter {
*/
async init(): Promise {
try {
- console.error(`[INFO] Connecting to PostgreSQL: ${this.host}, Database: ${this.database}`);
+ console.error(
+ `[INFO] Connecting to PostgreSQL: ${this.host}, Database: ${this.database}`
+ );
console.error(`[DEBUG] Connection details:`, {
- host: this.host,
+ host: this.host,
database: this.database,
port: this.config.port,
user: this.config.user,
connectionTimeoutMillis: this.config.connectionTimeoutMillis,
- ssl: !!this.config.ssl
+ ssl: !!this.config.ssl,
});
-
+
this.client = new pg.Client(this.config);
await this.client.connect();
console.error(`[INFO] PostgreSQL connection established successfully`);
} catch (err) {
- console.error(`[ERROR] PostgreSQL connection error: ${(err as Error).message}`);
- throw new Error(`Failed to connect to PostgreSQL: ${(err as Error).message}`);
+ console.error(
+ `[ERROR] PostgreSQL connection error: ${(err as Error).message}`
+ );
+ throw new Error(
+ `Failed to connect to PostgreSQL: ${(err as Error).message}`
+ );
}
}
@@ -74,7 +80,7 @@ export class PostgresqlAdapter implements DbAdapter {
try {
// PostgreSQL uses $1, $2, etc. for parameterized queries
const preparedQuery = query.replace(/\?/g, (_, i) => `$${i + 1}`);
-
+
const result = await this.client.query(preparedQuery, params);
return result.rows;
} catch (err) {
@@ -88,7 +94,10 @@ export class PostgresqlAdapter implements DbAdapter {
* @param params Query parameters
* @returns Promise with result info
*/
- async run(query: string, params: any[] = []): Promise<{ changes: number, lastID: number }> {
+ async run(
+ query: string,
+ params: any[] = []
+ ): Promise<{ changes: number; lastID: number }> {
if (!this.client) {
throw new Error("Database not initialized");
}
@@ -96,17 +105,17 @@ export class PostgresqlAdapter implements DbAdapter {
try {
// Replace ? with numbered parameters
const preparedQuery = query.replace(/\?/g, (_, i) => `$${i + 1}`);
-
+
let lastID = 0;
let changes = 0;
-
+
// For INSERT queries, try to get the inserted ID
- if (query.trim().toUpperCase().startsWith('INSERT')) {
+ if (query.trim().toUpperCase().startsWith("INSERT")) {
// Add RETURNING clause to get the inserted ID if it doesn't already have one
- const returningQuery = preparedQuery.includes('RETURNING')
- ? preparedQuery
+ const returningQuery = preparedQuery.includes("RETURNING")
+ ? preparedQuery
: `${preparedQuery} RETURNING id`;
-
+
const result = await this.client.query(returningQuery, params);
changes = result.rowCount || 0;
lastID = result.rows[0]?.id || 0;
@@ -114,7 +123,7 @@ export class PostgresqlAdapter implements DbAdapter {
const result = await this.client.query(preparedQuery, params);
changes = result.rowCount || 0;
}
-
+
return { changes, lastID };
} catch (err) {
throw new Error(`PostgreSQL query error: ${(err as Error).message}`);
@@ -151,12 +160,17 @@ export class PostgresqlAdapter implements DbAdapter {
/**
* Get database metadata
*/
- getMetadata(): { name: string, type: string, server: string, database: string } {
+ getMetadata(): {
+ name: string;
+ type: string;
+ server: string;
+ database: string;
+ } {
return {
name: "PostgreSQL",
type: "postgresql",
server: this.host,
- database: this.database
+ database: this.database,
};
}
@@ -194,4 +208,80 @@ export class PostgresqlAdapter implements DbAdapter {
c.ordinal_position
`;
}
-}
\ No newline at end of file
+
+ /**
+ * Get database-specific query for listing indexes
+ * @param tableName Optional table name to filter indexes
+ */
+ getListIndexesQuery(tableName?: string): string {
+ let query = `
+ SELECT
+ i.indexname as name,
+ i.tablename as table_name,
+ i.indexdef as definition,
+ CASE WHEN i.indexdef LIKE '%UNIQUE%' THEN true ELSE false END as is_unique
+ FROM
+ pg_indexes i
+ WHERE
+ i.schemaname = 'public'
+ `;
+
+ if (tableName) {
+ query += ` AND i.tablename = $1`;
+ }
+
+ query += ` ORDER BY i.tablename, i.indexname`;
+ return query;
+ }
+
+ /**
+ * Get database-specific query for describing an index
+ * @param indexName Index name
+ * @param tableName Optional table name for disambiguation
+ */
+ getDescribeIndexQuery(indexName: string, tableName?: string): string {
+ let query = `
+ SELECT
+ i.indexname as name,
+ i.tablename as table_name,
+ i.indexdef as definition,
+ i.indexdef as full_definition,
+ CASE WHEN i.indexdef LIKE '%UNIQUE%' THEN true ELSE false END as is_unique,
+ CASE WHEN idx.indisprimary THEN true ELSE false END as is_primary,
+ am.amname as method,
+ a.attname as column_name,
+ a.attnum as column_position,
+ CASE
+ WHEN array_position(string_to_array(idx.indkey::text, ' ')::int[], a.attnum) <= idx.indnkeyatts
+ THEN 'key'
+ ELSE 'included'
+ END as column_type
+ FROM
+ pg_indexes i
+ JOIN
+ pg_class c ON c.relname = i.indexname
+ JOIN
+ pg_index idx ON idx.indexrelid = c.oid
+ JOIN
+ pg_am am ON c.relam = am.oid
+ JOIN
+ pg_attribute a ON a.attrelid = idx.indrelid
+ AND a.attnum = ANY(idx.indkey)
+ WHERE
+ i.schemaname = 'public'
+ AND i.indexname = $1
+ `;
+
+ if (tableName) {
+ query += ` AND i.tablename = $2`;
+ }
+
+ query += `
+ ORDER BY
+ i.indexname,
+ array_position(string_to_array(idx.indkey::text, ' ')::int[], a.attnum)
+ `;
+
+ return query;
+ }
+}
diff --git a/src/db/sqlite-adapter.ts b/src/db/sqlite-adapter.ts
index 2b0b2d9..f416e99 100644
--- a/src/db/sqlite-adapter.ts
+++ b/src/db/sqlite-adapter.ts
@@ -142,4 +142,61 @@ export class SqliteAdapter implements DbAdapter {
getDescribeTableQuery(tableName: string): string {
return `PRAGMA table_info(${tableName})`;
}
-}
\ No newline at end of file
+
+ /**
+ * Get database-specific query for listing indexes
+ * @param tableName Optional table name to filter indexes
+ */
+ getListIndexesQuery(tableName?: string): string {
+ let query = `
+ SELECT
+ name,
+ tbl_name as table_name,
+ sql,
+ "unique" as is_unique
+ FROM sqlite_master
+ WHERE type = 'index'
+ AND name NOT LIKE 'sqlite_%'
+ `;
+
+ if (tableName) {
+ query += ` AND tbl_name = ?`;
+ }
+
+ query += ` ORDER BY tbl_name, name`;
+ return query;
+ }
+
+ /**
+ * Get database-specific query for describing an index
+ * @param indexName Index name
+ * @param tableName Optional table name for disambiguation
+ */
+ getDescribeIndexQuery(indexName: string, tableName?: string): string {
+ let query = `
+ SELECT
+ name,
+ tbl_name as table_name,
+ sql,
+ sql as definition,
+ CASE
+ WHEN sql LIKE '%UNIQUE%' THEN 1
+ ELSE 0
+ END as is_unique,
+ 'btree' as type,
+ CASE
+ WHEN name LIKE '%_pk_%' OR sql LIKE '%PRIMARY KEY%' THEN 1
+ ELSE 0
+ END as is_primary
+ FROM sqlite_master
+ WHERE type = 'index'
+ AND name = ?
+ `;
+
+ if (tableName) {
+ query += ` AND tbl_name = ?`;
+ }
+
+ return query;
+ }
+}
\ No newline at end of file
diff --git a/src/db/sqlserver-adapter.ts b/src/db/sqlserver-adapter.ts
index d9d8c8a..f3bc05a 100644
--- a/src/db/sqlserver-adapter.ts
+++ b/src/db/sqlserver-adapter.ts
@@ -1,5 +1,5 @@
+import sql from "mssql";
import { DbAdapter } from "./adapter.js";
-import sql from 'mssql';
/**
* SQL Server database adapter implementation
@@ -21,7 +21,7 @@ export class SqlServerAdapter implements DbAdapter {
}) {
this.server = connectionInfo.server;
this.database = connectionInfo.database;
-
+
// Create SQL Server connection config
this.config = {
server: connectionInfo.server,
@@ -29,8 +29,8 @@ export class SqlServerAdapter implements DbAdapter {
port: connectionInfo.port || 1433,
options: {
trustServerCertificate: connectionInfo.trustServerCertificate ?? true,
- ...connectionInfo.options
- }
+ ...connectionInfo.options,
+ },
};
// Add authentication options
@@ -49,12 +49,18 @@ export class SqlServerAdapter implements DbAdapter {
*/
async init(): Promise {
try {
- console.error(`[INFO] Connecting to SQL Server: ${this.server}, Database: ${this.database}`);
+ console.error(
+ `[INFO] Connecting to SQL Server: ${this.server}, Database: ${this.database}`
+ );
this.pool = await new sql.ConnectionPool(this.config).connect();
console.error(`[INFO] SQL Server connection established successfully`);
} catch (err) {
- console.error(`[ERROR] SQL Server connection error: ${(err as Error).message}`);
- throw new Error(`Failed to connect to SQL Server: ${(err as Error).message}`);
+ console.error(
+ `[ERROR] SQL Server connection error: ${(err as Error).message}`
+ );
+ throw new Error(
+ `Failed to connect to SQL Server: ${(err as Error).message}`
+ );
}
}
@@ -71,15 +77,16 @@ export class SqlServerAdapter implements DbAdapter {
try {
const request = this.pool.request();
-
+
// Add parameters to the request
params.forEach((param, index) => {
request.input(`param${index}`, param);
});
-
+
// Replace ? with named parameters
- const preparedQuery = query.replace(/\?/g, (_, i) => `@param${i}`);
-
+ let index = 0;
+ const preparedQuery = query.replace(/\?/g, () => `@param${index++}`);
+
const result = await request.query(preparedQuery);
return result.recordset;
} catch (err) {
@@ -93,26 +100,29 @@ export class SqlServerAdapter implements DbAdapter {
* @param params Query parameters
* @returns Promise with result info
*/
- async run(query: string, params: any[] = []): Promise<{ changes: number, lastID: number }> {
+ async run(
+ query: string,
+ params: any[] = []
+ ): Promise<{ changes: number; lastID: number }> {
if (!this.pool) {
throw new Error("Database not initialized");
}
try {
const request = this.pool.request();
-
+
// Add parameters to the request
params.forEach((param, index) => {
request.input(`param${index}`, param);
});
-
+
// Replace ? with named parameters
const preparedQuery = query.replace(/\?/g, (_, i) => `@param${i}`);
-
+
// Add output parameter for identity value if it's an INSERT
let lastID = 0;
- if (query.trim().toUpperCase().startsWith('INSERT')) {
- request.output('insertedId', sql.Int, 0);
+ if (query.trim().toUpperCase().startsWith("INSERT")) {
+ request.output("insertedId", sql.Int, 0);
const updatedQuery = `${preparedQuery}; SELECT @insertedId = SCOPE_IDENTITY();`;
const result = await request.query(updatedQuery);
lastID = result.output.insertedId || 0;
@@ -120,10 +130,10 @@ export class SqlServerAdapter implements DbAdapter {
const result = await request.query(preparedQuery);
lastID = 0;
}
-
- return {
- changes: this.getAffectedRows(query, lastID),
- lastID: lastID
+
+ return {
+ changes: this.getAffectedRows(query, lastID),
+ lastID: lastID,
};
} catch (err) {
throw new Error(`SQL Server query error: ${(err as Error).message}`);
@@ -161,12 +171,17 @@ export class SqlServerAdapter implements DbAdapter {
/**
* Get database metadata
*/
- getMetadata(): { name: string, type: string, server: string, database: string } {
+ getMetadata(): {
+ name: string;
+ type: string;
+ server: string;
+ database: string;
+ } {
return {
name: "SQL Server",
type: "sqlserver",
server: this.server,
- database: this.database
+ database: this.database,
};
}
@@ -202,14 +217,89 @@ export class SqlServerAdapter implements DbAdapter {
`;
}
+ /**
+ * Get database-specific query for listing indexes
+ * @param tableName Optional table name to filter indexes
+ */
+ getListIndexesQuery(tableName?: string): string {
+ let query = `
+ SELECT
+ i.name,
+ t.name as table_name,
+ i.is_unique,
+ i.type_desc as type
+ FROM
+ sys.indexes i
+ INNER JOIN
+ sys.tables t ON i.object_id = t.object_id
+ WHERE
+ i.is_primary_key = 0
+ AND i.is_unique_constraint = 0
+ AND i.name IS NOT NULL
+ `;
+
+ if (tableName) {
+ query += ` AND t.name = ?`;
+ }
+
+ query += ` ORDER BY t.name, i.name`;
+ return query;
+ }
+
+ /**
+ * Get database-specific query for describing an index
+ * @param indexName Index name
+ * @param tableName Optional table name for disambiguation
+ */
+ getDescribeIndexQuery(indexName: string, tableName?: string): string {
+ let query = `
+ SELECT
+ i.name,
+ t.name as table_name,
+ i.is_unique,
+ i.is_primary_key,
+ i.type_desc as type,
+ i.fill_factor,
+ i.is_padded,
+ i.is_disabled,
+ i.allow_row_locks,
+ i.allow_page_locks,
+ c.name as column_name,
+ ic.key_ordinal as column_position,
+ ic.is_descending_key,
+ ic.is_included_column,
+ CASE
+ WHEN ic.is_included_column = 1 THEN 'included'
+ ELSE 'key'
+ END as column_type
+ FROM
+ sys.indexes i
+ INNER JOIN
+ sys.tables t ON i.object_id = t.object_id
+ INNER JOIN
+ sys.index_columns ic ON i.object_id = ic.object_id AND i.index_id = ic.index_id
+ INNER JOIN
+ sys.columns c ON ic.object_id = c.object_id AND ic.column_id = c.column_id
+ WHERE
+ i.name = ?
+ `;
+
+ if (tableName) {
+ query += ` AND t.name = ?`;
+ }
+
+ query += ` ORDER BY ic.is_included_column, ic.key_ordinal`;
+ return query;
+ }
+
/**
* Helper to get the number of affected rows based on query type
*/
private getAffectedRows(query: string, lastID: number): number {
- const queryType = query.trim().split(' ')[0].toUpperCase();
- if (queryType === 'INSERT' && lastID > 0) {
+ const queryType = query.trim().split(" ")[0].toUpperCase();
+ if (queryType === "INSERT" && lastID > 0) {
return 1;
}
return 0; // For SELECT, unknown for UPDATE/DELETE without additional query
}
-}
\ No newline at end of file
+}
diff --git a/src/handlers/toolHandlers.ts b/src/handlers/toolHandlers.ts
index 463bcc3..01fbad4 100644
--- a/src/handlers/toolHandlers.ts
+++ b/src/handlers/toolHandlers.ts
@@ -1,9 +1,20 @@
-import { formatErrorResponse } from '../utils/formatUtils.js';
+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';
+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";
+import {
+ listIndexes,
+ listTableIndexes,
+ describeIndex,
+} from "../tools/indexTools.js";
/**
* Handle listing available tools
@@ -47,7 +58,8 @@ export function handleListTools() {
},
{
name: "alter_table",
- description: "Modify existing table schema (add columns, rename tables, etc.)",
+ description:
+ "Modify existing table schema (add columns, rename tables, etc.)",
inputSchema: {
type: "object",
properties: {
@@ -58,7 +70,8 @@ export function handleListTools() {
},
{
name: "drop_table",
- description: "Remove a table from the database with safety confirmation",
+ description:
+ "Remove a table from the database with safety confirmation",
inputSchema: {
type: "object",
properties: {
@@ -118,6 +131,37 @@ export function handleListTools() {
properties: {},
},
},
+ {
+ name: "list_indexes",
+ description: "List all indexes in the database",
+ inputSchema: {
+ type: "object",
+ properties: {},
+ },
+ },
+ {
+ name: "list_table_indexes",
+ description: "List all indexes for a specific table",
+ inputSchema: {
+ type: "object",
+ properties: {
+ table_name: { type: "string" },
+ },
+ required: ["table_name"],
+ },
+ },
+ {
+ name: "describe_index",
+ description: "Get detailed information about a specific index",
+ inputSchema: {
+ type: "object",
+ properties: {
+ index_name: { type: "string" },
+ table_name: { type: "string" },
+ },
+ required: ["index_name"],
+ },
+ },
],
};
}
@@ -133,38 +177,47 @@ export async function handleToolCall(name: string, args: any) {
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();
-
+
+ case "list_indexes":
+ return await listIndexes();
+
+ case "list_table_indexes":
+ return await listTableIndexes(args.table_name);
+
+ case "describe_index":
+ return await describeIndex(args.index_name, args.table_name);
+
default:
throw new Error(`Unknown tool: ${name}`);
}
} catch (error: any) {
return formatErrorResponse(error);
}
-}
\ No newline at end of file
+}
diff --git a/src/tools/indexTools.ts b/src/tools/indexTools.ts
new file mode 100644
index 0000000..936f4e1
--- /dev/null
+++ b/src/tools/indexTools.ts
@@ -0,0 +1,321 @@
+import {
+ dbAll,
+ getDatabaseMetadata,
+ getDescribeIndexQuery,
+ getListIndexesQuery,
+} from "../db/index.js";
+import { formatSuccessResponse } from "../utils/formatUtils.js";
+
+/**
+ * Interface for index information
+ */
+interface IndexInfo {
+ name: string;
+ table_name: string;
+ columns: string[];
+ unique: boolean;
+ type?: string;
+ method?: string;
+ primary?: boolean;
+}
+
+/**
+ * List all indexes in the database
+ * @param tableName Optional table name to filter indexes
+ * @returns List of all indexes with their properties
+ */
+export async function listIndexes(tableName?: string) {
+ try {
+ const dbInfo = getDatabaseMetadata();
+ const dbType = dbInfo.type;
+
+ const query = getListIndexesQuery(tableName);
+
+ const params: any[] = [];
+ if (tableName) {
+ params.push(tableName);
+ }
+
+ const results = await dbAll(query, params);
+ const indexes = await processIndexResults(results, dbType);
+
+ return formatSuccessResponse(indexes);
+ } catch (error: any) {
+ throw new Error(`Error listing indexes: ${error.message}`);
+ }
+}
+
+/**
+ * List indexes for a specific table
+ * @param tableName Name of the table
+ * @returns List of indexes for the specified table
+ */
+export async function listTableIndexes(tableName: string) {
+ try {
+ if (!tableName) {
+ throw new Error("Table name is required");
+ }
+
+ return await listIndexes(tableName);
+ } catch (error: any) {
+ throw new Error(`Error listing table indexes: ${error.message}`);
+ }
+}
+
+/**
+ * Get detailed information about a specific index
+ * @param indexName Name of the index
+ * @param tableName Optional table name for disambiguation
+ * @returns Detailed information about the index
+ */
+export async function describeIndex(indexName: string, tableName?: string) {
+ try {
+ if (!indexName) {
+ throw new Error("Index name is required");
+ }
+
+ const dbInfo = getDatabaseMetadata();
+ const dbType = dbInfo.type;
+
+ const query = getDescribeIndexQuery(indexName, tableName);
+
+ const params: any[] = [indexName];
+ if (tableName) {
+ params.push(tableName);
+ }
+
+ const results = await dbAll(query, params);
+
+ if (results.length === 0) {
+ throw new Error(
+ `Index '${indexName}' not found${
+ tableName ? ` in table '${tableName}'` : ""
+ }`
+ );
+ }
+
+ const indexDetails = await processIndexDetails(results, dbType, indexName);
+
+ return formatSuccessResponse(indexDetails);
+ } catch (error: any) {
+ throw new Error(`Error describing index: ${error.message}`);
+ }
+}
+
+/**
+ * Process index results based on database type
+ * @param results Raw query results
+ * @param dbType Database type
+ * @returns Processed index information
+ */
+async function processIndexResults(
+ results: any[],
+ dbType: string
+): Promise {
+ const indexMap = new Map();
+
+ switch (dbType) {
+ case "sqlite":
+ for (const row of results) {
+ const indexInfo: IndexInfo = {
+ name: row.name,
+ table_name: row.table_name,
+ columns: [],
+ unique: !!row.is_unique,
+ type: "btree", // SQLite uses B-tree by default
+ };
+
+ // Get column information for SQLite index
+ try {
+ const columnQuery = `PRAGMA index_info(${row.name})`;
+ const columnResults = await dbAll(columnQuery);
+ indexInfo.columns = columnResults.map((col: any) => col.name);
+ } catch (error) {
+ // If PRAGMA fails, try to parse from SQL
+ if (row.sql) {
+ const match = row.sql.match(/\((.*?)\)/);
+ if (match) {
+ indexInfo.columns = match[1]
+ .split(",")
+ .map((col: string) => col.trim().replace(/["`]/g, ""));
+ }
+ }
+ }
+
+ indexMap.set(row.name, indexInfo);
+ }
+ break;
+
+ case "postgresql":
+ for (const row of results) {
+ const indexInfo: IndexInfo = {
+ name: row.name,
+ table_name: row.table_name,
+ columns: [],
+ unique: !!row.is_unique,
+ type: row.method || "btree",
+ primary: !!row.is_primary,
+ };
+
+ // Extract columns from definition
+ if (row.definition) {
+ const match = row.definition.match(/\((.*?)\)/);
+ if (match) {
+ indexInfo.columns = match[1]
+ .split(",")
+ .map((col: string) => col.trim());
+ }
+ }
+
+ indexMap.set(row.name, indexInfo);
+ }
+ break;
+
+ case "mysql":
+ case "sqlserver":
+ // Group by index name and collect columns
+ for (const row of results) {
+ const indexName = row.name || row.index_name;
+ const tableName = row.table_name;
+
+ if (!indexMap.has(indexName)) {
+ indexMap.set(indexName, {
+ name: indexName,
+ table_name: tableName,
+ columns: [],
+ unique: !!row.is_unique,
+ type: row.type || "btree",
+ primary: !!row.is_primary_key,
+ });
+ }
+
+ const indexInfo = indexMap.get(indexName)!;
+ if (row.column_name && !indexInfo.columns.includes(row.column_name)) {
+ indexInfo.columns.push(row.column_name);
+ }
+ }
+ break;
+ }
+
+ return Array.from(indexMap.values());
+}
+
+/**
+ * Process detailed index information
+ * @param results Raw query results
+ * @param dbType Database type
+ * @param indexName Index name
+ * @returns Detailed index information
+ */
+async function processIndexDetails(
+ results: any[],
+ dbType: string,
+ indexName: string
+): Promise {
+ const firstRow = results[0];
+
+ const details: any = {
+ name: indexName,
+ table_name: firstRow.table_name,
+ columns: [],
+ unique: false,
+ type: "btree",
+ };
+
+ switch (dbType) {
+ case "sqlite":
+ details.unique = !!firstRow.is_unique;
+ details.definition = firstRow.sql;
+
+ // Get column information
+ try {
+ const columnQuery = `PRAGMA index_info(${indexName})`;
+ const columnResults = await dbAll(columnQuery);
+ details.columns = columnResults.map((col: any) => ({
+ name: col.name,
+ position: col.seqno,
+ }));
+ } catch (error) {
+ details.columns = [];
+ }
+ break;
+
+ case "postgresql":
+ details.unique = !!firstRow.is_unique;
+ details.primary = !!firstRow.is_primary;
+ details.method = firstRow.method;
+ details.definition = firstRow.full_definition || firstRow.definition;
+
+ // Process columns with their types (key vs included)
+ const pgKeyColumns = results
+ .filter((row: any) => row.column_type === 'key')
+ .map((row: any) => ({
+ name: row.column_name,
+ position: row.column_position,
+ type: 'key'
+ }));
+
+ const pgIncludedColumns = results
+ .filter((row: any) => row.column_type === 'included')
+ .map((row: any) => ({
+ name: row.column_name,
+ position: row.column_position,
+ type: 'included'
+ }));
+
+ details.columns = pgKeyColumns;
+ if (pgIncludedColumns.length > 0) {
+ details.included_columns = pgIncludedColumns;
+ }
+ break;
+
+ case "mysql":
+ details.unique = !!firstRow.is_unique;
+ details.type = firstRow.type;
+ details.comment = firstRow.comment;
+
+ details.columns = results.map((row: any) => ({
+ name: row.column_name,
+ position: row.column_position,
+ cardinality: row.CARDINALITY,
+ sub_part: row.SUB_PART,
+ nullable: row.NULLABLE,
+ }));
+ break;
+
+ case "sqlserver":
+ details.unique = !!firstRow.is_unique;
+ details.primary = !!firstRow.is_primary_key;
+ details.type = firstRow.type;
+ details.fill_factor = firstRow.fill_factor;
+ details.is_padded = !!firstRow.is_padded;
+ details.is_disabled = !!firstRow.is_disabled;
+ details.allow_row_locks = !!firstRow.allow_row_locks;
+ details.allow_page_locks = !!firstRow.allow_page_locks;
+
+ // Separate key columns from included columns
+ const keyColumns = results
+ .filter((row: any) => row.column_type === 'key')
+ .map((row: any) => ({
+ name: row.column_name,
+ position: row.column_position,
+ is_descending: !!row.is_descending_key,
+ type: 'key'
+ }));
+
+ const includedColumns = results
+ .filter((row: any) => row.column_type === 'included')
+ .map((row: any) => ({
+ name: row.column_name,
+ position: row.column_position,
+ is_descending: !!row.is_descending_key,
+ type: 'included'
+ }));
+
+ details.columns = keyColumns;
+ details.included_columns = includedColumns;
+ break;
+ }
+
+ return details;
+}