@neverinfamous/postgres-mcp 1.1.0 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (243) hide show
  1. package/README.md +95 -81
  2. package/dist/__tests__/mocks/adapter.d.ts.map +1 -1
  3. package/dist/__tests__/mocks/adapter.js +0 -1
  4. package/dist/__tests__/mocks/adapter.js.map +1 -1
  5. package/dist/__tests__/mocks/pool.d.ts.map +1 -1
  6. package/dist/__tests__/mocks/pool.js +0 -1
  7. package/dist/__tests__/mocks/pool.js.map +1 -1
  8. package/dist/adapters/DatabaseAdapter.js +1 -1
  9. package/dist/adapters/DatabaseAdapter.js.map +1 -1
  10. package/dist/adapters/postgresql/PostgresAdapter.d.ts.map +1 -1
  11. package/dist/adapters/postgresql/PostgresAdapter.js +78 -8
  12. package/dist/adapters/postgresql/PostgresAdapter.js.map +1 -1
  13. package/dist/adapters/postgresql/prompts/backup.d.ts.map +1 -1
  14. package/dist/adapters/postgresql/prompts/backup.js +2 -3
  15. package/dist/adapters/postgresql/prompts/backup.js.map +1 -1
  16. package/dist/adapters/postgresql/prompts/citext.d.ts.map +1 -1
  17. package/dist/adapters/postgresql/prompts/citext.js +3 -4
  18. package/dist/adapters/postgresql/prompts/citext.js.map +1 -1
  19. package/dist/adapters/postgresql/prompts/extensionSetup.d.ts.map +1 -1
  20. package/dist/adapters/postgresql/prompts/extensionSetup.js +2 -3
  21. package/dist/adapters/postgresql/prompts/extensionSetup.js.map +1 -1
  22. package/dist/adapters/postgresql/prompts/health.d.ts.map +1 -1
  23. package/dist/adapters/postgresql/prompts/health.js +2 -3
  24. package/dist/adapters/postgresql/prompts/health.js.map +1 -1
  25. package/dist/adapters/postgresql/prompts/index.js +20 -27
  26. package/dist/adapters/postgresql/prompts/index.js.map +1 -1
  27. package/dist/adapters/postgresql/prompts/indexTuning.d.ts.map +1 -1
  28. package/dist/adapters/postgresql/prompts/indexTuning.js +2 -3
  29. package/dist/adapters/postgresql/prompts/indexTuning.js.map +1 -1
  30. package/dist/adapters/postgresql/prompts/kcache.d.ts.map +1 -1
  31. package/dist/adapters/postgresql/prompts/kcache.js +3 -4
  32. package/dist/adapters/postgresql/prompts/kcache.js.map +1 -1
  33. package/dist/adapters/postgresql/prompts/ltree.d.ts.map +1 -1
  34. package/dist/adapters/postgresql/prompts/ltree.js +5 -6
  35. package/dist/adapters/postgresql/prompts/ltree.js.map +1 -1
  36. package/dist/adapters/postgresql/prompts/partman.d.ts.map +1 -1
  37. package/dist/adapters/postgresql/prompts/partman.js +2 -3
  38. package/dist/adapters/postgresql/prompts/partman.js.map +1 -1
  39. package/dist/adapters/postgresql/prompts/pgcron.d.ts.map +1 -1
  40. package/dist/adapters/postgresql/prompts/pgcron.js +2 -3
  41. package/dist/adapters/postgresql/prompts/pgcron.js.map +1 -1
  42. package/dist/adapters/postgresql/prompts/pgcrypto.d.ts.map +1 -1
  43. package/dist/adapters/postgresql/prompts/pgcrypto.js +3 -4
  44. package/dist/adapters/postgresql/prompts/pgcrypto.js.map +1 -1
  45. package/dist/adapters/postgresql/prompts/pgvector.d.ts.map +1 -1
  46. package/dist/adapters/postgresql/prompts/pgvector.js +3 -4
  47. package/dist/adapters/postgresql/prompts/pgvector.js.map +1 -1
  48. package/dist/adapters/postgresql/prompts/postgis.d.ts.map +1 -1
  49. package/dist/adapters/postgresql/prompts/postgis.js +2 -3
  50. package/dist/adapters/postgresql/prompts/postgis.js.map +1 -1
  51. package/dist/adapters/postgresql/schemas/admin.d.ts +10 -5
  52. package/dist/adapters/postgresql/schemas/admin.d.ts.map +1 -1
  53. package/dist/adapters/postgresql/schemas/admin.js +10 -5
  54. package/dist/adapters/postgresql/schemas/admin.js.map +1 -1
  55. package/dist/adapters/postgresql/schemas/backup.d.ts +8 -4
  56. package/dist/adapters/postgresql/schemas/backup.d.ts.map +1 -1
  57. package/dist/adapters/postgresql/schemas/backup.js +11 -4
  58. package/dist/adapters/postgresql/schemas/backup.js.map +1 -1
  59. package/dist/adapters/postgresql/schemas/core.d.ts +54 -19
  60. package/dist/adapters/postgresql/schemas/core.d.ts.map +1 -1
  61. package/dist/adapters/postgresql/schemas/core.js +65 -17
  62. package/dist/adapters/postgresql/schemas/core.js.map +1 -1
  63. package/dist/adapters/postgresql/schemas/cron.d.ts +51 -32
  64. package/dist/adapters/postgresql/schemas/cron.d.ts.map +1 -1
  65. package/dist/adapters/postgresql/schemas/cron.js +64 -44
  66. package/dist/adapters/postgresql/schemas/cron.js.map +1 -1
  67. package/dist/adapters/postgresql/schemas/extensions.d.ts +168 -73
  68. package/dist/adapters/postgresql/schemas/extensions.d.ts.map +1 -1
  69. package/dist/adapters/postgresql/schemas/extensions.js +179 -62
  70. package/dist/adapters/postgresql/schemas/extensions.js.map +1 -1
  71. package/dist/adapters/postgresql/schemas/index.d.ts +5 -5
  72. package/dist/adapters/postgresql/schemas/index.d.ts.map +1 -1
  73. package/dist/adapters/postgresql/schemas/index.js +9 -7
  74. package/dist/adapters/postgresql/schemas/index.js.map +1 -1
  75. package/dist/adapters/postgresql/schemas/jsonb.d.ts +94 -42
  76. package/dist/adapters/postgresql/schemas/jsonb.d.ts.map +1 -1
  77. package/dist/adapters/postgresql/schemas/jsonb.js +101 -30
  78. package/dist/adapters/postgresql/schemas/jsonb.js.map +1 -1
  79. package/dist/adapters/postgresql/schemas/monitoring.d.ts +28 -11
  80. package/dist/adapters/postgresql/schemas/monitoring.d.ts.map +1 -1
  81. package/dist/adapters/postgresql/schemas/monitoring.js +49 -24
  82. package/dist/adapters/postgresql/schemas/monitoring.js.map +1 -1
  83. package/dist/adapters/postgresql/schemas/partitioning.d.ts +15 -11
  84. package/dist/adapters/postgresql/schemas/partitioning.d.ts.map +1 -1
  85. package/dist/adapters/postgresql/schemas/partitioning.js +17 -13
  86. package/dist/adapters/postgresql/schemas/partitioning.js.map +1 -1
  87. package/dist/adapters/postgresql/schemas/performance.d.ts +62 -31
  88. package/dist/adapters/postgresql/schemas/performance.d.ts.map +1 -1
  89. package/dist/adapters/postgresql/schemas/performance.js +86 -24
  90. package/dist/adapters/postgresql/schemas/performance.js.map +1 -1
  91. package/dist/adapters/postgresql/schemas/postgis.d.ts +20 -0
  92. package/dist/adapters/postgresql/schemas/postgis.d.ts.map +1 -1
  93. package/dist/adapters/postgresql/schemas/postgis.js +20 -3
  94. package/dist/adapters/postgresql/schemas/postgis.js.map +1 -1
  95. package/dist/adapters/postgresql/schemas/schema-mgmt.d.ts +35 -23
  96. package/dist/adapters/postgresql/schemas/schema-mgmt.d.ts.map +1 -1
  97. package/dist/adapters/postgresql/schemas/schema-mgmt.js +69 -26
  98. package/dist/adapters/postgresql/schemas/schema-mgmt.js.map +1 -1
  99. package/dist/adapters/postgresql/schemas/stats.d.ts +33 -20
  100. package/dist/adapters/postgresql/schemas/stats.d.ts.map +1 -1
  101. package/dist/adapters/postgresql/schemas/stats.js +36 -20
  102. package/dist/adapters/postgresql/schemas/stats.js.map +1 -1
  103. package/dist/adapters/postgresql/schemas/text-search.d.ts +8 -5
  104. package/dist/adapters/postgresql/schemas/text-search.d.ts.map +1 -1
  105. package/dist/adapters/postgresql/schemas/text-search.js +15 -5
  106. package/dist/adapters/postgresql/schemas/text-search.js.map +1 -1
  107. package/dist/adapters/postgresql/tools/admin.d.ts.map +1 -1
  108. package/dist/adapters/postgresql/tools/admin.js +211 -140
  109. package/dist/adapters/postgresql/tools/admin.js.map +1 -1
  110. package/dist/adapters/postgresql/tools/backup/dump.d.ts.map +1 -1
  111. package/dist/adapters/postgresql/tools/backup/dump.js +410 -387
  112. package/dist/adapters/postgresql/tools/backup/dump.js.map +1 -1
  113. package/dist/adapters/postgresql/tools/backup/planning.d.ts.map +1 -1
  114. package/dist/adapters/postgresql/tools/backup/planning.js +175 -172
  115. package/dist/adapters/postgresql/tools/backup/planning.js.map +1 -1
  116. package/dist/adapters/postgresql/tools/citext.d.ts.map +1 -1
  117. package/dist/adapters/postgresql/tools/citext.js +221 -163
  118. package/dist/adapters/postgresql/tools/citext.js.map +1 -1
  119. package/dist/adapters/postgresql/tools/core/convenience.d.ts +9 -1
  120. package/dist/adapters/postgresql/tools/core/convenience.d.ts.map +1 -1
  121. package/dist/adapters/postgresql/tools/core/convenience.js +96 -9
  122. package/dist/adapters/postgresql/tools/core/convenience.js.map +1 -1
  123. package/dist/adapters/postgresql/tools/core/error-helpers.d.ts +48 -0
  124. package/dist/adapters/postgresql/tools/core/error-helpers.d.ts.map +1 -0
  125. package/dist/adapters/postgresql/tools/core/error-helpers.js +256 -0
  126. package/dist/adapters/postgresql/tools/core/error-helpers.js.map +1 -0
  127. package/dist/adapters/postgresql/tools/core/health.d.ts.map +1 -1
  128. package/dist/adapters/postgresql/tools/core/health.js +23 -6
  129. package/dist/adapters/postgresql/tools/core/health.js.map +1 -1
  130. package/dist/adapters/postgresql/tools/core/indexes.d.ts.map +1 -1
  131. package/dist/adapters/postgresql/tools/core/indexes.js +45 -4
  132. package/dist/adapters/postgresql/tools/core/indexes.js.map +1 -1
  133. package/dist/adapters/postgresql/tools/core/objects.d.ts.map +1 -1
  134. package/dist/adapters/postgresql/tools/core/objects.js +104 -85
  135. package/dist/adapters/postgresql/tools/core/objects.js.map +1 -1
  136. package/dist/adapters/postgresql/tools/core/query.d.ts.map +1 -1
  137. package/dist/adapters/postgresql/tools/core/query.js +100 -42
  138. package/dist/adapters/postgresql/tools/core/query.js.map +1 -1
  139. package/dist/adapters/postgresql/tools/core/schemas.d.ts +52 -25
  140. package/dist/adapters/postgresql/tools/core/schemas.d.ts.map +1 -1
  141. package/dist/adapters/postgresql/tools/core/schemas.js +55 -25
  142. package/dist/adapters/postgresql/tools/core/schemas.js.map +1 -1
  143. package/dist/adapters/postgresql/tools/core/tables.d.ts.map +1 -1
  144. package/dist/adapters/postgresql/tools/core/tables.js +74 -30
  145. package/dist/adapters/postgresql/tools/core/tables.js.map +1 -1
  146. package/dist/adapters/postgresql/tools/cron.d.ts.map +1 -1
  147. package/dist/adapters/postgresql/tools/cron.js +274 -179
  148. package/dist/adapters/postgresql/tools/cron.js.map +1 -1
  149. package/dist/adapters/postgresql/tools/jsonb/advanced.d.ts.map +1 -1
  150. package/dist/adapters/postgresql/tools/jsonb/advanced.js +372 -284
  151. package/dist/adapters/postgresql/tools/jsonb/advanced.js.map +1 -1
  152. package/dist/adapters/postgresql/tools/jsonb/basic.d.ts.map +1 -1
  153. package/dist/adapters/postgresql/tools/jsonb/basic.js +617 -398
  154. package/dist/adapters/postgresql/tools/jsonb/basic.js.map +1 -1
  155. package/dist/adapters/postgresql/tools/kcache.d.ts.map +1 -1
  156. package/dist/adapters/postgresql/tools/kcache.js +282 -220
  157. package/dist/adapters/postgresql/tools/kcache.js.map +1 -1
  158. package/dist/adapters/postgresql/tools/ltree.d.ts.map +1 -1
  159. package/dist/adapters/postgresql/tools/ltree.js +126 -35
  160. package/dist/adapters/postgresql/tools/ltree.js.map +1 -1
  161. package/dist/adapters/postgresql/tools/monitoring.d.ts.map +1 -1
  162. package/dist/adapters/postgresql/tools/monitoring.js +59 -40
  163. package/dist/adapters/postgresql/tools/monitoring.js.map +1 -1
  164. package/dist/adapters/postgresql/tools/partitioning.d.ts.map +1 -1
  165. package/dist/adapters/postgresql/tools/partitioning.js +150 -15
  166. package/dist/adapters/postgresql/tools/partitioning.js.map +1 -1
  167. package/dist/adapters/postgresql/tools/partman/management.d.ts.map +1 -1
  168. package/dist/adapters/postgresql/tools/partman/management.js +12 -5
  169. package/dist/adapters/postgresql/tools/partman/management.js.map +1 -1
  170. package/dist/adapters/postgresql/tools/partman/operations.d.ts.map +1 -1
  171. package/dist/adapters/postgresql/tools/partman/operations.js +135 -22
  172. package/dist/adapters/postgresql/tools/partman/operations.js.map +1 -1
  173. package/dist/adapters/postgresql/tools/performance/analysis.d.ts.map +1 -1
  174. package/dist/adapters/postgresql/tools/performance/analysis.js +264 -160
  175. package/dist/adapters/postgresql/tools/performance/analysis.js.map +1 -1
  176. package/dist/adapters/postgresql/tools/performance/explain.d.ts.map +1 -1
  177. package/dist/adapters/postgresql/tools/performance/explain.js +61 -21
  178. package/dist/adapters/postgresql/tools/performance/explain.js.map +1 -1
  179. package/dist/adapters/postgresql/tools/performance/monitoring.d.ts.map +1 -1
  180. package/dist/adapters/postgresql/tools/performance/monitoring.js +44 -7
  181. package/dist/adapters/postgresql/tools/performance/monitoring.js.map +1 -1
  182. package/dist/adapters/postgresql/tools/performance/optimization.d.ts.map +1 -1
  183. package/dist/adapters/postgresql/tools/performance/optimization.js +92 -81
  184. package/dist/adapters/postgresql/tools/performance/optimization.js.map +1 -1
  185. package/dist/adapters/postgresql/tools/performance/stats.d.ts.map +1 -1
  186. package/dist/adapters/postgresql/tools/performance/stats.js +128 -37
  187. package/dist/adapters/postgresql/tools/performance/stats.js.map +1 -1
  188. package/dist/adapters/postgresql/tools/pgcrypto.d.ts.map +1 -1
  189. package/dist/adapters/postgresql/tools/pgcrypto.js +242 -87
  190. package/dist/adapters/postgresql/tools/pgcrypto.js.map +1 -1
  191. package/dist/adapters/postgresql/tools/postgis/advanced.d.ts.map +1 -1
  192. package/dist/adapters/postgresql/tools/postgis/advanced.js +293 -201
  193. package/dist/adapters/postgresql/tools/postgis/advanced.js.map +1 -1
  194. package/dist/adapters/postgresql/tools/postgis/basic.d.ts.map +1 -1
  195. package/dist/adapters/postgresql/tools/postgis/basic.js +359 -249
  196. package/dist/adapters/postgresql/tools/postgis/basic.js.map +1 -1
  197. package/dist/adapters/postgresql/tools/postgis/standalone.d.ts.map +1 -1
  198. package/dist/adapters/postgresql/tools/postgis/standalone.js +135 -51
  199. package/dist/adapters/postgresql/tools/postgis/standalone.js.map +1 -1
  200. package/dist/adapters/postgresql/tools/schema.d.ts.map +1 -1
  201. package/dist/adapters/postgresql/tools/schema.js +515 -226
  202. package/dist/adapters/postgresql/tools/schema.js.map +1 -1
  203. package/dist/adapters/postgresql/tools/stats/advanced.d.ts.map +1 -1
  204. package/dist/adapters/postgresql/tools/stats/advanced.js +515 -476
  205. package/dist/adapters/postgresql/tools/stats/advanced.js.map +1 -1
  206. package/dist/adapters/postgresql/tools/stats/basic.d.ts.map +1 -1
  207. package/dist/adapters/postgresql/tools/stats/basic.js +302 -293
  208. package/dist/adapters/postgresql/tools/stats/basic.js.map +1 -1
  209. package/dist/adapters/postgresql/tools/text.d.ts.map +1 -1
  210. package/dist/adapters/postgresql/tools/text.js +398 -220
  211. package/dist/adapters/postgresql/tools/text.js.map +1 -1
  212. package/dist/adapters/postgresql/tools/transactions.d.ts.map +1 -1
  213. package/dist/adapters/postgresql/tools/transactions.js +157 -50
  214. package/dist/adapters/postgresql/tools/transactions.js.map +1 -1
  215. package/dist/adapters/postgresql/tools/vector/advanced.d.ts.map +1 -1
  216. package/dist/adapters/postgresql/tools/vector/advanced.js +70 -38
  217. package/dist/adapters/postgresql/tools/vector/advanced.js.map +1 -1
  218. package/dist/adapters/postgresql/tools/vector/basic.d.ts +8 -0
  219. package/dist/adapters/postgresql/tools/vector/basic.d.ts.map +1 -1
  220. package/dist/adapters/postgresql/tools/vector/basic.js +194 -82
  221. package/dist/adapters/postgresql/tools/vector/basic.js.map +1 -1
  222. package/dist/cli/args.d.ts +2 -0
  223. package/dist/cli/args.d.ts.map +1 -1
  224. package/dist/cli/args.js +15 -0
  225. package/dist/cli/args.js.map +1 -1
  226. package/dist/cli.js +7 -6
  227. package/dist/cli.js.map +1 -1
  228. package/dist/codemode/api.d.ts.map +1 -1
  229. package/dist/codemode/api.js +4 -3
  230. package/dist/codemode/api.js.map +1 -1
  231. package/dist/constants/ServerInstructions.d.ts +1 -1
  232. package/dist/constants/ServerInstructions.d.ts.map +1 -1
  233. package/dist/constants/ServerInstructions.js +76 -34
  234. package/dist/constants/ServerInstructions.js.map +1 -1
  235. package/dist/filtering/ToolConstants.d.ts +29 -13
  236. package/dist/filtering/ToolConstants.d.ts.map +1 -1
  237. package/dist/filtering/ToolConstants.js +44 -27
  238. package/dist/filtering/ToolConstants.js.map +1 -1
  239. package/dist/utils/logger.js +2 -2
  240. package/dist/utils/logger.js.map +1 -1
  241. package/dist/utils/progress-utils.js +1 -1
  242. package/dist/utils/progress-utils.js.map +1 -1
  243. package/package.json +13 -9
@@ -4,9 +4,10 @@
4
4
  * Text processing, FTS, trigrams, and fuzzy matching.
5
5
  * 14 tools total.
6
6
  */
7
- import { z } from "zod";
7
+ import { z, ZodError } from "zod";
8
8
  import { readOnly, write } from "../../../utils/annotations.js";
9
9
  import { getToolIcons } from "../../../utils/icons.js";
10
+ import { formatPostgresError } from "./core/error-helpers.js";
10
11
  import { sanitizeIdentifier, sanitizeIdentifiers, sanitizeTableName, } from "../../../utils/identifiers.js";
11
12
  import { sanitizeFtsConfig } from "../../../utils/fts-config.js";
12
13
  import { sanitizeWhereClause } from "../../../utils/where-clause.js";
@@ -43,43 +44,65 @@ function createTextSearchTool(adapter) {
43
44
  annotations: readOnly("Full-Text Search"),
44
45
  icons: getToolIcons("text", readOnly("Full-Text Search")),
45
46
  handler: async (params, _context) => {
46
- const parsed = TextSearchSchema.parse(params);
47
- const cfg = sanitizeFtsConfig(parsed.config ?? "english");
48
- // Handle both column (string) and columns (array) parameters
49
- // The preprocessor converts column columns, but we handle both for safety
50
- let cols;
51
- if (parsed.columns !== undefined && parsed.columns.length > 0) {
52
- cols = parsed.columns;
53
- }
54
- else if (parsed.column !== undefined) {
55
- cols = [parsed.column];
56
- }
57
- else {
58
- throw new Error("Either 'columns' (array) or 'column' (string) is required");
59
- }
60
- // Build qualified table name with schema support
61
- // The preprocessor guarantees table is set (converts tableName → table)
62
- const resolvedTable = parsed.table ?? parsed.tableName;
63
- if (!resolvedTable) {
64
- throw new Error("Either 'table' or 'tableName' is required");
65
- }
66
- const tableName = sanitizeTableName(resolvedTable, parsed.schema);
67
- const sanitizedCols = sanitizeIdentifiers(cols);
68
- const selectCols = parsed.select !== undefined && parsed.select.length > 0
69
- ? sanitizeIdentifiers(parsed.select).join(", ")
70
- : "*";
71
- const tsvector = sanitizedCols
72
- .map((c) => `coalesce(${c}, '')`)
73
- .join(" || ' ' || ");
74
- const limitClause = parsed.limit !== undefined && parsed.limit > 0
75
- ? ` LIMIT ${String(parsed.limit)}`
76
- : "";
77
- const sql = `SELECT ${selectCols}, ts_rank_cd(to_tsvector('${cfg}', ${tsvector}), plainto_tsquery('${cfg}', $1)) as rank
47
+ try {
48
+ const parsed = TextSearchSchema.parse(params);
49
+ const cfg = sanitizeFtsConfig(parsed.config ?? "english");
50
+ // Handle both column (string) and columns (array) parameters
51
+ // The preprocessor converts column → columns, but we handle both for safety
52
+ let cols;
53
+ if (parsed.columns !== undefined && parsed.columns.length > 0) {
54
+ cols = parsed.columns;
55
+ }
56
+ else if (parsed.column !== undefined) {
57
+ cols = [parsed.column];
58
+ }
59
+ else {
60
+ return {
61
+ success: false,
62
+ error: "Either 'columns' (array) or 'column' (string) is required",
63
+ };
64
+ }
65
+ // Build qualified table name with schema support
66
+ // The preprocessor guarantees table is set (converts tableName → table)
67
+ const resolvedTable = parsed.table ?? parsed.tableName;
68
+ if (!resolvedTable) {
69
+ return {
70
+ success: false,
71
+ error: "Either 'table' or 'tableName' is required",
72
+ };
73
+ }
74
+ const tableName = sanitizeTableName(resolvedTable, parsed.schema);
75
+ const sanitizedCols = sanitizeIdentifiers(cols);
76
+ const selectCols = parsed.select !== undefined && parsed.select.length > 0
77
+ ? sanitizeIdentifiers(parsed.select).join(", ")
78
+ : "*";
79
+ const tsvector = sanitizedCols
80
+ .map((c) => `coalesce(${c}, '')`)
81
+ .join(" || ' ' || ");
82
+ const limitClause = parsed.limit !== undefined && parsed.limit > 0
83
+ ? ` LIMIT ${String(parsed.limit)}`
84
+ : "";
85
+ const sql = `SELECT ${selectCols}, ts_rank_cd(to_tsvector('${cfg}', ${tsvector}), plainto_tsquery('${cfg}', $1)) as rank
78
86
  FROM ${tableName}
79
87
  WHERE to_tsvector('${cfg}', ${tsvector}) @@ plainto_tsquery('${cfg}', $1)
80
88
  ORDER BY rank DESC${limitClause}`;
81
- const result = await adapter.executeQuery(sql, [parsed.query]);
82
- return { rows: result.rows, count: result.rows?.length ?? 0 };
89
+ const result = await adapter.executeQuery(sql, [parsed.query]);
90
+ return { rows: result.rows, count: result.rows?.length ?? 0 };
91
+ }
92
+ catch (error) {
93
+ if (error instanceof ZodError) {
94
+ return {
95
+ success: false,
96
+ error: `pg_text_search validation error: ${error.issues.map((e) => e.message).join(", ")}`,
97
+ };
98
+ }
99
+ return {
100
+ success: false,
101
+ error: formatPostgresError(error, {
102
+ tool: "pg_text_search",
103
+ }),
104
+ };
105
+ }
83
106
  },
84
107
  };
85
108
  }
@@ -115,42 +138,64 @@ function createTextRankTool(adapter) {
115
138
  annotations: readOnly("Text Rank"),
116
139
  icons: getToolIcons("text", readOnly("Text Rank")),
117
140
  handler: async (params, _context) => {
118
- const parsed = TextRankSchema.parse(params);
119
- const cfg = sanitizeFtsConfig(parsed.config ?? "english");
120
- const norm = parsed.normalization ?? 0;
121
- // Handle both column (string) and columns (array) parameters
122
- let cols;
123
- if (parsed.columns !== undefined && parsed.columns.length > 0) {
124
- cols = parsed.columns;
125
- }
126
- else if (parsed.column !== undefined) {
127
- cols = [parsed.column];
128
- }
129
- else {
130
- throw new Error("Either column or columns parameter is required");
131
- }
132
- // The preprocessor guarantees table is set (converts tableName → table)
133
- const resolvedTable = parsed.table ?? parsed.tableName;
134
- if (!resolvedTable) {
135
- throw new Error("Either 'table' or 'tableName' is required");
136
- }
137
- const tableName = sanitizeTableName(resolvedTable, parsed.schema);
138
- const sanitizedCols = sanitizeIdentifiers(cols);
139
- const selectCols = parsed.select !== undefined && parsed.select.length > 0
140
- ? sanitizeIdentifiers(parsed.select).join(", ")
141
- : "*";
142
- const tsvector = sanitizedCols
143
- .map((c) => `coalesce(${c}, '')`)
144
- .join(" || ' ' || ");
145
- const limitClause = parsed.limit !== undefined && parsed.limit > 0
146
- ? ` LIMIT ${String(parsed.limit)}`
147
- : "";
148
- const sql = `SELECT ${selectCols}, ts_rank_cd(to_tsvector('${cfg}', ${tsvector}), plainto_tsquery('${cfg}', $1), ${String(norm)}) as rank
141
+ try {
142
+ const parsed = TextRankSchema.parse(params);
143
+ const cfg = sanitizeFtsConfig(parsed.config ?? "english");
144
+ const norm = parsed.normalization ?? 0;
145
+ // Handle both column (string) and columns (array) parameters
146
+ let cols;
147
+ if (parsed.columns !== undefined && parsed.columns.length > 0) {
148
+ cols = parsed.columns;
149
+ }
150
+ else if (parsed.column !== undefined) {
151
+ cols = [parsed.column];
152
+ }
153
+ else {
154
+ return {
155
+ success: false,
156
+ error: "Either column or columns parameter is required",
157
+ };
158
+ }
159
+ // The preprocessor guarantees table is set (converts tableName → table)
160
+ const resolvedTable = parsed.table ?? parsed.tableName;
161
+ if (!resolvedTable) {
162
+ return {
163
+ success: false,
164
+ error: "Either 'table' or 'tableName' is required",
165
+ };
166
+ }
167
+ const tableName = sanitizeTableName(resolvedTable, parsed.schema);
168
+ const sanitizedCols = sanitizeIdentifiers(cols);
169
+ const selectCols = parsed.select !== undefined && parsed.select.length > 0
170
+ ? sanitizeIdentifiers(parsed.select).join(", ")
171
+ : "*";
172
+ const tsvector = sanitizedCols
173
+ .map((c) => `coalesce(${c}, '')`)
174
+ .join(" || ' ' || ");
175
+ const limitClause = parsed.limit !== undefined && parsed.limit > 0
176
+ ? ` LIMIT ${String(parsed.limit)}`
177
+ : "";
178
+ const sql = `SELECT ${selectCols}, ts_rank_cd(to_tsvector('${cfg}', ${tsvector}), plainto_tsquery('${cfg}', $1), ${String(norm)}) as rank
149
179
  FROM ${tableName}
150
180
  WHERE to_tsvector('${cfg}', ${tsvector}) @@ plainto_tsquery('${cfg}', $1)
151
181
  ORDER BY rank DESC${limitClause}`;
152
- const result = await adapter.executeQuery(sql, [parsed.query]);
153
- return { rows: result.rows, count: result.rows?.length ?? 0 };
182
+ const result = await adapter.executeQuery(sql, [parsed.query]);
183
+ return { rows: result.rows, count: result.rows?.length ?? 0 };
184
+ }
185
+ catch (error) {
186
+ if (error instanceof ZodError) {
187
+ return {
188
+ success: false,
189
+ error: `pg_text_rank validation error: ${error.issues.map((e) => e.message).join(", ")}`,
190
+ };
191
+ }
192
+ return {
193
+ success: false,
194
+ error: formatPostgresError(error, {
195
+ tool: "pg_text_rank",
196
+ }),
197
+ };
198
+ }
154
199
  },
155
200
  };
156
201
  }
@@ -164,29 +209,48 @@ function createTrigramSimilarityTool(adapter) {
164
209
  annotations: readOnly("Trigram Similarity"),
165
210
  icons: getToolIcons("text", readOnly("Trigram Similarity")),
166
211
  handler: async (params, _context) => {
167
- const parsed = TrigramSimilaritySchema.parse(params);
168
- const thresh = parsed.threshold ?? 0.3;
169
- // Default limit to 100 to prevent large payloads
170
- const limitVal = parsed.limit !== undefined && parsed.limit > 0 ? parsed.limit : 100;
171
- // The preprocessor guarantees table is set (converts tableName table)
172
- const resolvedTable = parsed.table ?? parsed.tableName;
173
- if (!resolvedTable) {
174
- throw new Error("Either 'table' or 'tableName' is required");
175
- }
176
- const tableName = sanitizeTableName(resolvedTable, parsed.schema);
177
- const columnName = sanitizeIdentifier(parsed.column);
178
- const selectCols = parsed.select !== undefined && parsed.select.length > 0
179
- ? sanitizeIdentifiers(parsed.select).join(", ")
180
- : "*";
181
- const additionalWhere = parsed.where
182
- ? ` AND (${sanitizeWhereClause(parsed.where)})`
183
- : "";
184
- const sql = `SELECT ${selectCols}, similarity(${columnName}, $1) as similarity
212
+ try {
213
+ const parsed = TrigramSimilaritySchema.parse(params);
214
+ const thresh = parsed.threshold ?? 0.3;
215
+ // Default limit to 100 to prevent large payloads
216
+ const limitVal = parsed.limit !== undefined && parsed.limit > 0 ? parsed.limit : 100;
217
+ // The preprocessor guarantees table is set (converts tableName → table)
218
+ const resolvedTable = parsed.table ?? parsed.tableName;
219
+ if (!resolvedTable) {
220
+ return {
221
+ success: false,
222
+ error: "Either 'table' or 'tableName' is required",
223
+ };
224
+ }
225
+ const tableName = sanitizeTableName(resolvedTable, parsed.schema);
226
+ const columnName = sanitizeIdentifier(parsed.column);
227
+ const selectCols = parsed.select !== undefined && parsed.select.length > 0
228
+ ? sanitizeIdentifiers(parsed.select).join(", ")
229
+ : "*";
230
+ const additionalWhere = parsed.where
231
+ ? ` AND (${sanitizeWhereClause(parsed.where)})`
232
+ : "";
233
+ const sql = `SELECT ${selectCols}, similarity(${columnName}, $1) as similarity
185
234
  FROM ${tableName}
186
235
  WHERE similarity(${columnName}, $1) > ${String(thresh)}${additionalWhere}
187
236
  ORDER BY similarity DESC LIMIT ${String(limitVal)}`;
188
- const result = await adapter.executeQuery(sql, [parsed.value]);
189
- return { rows: result.rows, count: result.rows?.length ?? 0 };
237
+ const result = await adapter.executeQuery(sql, [parsed.value]);
238
+ return { rows: result.rows, count: result.rows?.length ?? 0 };
239
+ }
240
+ catch (error) {
241
+ if (error instanceof ZodError) {
242
+ return {
243
+ success: false,
244
+ error: `pg_trigram_similarity validation error: ${error.issues.map((e) => e.message).join(", ")}`,
245
+ };
246
+ }
247
+ return {
248
+ success: false,
249
+ error: formatPostgresError(error, {
250
+ tool: "pg_trigram_similarity",
251
+ }),
252
+ };
253
+ }
190
254
  },
191
255
  };
192
256
  }
@@ -198,7 +262,10 @@ function createFuzzyMatchTool(adapter) {
198
262
  tableName: z.string().optional().describe("Table name (alias for table)"),
199
263
  column: z.string(),
200
264
  value: z.string(),
201
- method: z.enum(["soundex", "levenshtein", "metaphone"]).optional(),
265
+ method: z
266
+ .string()
267
+ .optional()
268
+ .describe("Fuzzy match method (default: levenshtein). Valid: soundex, levenshtein, metaphone"),
202
269
  maxDistance: z
203
270
  .number()
204
271
  .optional()
@@ -225,37 +292,68 @@ function createFuzzyMatchTool(adapter) {
225
292
  annotations: readOnly("Fuzzy Match"),
226
293
  icons: getToolIcons("text", readOnly("Fuzzy Match")),
227
294
  handler: async (params, _context) => {
228
- const parsed = FuzzyMatchSchema.parse(params);
229
- // Method is already validated by zod enum, default to levenshtein if not provided
230
- const method = parsed.method ?? "levenshtein";
231
- const maxDist = parsed.maxDistance ?? 3;
232
- // Default limit to 100 to prevent large payloads
233
- const limitVal = parsed.limit !== undefined && parsed.limit > 0 ? parsed.limit : 100;
234
- // The preprocessor guarantees table is set (converts tableName → table)
235
- const resolvedTable = parsed.table ?? parsed.tableName;
236
- if (!resolvedTable) {
237
- throw new Error("Either 'table' or 'tableName' is required");
295
+ try {
296
+ const parsed = FuzzyMatchSchema.parse(params);
297
+ // Validate method (moved from z.enum to handler for structured error)
298
+ const VALID_METHODS = [
299
+ "levenshtein",
300
+ "soundex",
301
+ "metaphone",
302
+ ];
303
+ const rawMethod = parsed.method ?? "levenshtein";
304
+ if (!VALID_METHODS.includes(rawMethod)) {
305
+ return {
306
+ success: false,
307
+ error: `Invalid method "${rawMethod}". Valid methods: ${VALID_METHODS.join(", ")}`,
308
+ };
309
+ }
310
+ const method = rawMethod;
311
+ const maxDist = parsed.maxDistance ?? 3;
312
+ // Default limit to 100 to prevent large payloads
313
+ const limitVal = parsed.limit !== undefined && parsed.limit > 0 ? parsed.limit : 100;
314
+ // The preprocessor guarantees table is set (converts tableName → table)
315
+ const resolvedTable = parsed.table ?? parsed.tableName;
316
+ if (!resolvedTable) {
317
+ return {
318
+ success: false,
319
+ error: "Either 'table' or 'tableName' is required",
320
+ };
321
+ }
322
+ const tableName = sanitizeTableName(resolvedTable, parsed.schema);
323
+ const columnName = sanitizeIdentifier(parsed.column);
324
+ const selectCols = parsed.select !== undefined && parsed.select.length > 0
325
+ ? sanitizeIdentifiers(parsed.select).join(", ")
326
+ : "*";
327
+ const additionalWhere = parsed.where
328
+ ? ` AND (${sanitizeWhereClause(parsed.where)})`
329
+ : "";
330
+ let sql;
331
+ if (method === "soundex") {
332
+ sql = `SELECT ${selectCols}, soundex(${columnName}) as code FROM ${tableName} WHERE soundex(${columnName}) = soundex($1)${additionalWhere} LIMIT ${String(limitVal)}`;
333
+ }
334
+ else if (method === "metaphone") {
335
+ sql = `SELECT ${selectCols}, metaphone(${columnName}, 10) as code FROM ${tableName} WHERE metaphone(${columnName}, 10) = metaphone($1, 10)${additionalWhere} LIMIT ${String(limitVal)}`;
336
+ }
337
+ else {
338
+ sql = `SELECT ${selectCols}, levenshtein(${columnName}, $1) as distance FROM ${tableName} WHERE levenshtein(${columnName}, $1) <= ${String(maxDist)}${additionalWhere} ORDER BY distance LIMIT ${String(limitVal)}`;
339
+ }
340
+ const result = await adapter.executeQuery(sql, [parsed.value]);
341
+ return { rows: result.rows, count: result.rows?.length ?? 0 };
238
342
  }
239
- const tableName = sanitizeTableName(resolvedTable, parsed.schema);
240
- const columnName = sanitizeIdentifier(parsed.column);
241
- const selectCols = parsed.select !== undefined && parsed.select.length > 0
242
- ? sanitizeIdentifiers(parsed.select).join(", ")
243
- : "*";
244
- const additionalWhere = parsed.where
245
- ? ` AND (${sanitizeWhereClause(parsed.where)})`
246
- : "";
247
- let sql;
248
- if (method === "soundex") {
249
- sql = `SELECT ${selectCols}, soundex(${columnName}) as code FROM ${tableName} WHERE soundex(${columnName}) = soundex($1)${additionalWhere} LIMIT ${String(limitVal)}`;
343
+ catch (error) {
344
+ if (error instanceof ZodError) {
345
+ return {
346
+ success: false,
347
+ error: `pg_fuzzy_match validation error: ${error.issues.map((e) => e.message).join(", ")}`,
348
+ };
349
+ }
350
+ return {
351
+ success: false,
352
+ error: formatPostgresError(error, {
353
+ tool: "pg_fuzzy_match",
354
+ }),
355
+ };
250
356
  }
251
- else if (method === "metaphone") {
252
- sql = `SELECT ${selectCols}, metaphone(${columnName}, 10) as code FROM ${tableName} WHERE metaphone(${columnName}, 10) = metaphone($1, 10)${additionalWhere} LIMIT ${String(limitVal)}`;
253
- }
254
- else {
255
- sql = `SELECT ${selectCols}, levenshtein(${columnName}, $1) as distance FROM ${tableName} WHERE levenshtein(${columnName}, $1) <= ${String(maxDist)}${additionalWhere} ORDER BY distance LIMIT ${String(limitVal)}`;
256
- }
257
- const result = await adapter.executeQuery(sql, [parsed.value]);
258
- return { rows: result.rows, count: result.rows?.length ?? 0 };
259
357
  },
260
358
  };
261
359
  }
@@ -269,25 +367,46 @@ function createRegexpMatchTool(adapter) {
269
367
  annotations: readOnly("Regexp Match"),
270
368
  icons: getToolIcons("text", readOnly("Regexp Match")),
271
369
  handler: async (params, _context) => {
272
- const parsed = RegexpMatchSchema.parse(params);
273
- // The preprocessor guarantees table is set (converts tableName → table)
274
- const resolvedTable = parsed.table ?? parsed.tableName;
275
- if (!resolvedTable) {
276
- throw new Error("Either 'table' or 'tableName' is required");
370
+ try {
371
+ const parsed = RegexpMatchSchema.parse(params);
372
+ // The preprocessor guarantees table is set (converts tableName → table)
373
+ const resolvedTable = parsed.table ?? parsed.tableName;
374
+ if (!resolvedTable) {
375
+ return {
376
+ success: false,
377
+ error: "Either 'table' or 'tableName' is required",
378
+ };
379
+ }
380
+ const tableName = sanitizeTableName(resolvedTable, parsed.schema);
381
+ const columnName = sanitizeIdentifier(parsed.column);
382
+ const selectCols = parsed.select !== undefined && parsed.select.length > 0
383
+ ? sanitizeIdentifiers(parsed.select).join(", ")
384
+ : "*";
385
+ const op = parsed.flags?.includes("i") ? "~*" : "~";
386
+ const additionalWhere = parsed.where
387
+ ? ` AND (${sanitizeWhereClause(parsed.where)})`
388
+ : "";
389
+ // Default limit to 100 to prevent large payloads
390
+ const limitVal = parsed.limit !== undefined && parsed.limit > 0 ? parsed.limit : 100;
391
+ const limitClause = ` LIMIT ${String(limitVal)}`;
392
+ const sql = `SELECT ${selectCols} FROM ${tableName} WHERE ${columnName} ${op} $1${additionalWhere}${limitClause}`;
393
+ const result = await adapter.executeQuery(sql, [parsed.pattern]);
394
+ return { rows: result.rows, count: result.rows?.length ?? 0 };
395
+ }
396
+ catch (error) {
397
+ if (error instanceof ZodError) {
398
+ return {
399
+ success: false,
400
+ error: `pg_regexp_match validation error: ${error.issues.map((e) => e.message).join(", ")}`,
401
+ };
402
+ }
403
+ return {
404
+ success: false,
405
+ error: formatPostgresError(error, {
406
+ tool: "pg_regexp_match",
407
+ }),
408
+ };
277
409
  }
278
- const tableName = sanitizeTableName(resolvedTable, parsed.schema);
279
- const columnName = sanitizeIdentifier(parsed.column);
280
- const selectCols = parsed.select !== undefined && parsed.select.length > 0
281
- ? sanitizeIdentifiers(parsed.select).join(", ")
282
- : "*";
283
- const op = parsed.flags?.includes("i") ? "~*" : "~";
284
- const additionalWhere = parsed.where
285
- ? ` AND (${sanitizeWhereClause(parsed.where)})`
286
- : "";
287
- const limitClause = parsed.limit !== undefined ? ` LIMIT ${String(parsed.limit)}` : "";
288
- const sql = `SELECT ${selectCols} FROM ${tableName} WHERE ${columnName} ${op} $1${additionalWhere}${limitClause}`;
289
- const result = await adapter.executeQuery(sql, [parsed.pattern]);
290
- return { rows: result.rows, count: result.rows?.length ?? 0 };
291
410
  },
292
411
  };
293
412
  }
@@ -304,7 +423,10 @@ function createLikeSearchTool(adapter) {
304
423
  .optional()
305
424
  .describe("Use case-sensitive LIKE (default: false, uses ILIKE)"),
306
425
  select: z.array(z.string()).optional(),
307
- limit: z.number().optional(),
426
+ limit: z
427
+ .number()
428
+ .optional()
429
+ .describe("Max results (default: 100 to prevent large payloads)"),
308
430
  where: z.string().optional().describe("Additional WHERE clause filter"),
309
431
  schema: z.string().optional().describe("Schema name (default: public)"),
310
432
  })
@@ -322,27 +444,46 @@ function createLikeSearchTool(adapter) {
322
444
  annotations: readOnly("LIKE Search"),
323
445
  icons: getToolIcons("text", readOnly("LIKE Search")),
324
446
  handler: async (params, _context) => {
325
- const parsed = LikeSearchSchema.parse(params);
326
- // The preprocessor guarantees table is set (converts tableName → table)
327
- const resolvedTable = parsed.table ?? parsed.tableName;
328
- if (!resolvedTable) {
329
- throw new Error("Either 'table' or 'tableName' is required");
447
+ try {
448
+ const parsed = LikeSearchSchema.parse(params);
449
+ // The preprocessor guarantees table is set (converts tableName → table)
450
+ const resolvedTable = parsed.table ?? parsed.tableName;
451
+ if (!resolvedTable) {
452
+ return {
453
+ success: false,
454
+ error: "Either 'table' or 'tableName' is required",
455
+ };
456
+ }
457
+ const tableName = sanitizeTableName(resolvedTable, parsed.schema);
458
+ const columnName = sanitizeIdentifier(parsed.column);
459
+ const selectCols = parsed.select !== undefined && parsed.select.length > 0
460
+ ? sanitizeIdentifiers(parsed.select).join(", ")
461
+ : "*";
462
+ const op = parsed.caseSensitive === true ? "LIKE" : "ILIKE";
463
+ const additionalWhere = parsed.where
464
+ ? ` AND (${sanitizeWhereClause(parsed.where)})`
465
+ : "";
466
+ // Default limit to 100 to prevent large payloads
467
+ const limitVal = parsed.limit !== undefined && parsed.limit > 0 ? parsed.limit : 100;
468
+ const limitClause = ` LIMIT ${String(limitVal)}`;
469
+ const sql = `SELECT ${selectCols} FROM ${tableName} WHERE ${columnName} ${op} $1${additionalWhere}${limitClause}`;
470
+ const result = await adapter.executeQuery(sql, [parsed.pattern]);
471
+ return { rows: result.rows, count: result.rows?.length ?? 0 };
472
+ }
473
+ catch (error) {
474
+ if (error instanceof ZodError) {
475
+ return {
476
+ success: false,
477
+ error: `pg_like_search validation error: ${error.issues.map((e) => e.message).join(", ")}`,
478
+ };
479
+ }
480
+ return {
481
+ success: false,
482
+ error: formatPostgresError(error, {
483
+ tool: "pg_like_search",
484
+ }),
485
+ };
330
486
  }
331
- const tableName = sanitizeTableName(resolvedTable, parsed.schema);
332
- const columnName = sanitizeIdentifier(parsed.column);
333
- const selectCols = parsed.select !== undefined && parsed.select.length > 0
334
- ? sanitizeIdentifiers(parsed.select).join(", ")
335
- : "*";
336
- const op = parsed.caseSensitive === true ? "LIKE" : "ILIKE";
337
- const additionalWhere = parsed.where
338
- ? ` AND (${sanitizeWhereClause(parsed.where)})`
339
- : "";
340
- const limitClause = parsed.limit !== undefined && parsed.limit > 0
341
- ? ` LIMIT ${String(parsed.limit)}`
342
- : "";
343
- const sql = `SELECT ${selectCols} FROM ${tableName} WHERE ${columnName} ${op} $1${additionalWhere}${limitClause}`;
344
- const result = await adapter.executeQuery(sql, [parsed.pattern]);
345
- return { rows: result.rows, count: result.rows?.length ?? 0 };
346
487
  },
347
488
  };
348
489
  }
@@ -390,40 +531,59 @@ function createTextHeadlineTool(adapter) {
390
531
  annotations: readOnly("Text Headline"),
391
532
  icons: getToolIcons("text", readOnly("Text Headline")),
392
533
  handler: async (params, _context) => {
393
- const parsed = HeadlineSchema.parse(params);
394
- const cfg = sanitizeFtsConfig(parsed.config ?? "english");
395
- // Build options string from individual params or use provided options
396
- let opts;
397
- if (parsed.options) {
398
- opts = parsed.options;
399
- }
400
- else {
401
- const optParts = [];
402
- optParts.push(`StartSel=${parsed.startSel ?? "<b>"}`);
403
- optParts.push(`StopSel=${parsed.stopSel ?? "</b>"}`);
404
- optParts.push(`MaxWords=${String(parsed.maxWords ?? 35)}`);
405
- optParts.push(`MinWords=${String(parsed.minWords ?? 15)}`);
406
- opts = optParts.join(", ");
407
- }
408
- // The preprocessor guarantees table is set (converts tableName → table)
409
- const resolvedTable = parsed.table ?? parsed.tableName;
410
- if (!resolvedTable) {
411
- throw new Error("Either 'table' or 'tableName' is required");
412
- }
413
- const tableName = sanitizeTableName(resolvedTable, parsed.schema);
414
- const columnName = sanitizeIdentifier(parsed.column);
415
- // Use provided select columns, or default to * (user should specify PK for stable identification)
416
- const selectCols = parsed.select !== undefined && parsed.select.length > 0
417
- ? sanitizeIdentifiers(parsed.select).join(", ") + ", "
418
- : "";
419
- const limitClause = parsed.limit !== undefined && parsed.limit > 0
420
- ? ` LIMIT ${String(parsed.limit)}`
421
- : "";
422
- const sql = `SELECT ${selectCols}ts_headline('${cfg}', ${columnName}, plainto_tsquery('${cfg}', $1), '${opts}') as headline
534
+ try {
535
+ const parsed = HeadlineSchema.parse(params);
536
+ const cfg = sanitizeFtsConfig(parsed.config ?? "english");
537
+ // Build options string from individual params or use provided options
538
+ let opts;
539
+ if (parsed.options) {
540
+ opts = parsed.options;
541
+ }
542
+ else {
543
+ const optParts = [];
544
+ optParts.push(`StartSel=${parsed.startSel ?? "<b>"}`);
545
+ optParts.push(`StopSel=${parsed.stopSel ?? "</b>"}`);
546
+ optParts.push(`MaxWords=${String(parsed.maxWords ?? 35)}`);
547
+ optParts.push(`MinWords=${String(parsed.minWords ?? 15)}`);
548
+ opts = optParts.join(", ");
549
+ }
550
+ // The preprocessor guarantees table is set (converts tableName → table)
551
+ const resolvedTable = parsed.table ?? parsed.tableName;
552
+ if (!resolvedTable) {
553
+ return {
554
+ success: false,
555
+ error: "Either 'table' or 'tableName' is required",
556
+ };
557
+ }
558
+ const tableName = sanitizeTableName(resolvedTable, parsed.schema);
559
+ const columnName = sanitizeIdentifier(parsed.column);
560
+ // Use provided select columns, or default to * (user should specify PK for stable identification)
561
+ const selectCols = parsed.select !== undefined && parsed.select.length > 0
562
+ ? sanitizeIdentifiers(parsed.select).join(", ") + ", "
563
+ : "";
564
+ const limitClause = parsed.limit !== undefined && parsed.limit > 0
565
+ ? ` LIMIT ${String(parsed.limit)}`
566
+ : "";
567
+ const sql = `SELECT ${selectCols}ts_headline('${cfg}', ${columnName}, plainto_tsquery('${cfg}', $1), '${opts}') as headline
423
568
  FROM ${tableName}
424
569
  WHERE to_tsvector('${cfg}', ${columnName}) @@ plainto_tsquery('${cfg}', $1)${limitClause}`;
425
- const result = await adapter.executeQuery(sql, [parsed.query]);
426
- return { rows: result.rows, count: result.rows?.length ?? 0 };
570
+ const result = await adapter.executeQuery(sql, [parsed.query]);
571
+ return { rows: result.rows, count: result.rows?.length ?? 0 };
572
+ }
573
+ catch (error) {
574
+ if (error instanceof ZodError) {
575
+ return {
576
+ success: false,
577
+ error: `pg_text_headline validation error: ${error.issues.map((e) => e.message).join(", ")}`,
578
+ };
579
+ }
580
+ return {
581
+ success: false,
582
+ error: formatPostgresError(error, {
583
+ tool: "pg_text_headline",
584
+ }),
585
+ };
586
+ }
427
587
  },
428
588
  };
429
589
  }
@@ -456,36 +616,55 @@ function createFtsIndexTool(adapter) {
456
616
  annotations: write("Create FTS Index"),
457
617
  icons: getToolIcons("text", write("Create FTS Index")),
458
618
  handler: async (params, _context) => {
459
- const parsed = FtsIndexSchema.parse(params);
460
- const cfg = sanitizeFtsConfig(parsed.config ?? "english");
461
- // The preprocessor guarantees table is set (converts tableName → table)
462
- const resolvedTable = parsed.table ?? parsed.tableName;
463
- if (!resolvedTable) {
464
- throw new Error("Either 'table' or 'tableName' is required");
619
+ try {
620
+ const parsed = FtsIndexSchema.parse(params);
621
+ const cfg = sanitizeFtsConfig(parsed.config ?? "english");
622
+ // The preprocessor guarantees table is set (converts tableName → table)
623
+ const resolvedTable = parsed.table ?? parsed.tableName;
624
+ if (!resolvedTable) {
625
+ return {
626
+ success: false,
627
+ error: "Either 'table' or 'tableName' is required",
628
+ };
629
+ }
630
+ const defaultIndexName = `idx_${resolvedTable}_${parsed.column}_fts`;
631
+ const resolvedIndexName = parsed.name ?? defaultIndexName;
632
+ const indexName = sanitizeIdentifier(resolvedIndexName);
633
+ // Default to IF NOT EXISTS for safer operation (skip existing indexes)
634
+ const useIfNotExists = parsed.ifNotExists !== false;
635
+ const ifNotExists = useIfNotExists ? "IF NOT EXISTS " : "";
636
+ // Build qualified table name with schema support
637
+ const tableName = sanitizeTableName(resolvedTable, parsed.schema);
638
+ const columnName = sanitizeIdentifier(parsed.column);
639
+ // Check if index exists before creation (to accurately report 'skipped')
640
+ let existedBefore = false;
641
+ if (useIfNotExists) {
642
+ const checkResult = await adapter.executeQuery(`SELECT 1 FROM pg_indexes WHERE indexname = $1 LIMIT 1`, [resolvedIndexName]);
643
+ existedBefore = (checkResult.rows?.length ?? 0) > 0;
644
+ }
645
+ const sql = `CREATE INDEX ${ifNotExists}${indexName} ON ${tableName} USING gin(to_tsvector('${cfg}', ${columnName}))`;
646
+ await adapter.executeQuery(sql);
647
+ return {
648
+ success: true,
649
+ index: resolvedIndexName,
650
+ config: cfg,
651
+ skipped: existedBefore,
652
+ };
465
653
  }
466
- const defaultIndexName = `idx_${resolvedTable}_${parsed.column}_fts`;
467
- const resolvedIndexName = parsed.name ?? defaultIndexName;
468
- const indexName = sanitizeIdentifier(resolvedIndexName);
469
- // Default to IF NOT EXISTS for safer operation (skip existing indexes)
470
- const useIfNotExists = parsed.ifNotExists !== false;
471
- const ifNotExists = useIfNotExists ? "IF NOT EXISTS " : "";
472
- // Build qualified table name with schema support
473
- const tableName = sanitizeTableName(resolvedTable, parsed.schema);
474
- const columnName = sanitizeIdentifier(parsed.column);
475
- // Check if index exists before creation (to accurately report 'skipped')
476
- let existedBefore = false;
477
- if (useIfNotExists) {
478
- const checkResult = await adapter.executeQuery(`SELECT 1 FROM pg_indexes WHERE indexname = $1 LIMIT 1`, [resolvedIndexName]);
479
- existedBefore = (checkResult.rows?.length ?? 0) > 0;
654
+ catch (error) {
655
+ if (error instanceof ZodError) {
656
+ return {
657
+ success: false,
658
+ error: `pg_create_fts_index validation error: ${error.issues.map((e) => e.message).join(", ")}`,
659
+ };
660
+ }
661
+ return {
662
+ success: false,
663
+ error: formatPostgresError(error, {
664
+ tool: "pg_create_fts_index",
665
+ }),
666
+ };
480
667
  }
481
- const sql = `CREATE INDEX ${ifNotExists}${indexName} ON ${tableName} USING gin(to_tsvector('${cfg}', ${columnName}))`;
482
- await adapter.executeQuery(sql);
483
- return {
484
- success: true,
485
- index: resolvedIndexName,
486
- config: cfg,
487
- skipped: existedBefore,
488
- };
489
668
  },
490
669
  };
491
670
  }
@@ -529,8 +708,7 @@ function createTextSentimentTool(_adapter) {
529
708
  outputSchema: TextSentimentOutputSchema,
530
709
  annotations: readOnly("Text Sentiment"),
531
710
  icons: getToolIcons("text", readOnly("Text Sentiment")),
532
- // eslint-disable-next-line @typescript-eslint/require-await
533
- handler: async (params, _context) => {
711
+ handler: (params, _context) => {
534
712
  const parsed = SentimentSchema.parse(params ?? {});
535
713
  const text = parsed.text.toLowerCase();
536
714
  const positiveWords = [
@@ -619,7 +797,7 @@ function createTextSentimentTool(_adapter) {
619
797
  result.matchedPositive = matchedPositive;
620
798
  result.matchedNegative = matchedNegative;
621
799
  }
622
- return result;
800
+ return Promise.resolve(result);
623
801
  },
624
802
  };
625
803
  }