/** * get_index_stats - Gets detailed statistics for a specific index. * * Shows usage counts, size information, and effectiveness metrics. */ import { z } from 'zod'; import type { ToolContext, ToolPrivilegeLevel } from './types.js'; import { executeSqlWithFallback, isSqlErrorResponse } from './utils.js'; // SQL identifier validation - prevents SQL injection via identifier names const identifierPattern = /^[a-zA-Z_][a-zA-Z0-9_$]*$/; // Output schema for index stats const GetIndexStatsOutputSchema = z.object({ schema_name: z.string(), table_name: z.string(), index_name: z.string(), index_type: z.string(), is_unique: z.boolean(), is_primary: z.boolean(), is_valid: z.boolean(), number_of_scans: z.number(), tuples_read: z.number(), tuples_fetched: z.number(), index_size: z.string(), table_size: z.string(), index_size_bytes: z.number(), table_size_bytes: z.number(), usage_ratio: z.string().nullable(), // Percentage of table accesses that used this index }); // Input schema const GetIndexStatsInputSchema = z.object({ schema: z.string().default('public').describe('Schema name (defaults to public).'), index_name: z.string().describe('Name of the index.'), }); type GetIndexStatsInput = z.infer; // Static JSON Schema for MCP capabilities const mcpInputSchema = { type: 'object', properties: { schema: { type: 'string', default: 'public', description: 'Schema name (defaults to public).', }, index_name: { type: 'string', description: 'Name of the index.', }, }, required: ['index_name'], }; export const getIndexStatsTool = { name: 'get_index_stats', description: 'Gets detailed statistics for a specific index including usage counts and size.', privilegeLevel: 'regular' as ToolPrivilegeLevel, inputSchema: GetIndexStatsInputSchema, mcpInputSchema: mcpInputSchema, outputSchema: GetIndexStatsOutputSchema, execute: async (input: GetIndexStatsInput, context: ToolContext) => { const client = context.selfhostedClient; const { schema, index_name } = input; // SECURITY: Validate identifiers to prevent SQL injection if (!identifierPattern.test(schema)) { throw new Error(`Invalid schema name: ${schema}. Must be a valid SQL identifier.`); } if (!identifierPattern.test(index_name)) { throw new Error(`Invalid index name: ${index_name}. Must be a valid SQL identifier.`); } const sql = ` SELECT s.schemaname AS schema_name, s.relname AS table_name, s.indexrelname AS index_name, am.amname AS index_type, i.indisunique AS is_unique, i.indisprimary AS is_primary, i.indisvalid AS is_valid, COALESCE(s.idx_scan, 0)::bigint AS number_of_scans, COALESCE(s.idx_tup_read, 0)::bigint AS tuples_read, COALESCE(s.idx_tup_fetch, 0)::bigint AS tuples_fetched, pg_size_pretty(pg_relation_size(s.indexrelid)) AS index_size, pg_size_pretty(pg_relation_size(s.relid)) AS table_size, pg_relation_size(s.indexrelid)::bigint AS index_size_bytes, pg_relation_size(s.relid)::bigint AS table_size_bytes, CASE WHEN (st.seq_scan + st.idx_scan) > 0 THEN ROUND((st.idx_scan::numeric / (st.seq_scan + st.idx_scan)::numeric) * 100, 2)::text || '%' ELSE NULL END AS usage_ratio FROM pg_stat_user_indexes s JOIN pg_catalog.pg_index i ON i.indexrelid = s.indexrelid JOIN pg_catalog.pg_class c ON c.oid = s.indexrelid JOIN pg_catalog.pg_am am ON am.oid = c.relam JOIN pg_stat_user_tables st ON st.relid = s.relid WHERE s.schemaname = '${schema}' AND s.indexrelname = '${index_name}' LIMIT 1 `; const result = await executeSqlWithFallback(client, sql, true); if (isSqlErrorResponse(result)) { throw new Error(result.error.message || 'Failed to get index stats'); } const rows = result as unknown[]; if (rows.length === 0) { throw new Error(`Index "${index_name}" not found in schema "${schema}".`); } return GetIndexStatsOutputSchema.parse(rows[0]); }, };