@neverinfamous/postgres-mcp 1.2.0 → 2.0.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 (293) hide show
  1. package/README.md +202 -148
  2. package/dist/__tests__/benchmarks/codemode.bench.d.ts +10 -0
  3. package/dist/__tests__/benchmarks/codemode.bench.d.ts.map +1 -0
  4. package/dist/__tests__/benchmarks/codemode.bench.js +159 -0
  5. package/dist/__tests__/benchmarks/codemode.bench.js.map +1 -0
  6. package/dist/__tests__/benchmarks/connection-pool.bench.d.ts +10 -0
  7. package/dist/__tests__/benchmarks/connection-pool.bench.d.ts.map +1 -0
  8. package/dist/__tests__/benchmarks/connection-pool.bench.js +123 -0
  9. package/dist/__tests__/benchmarks/connection-pool.bench.js.map +1 -0
  10. package/dist/__tests__/benchmarks/handler-dispatch.bench.d.ts +11 -0
  11. package/dist/__tests__/benchmarks/handler-dispatch.bench.d.ts.map +1 -0
  12. package/dist/__tests__/benchmarks/handler-dispatch.bench.js +199 -0
  13. package/dist/__tests__/benchmarks/handler-dispatch.bench.js.map +1 -0
  14. package/dist/__tests__/benchmarks/logger-sanitization.bench.d.ts +15 -0
  15. package/dist/__tests__/benchmarks/logger-sanitization.bench.d.ts.map +1 -0
  16. package/dist/__tests__/benchmarks/logger-sanitization.bench.js +155 -0
  17. package/dist/__tests__/benchmarks/logger-sanitization.bench.js.map +1 -0
  18. package/dist/__tests__/benchmarks/resource-prompts.bench.d.ts +10 -0
  19. package/dist/__tests__/benchmarks/resource-prompts.bench.d.ts.map +1 -0
  20. package/dist/__tests__/benchmarks/resource-prompts.bench.js +181 -0
  21. package/dist/__tests__/benchmarks/resource-prompts.bench.js.map +1 -0
  22. package/dist/__tests__/benchmarks/schema-parsing.bench.d.ts +11 -0
  23. package/dist/__tests__/benchmarks/schema-parsing.bench.d.ts.map +1 -0
  24. package/dist/__tests__/benchmarks/schema-parsing.bench.js +209 -0
  25. package/dist/__tests__/benchmarks/schema-parsing.bench.js.map +1 -0
  26. package/dist/__tests__/benchmarks/tool-filtering.bench.d.ts +9 -0
  27. package/dist/__tests__/benchmarks/tool-filtering.bench.d.ts.map +1 -0
  28. package/dist/__tests__/benchmarks/tool-filtering.bench.js +83 -0
  29. package/dist/__tests__/benchmarks/tool-filtering.bench.js.map +1 -0
  30. package/dist/__tests__/benchmarks/transport-auth.bench.d.ts +10 -0
  31. package/dist/__tests__/benchmarks/transport-auth.bench.d.ts.map +1 -0
  32. package/dist/__tests__/benchmarks/transport-auth.bench.js +128 -0
  33. package/dist/__tests__/benchmarks/transport-auth.bench.js.map +1 -0
  34. package/dist/__tests__/benchmarks/utilities.bench.d.ts +10 -0
  35. package/dist/__tests__/benchmarks/utilities.bench.d.ts.map +1 -0
  36. package/dist/__tests__/benchmarks/utilities.bench.js +164 -0
  37. package/dist/__tests__/benchmarks/utilities.bench.js.map +1 -0
  38. package/dist/adapters/DatabaseAdapter.d.ts.map +1 -1
  39. package/dist/adapters/DatabaseAdapter.js +12 -0
  40. package/dist/adapters/DatabaseAdapter.js.map +1 -1
  41. package/dist/adapters/postgresql/PostgresAdapter.d.ts.map +1 -1
  42. package/dist/adapters/postgresql/PostgresAdapter.js +56 -3
  43. package/dist/adapters/postgresql/PostgresAdapter.js.map +1 -1
  44. package/dist/adapters/postgresql/prompts/ltree.js +2 -2
  45. package/dist/adapters/postgresql/prompts/ltree.js.map +1 -1
  46. package/dist/adapters/postgresql/schemas/admin.d.ts +10 -5
  47. package/dist/adapters/postgresql/schemas/admin.d.ts.map +1 -1
  48. package/dist/adapters/postgresql/schemas/admin.js +10 -5
  49. package/dist/adapters/postgresql/schemas/admin.js.map +1 -1
  50. package/dist/adapters/postgresql/schemas/backup.d.ts +45 -27
  51. package/dist/adapters/postgresql/schemas/backup.d.ts.map +1 -1
  52. package/dist/adapters/postgresql/schemas/backup.js +64 -26
  53. package/dist/adapters/postgresql/schemas/backup.js.map +1 -1
  54. package/dist/adapters/postgresql/schemas/core.d.ts +53 -19
  55. package/dist/adapters/postgresql/schemas/core.d.ts.map +1 -1
  56. package/dist/adapters/postgresql/schemas/core.js +61 -17
  57. package/dist/adapters/postgresql/schemas/core.js.map +1 -1
  58. package/dist/adapters/postgresql/schemas/cron.d.ts +51 -32
  59. package/dist/adapters/postgresql/schemas/cron.d.ts.map +1 -1
  60. package/dist/adapters/postgresql/schemas/cron.js +64 -44
  61. package/dist/adapters/postgresql/schemas/cron.js.map +1 -1
  62. package/dist/adapters/postgresql/schemas/extensions.d.ts +224 -110
  63. package/dist/adapters/postgresql/schemas/extensions.d.ts.map +1 -1
  64. package/dist/adapters/postgresql/schemas/extensions.js +245 -96
  65. package/dist/adapters/postgresql/schemas/extensions.js.map +1 -1
  66. package/dist/adapters/postgresql/schemas/index.d.ts +7 -6
  67. package/dist/adapters/postgresql/schemas/index.d.ts.map +1 -1
  68. package/dist/adapters/postgresql/schemas/index.js +16 -8
  69. package/dist/adapters/postgresql/schemas/index.js.map +1 -1
  70. package/dist/adapters/postgresql/schemas/introspection.d.ts +445 -0
  71. package/dist/adapters/postgresql/schemas/introspection.d.ts.map +1 -0
  72. package/dist/adapters/postgresql/schemas/introspection.js +478 -0
  73. package/dist/adapters/postgresql/schemas/introspection.js.map +1 -0
  74. package/dist/adapters/postgresql/schemas/jsonb.d.ts +102 -42
  75. package/dist/adapters/postgresql/schemas/jsonb.d.ts.map +1 -1
  76. package/dist/adapters/postgresql/schemas/jsonb.js +125 -30
  77. package/dist/adapters/postgresql/schemas/jsonb.js.map +1 -1
  78. package/dist/adapters/postgresql/schemas/monitoring.d.ts +69 -36
  79. package/dist/adapters/postgresql/schemas/monitoring.d.ts.map +1 -1
  80. package/dist/adapters/postgresql/schemas/monitoring.js +98 -40
  81. package/dist/adapters/postgresql/schemas/monitoring.js.map +1 -1
  82. package/dist/adapters/postgresql/schemas/partitioning.d.ts +21 -24
  83. package/dist/adapters/postgresql/schemas/partitioning.d.ts.map +1 -1
  84. package/dist/adapters/postgresql/schemas/partitioning.js +26 -14
  85. package/dist/adapters/postgresql/schemas/partitioning.js.map +1 -1
  86. package/dist/adapters/postgresql/schemas/partman.d.ts +69 -0
  87. package/dist/adapters/postgresql/schemas/partman.d.ts.map +1 -1
  88. package/dist/adapters/postgresql/schemas/partman.js +46 -33
  89. package/dist/adapters/postgresql/schemas/partman.js.map +1 -1
  90. package/dist/adapters/postgresql/schemas/performance.d.ts +97 -49
  91. package/dist/adapters/postgresql/schemas/performance.d.ts.map +1 -1
  92. package/dist/adapters/postgresql/schemas/performance.js +139 -34
  93. package/dist/adapters/postgresql/schemas/performance.js.map +1 -1
  94. package/dist/adapters/postgresql/schemas/postgis.d.ts +20 -0
  95. package/dist/adapters/postgresql/schemas/postgis.d.ts.map +1 -1
  96. package/dist/adapters/postgresql/schemas/postgis.js +40 -0
  97. package/dist/adapters/postgresql/schemas/postgis.js.map +1 -1
  98. package/dist/adapters/postgresql/schemas/schema-mgmt.d.ts +50 -30
  99. package/dist/adapters/postgresql/schemas/schema-mgmt.d.ts.map +1 -1
  100. package/dist/adapters/postgresql/schemas/schema-mgmt.js +105 -33
  101. package/dist/adapters/postgresql/schemas/schema-mgmt.js.map +1 -1
  102. package/dist/adapters/postgresql/schemas/stats.d.ts +33 -20
  103. package/dist/adapters/postgresql/schemas/stats.d.ts.map +1 -1
  104. package/dist/adapters/postgresql/schemas/stats.js +36 -20
  105. package/dist/adapters/postgresql/schemas/stats.js.map +1 -1
  106. package/dist/adapters/postgresql/schemas/text-search.d.ts +34 -19
  107. package/dist/adapters/postgresql/schemas/text-search.d.ts.map +1 -1
  108. package/dist/adapters/postgresql/schemas/text-search.js +52 -13
  109. package/dist/adapters/postgresql/schemas/text-search.js.map +1 -1
  110. package/dist/adapters/postgresql/tools/admin.d.ts.map +1 -1
  111. package/dist/adapters/postgresql/tools/admin.js +272 -186
  112. package/dist/adapters/postgresql/tools/admin.js.map +1 -1
  113. package/dist/adapters/postgresql/tools/backup/dump.d.ts.map +1 -1
  114. package/dist/adapters/postgresql/tools/backup/dump.js +376 -350
  115. package/dist/adapters/postgresql/tools/backup/dump.js.map +1 -1
  116. package/dist/adapters/postgresql/tools/citext.d.ts.map +1 -1
  117. package/dist/adapters/postgresql/tools/citext.js +333 -243
  118. package/dist/adapters/postgresql/tools/citext.js.map +1 -1
  119. package/dist/adapters/postgresql/tools/codemode/index.d.ts.map +1 -1
  120. package/dist/adapters/postgresql/tools/codemode/index.js +2 -11
  121. package/dist/adapters/postgresql/tools/codemode/index.js.map +1 -1
  122. package/dist/adapters/postgresql/tools/core/convenience.d.ts +9 -1
  123. package/dist/adapters/postgresql/tools/core/convenience.d.ts.map +1 -1
  124. package/dist/adapters/postgresql/tools/core/convenience.js +101 -19
  125. package/dist/adapters/postgresql/tools/core/convenience.js.map +1 -1
  126. package/dist/adapters/postgresql/tools/core/error-helpers.d.ts +48 -0
  127. package/dist/adapters/postgresql/tools/core/error-helpers.d.ts.map +1 -0
  128. package/dist/adapters/postgresql/tools/core/error-helpers.js +256 -0
  129. package/dist/adapters/postgresql/tools/core/error-helpers.js.map +1 -0
  130. package/dist/adapters/postgresql/tools/core/health.d.ts.map +1 -1
  131. package/dist/adapters/postgresql/tools/core/health.js +18 -4
  132. package/dist/adapters/postgresql/tools/core/health.js.map +1 -1
  133. package/dist/adapters/postgresql/tools/core/indexes.d.ts.map +1 -1
  134. package/dist/adapters/postgresql/tools/core/indexes.js +48 -6
  135. package/dist/adapters/postgresql/tools/core/indexes.js.map +1 -1
  136. package/dist/adapters/postgresql/tools/core/objects.d.ts.map +1 -1
  137. package/dist/adapters/postgresql/tools/core/objects.js +104 -85
  138. package/dist/adapters/postgresql/tools/core/objects.js.map +1 -1
  139. package/dist/adapters/postgresql/tools/core/query.d.ts.map +1 -1
  140. package/dist/adapters/postgresql/tools/core/query.js +100 -42
  141. package/dist/adapters/postgresql/tools/core/query.js.map +1 -1
  142. package/dist/adapters/postgresql/tools/core/schemas.d.ts +51 -25
  143. package/dist/adapters/postgresql/tools/core/schemas.d.ts.map +1 -1
  144. package/dist/adapters/postgresql/tools/core/schemas.js +51 -25
  145. package/dist/adapters/postgresql/tools/core/schemas.js.map +1 -1
  146. package/dist/adapters/postgresql/tools/core/tables.d.ts.map +1 -1
  147. package/dist/adapters/postgresql/tools/core/tables.js +72 -32
  148. package/dist/adapters/postgresql/tools/core/tables.js.map +1 -1
  149. package/dist/adapters/postgresql/tools/cron.d.ts.map +1 -1
  150. package/dist/adapters/postgresql/tools/cron.js +333 -206
  151. package/dist/adapters/postgresql/tools/cron.js.map +1 -1
  152. package/dist/adapters/postgresql/tools/introspection.d.ts +15 -0
  153. package/dist/adapters/postgresql/tools/introspection.d.ts.map +1 -0
  154. package/dist/adapters/postgresql/tools/introspection.js +1682 -0
  155. package/dist/adapters/postgresql/tools/introspection.js.map +1 -0
  156. package/dist/adapters/postgresql/tools/jsonb/advanced.d.ts.map +1 -1
  157. package/dist/adapters/postgresql/tools/jsonb/advanced.js +394 -297
  158. package/dist/adapters/postgresql/tools/jsonb/advanced.js.map +1 -1
  159. package/dist/adapters/postgresql/tools/jsonb/basic.d.ts.map +1 -1
  160. package/dist/adapters/postgresql/tools/jsonb/basic.js +686 -398
  161. package/dist/adapters/postgresql/tools/jsonb/basic.js.map +1 -1
  162. package/dist/adapters/postgresql/tools/kcache.d.ts.map +1 -1
  163. package/dist/adapters/postgresql/tools/kcache.js +278 -246
  164. package/dist/adapters/postgresql/tools/kcache.js.map +1 -1
  165. package/dist/adapters/postgresql/tools/ltree.d.ts.map +1 -1
  166. package/dist/adapters/postgresql/tools/ltree.js +137 -38
  167. package/dist/adapters/postgresql/tools/ltree.js.map +1 -1
  168. package/dist/adapters/postgresql/tools/monitoring.d.ts.map +1 -1
  169. package/dist/adapters/postgresql/tools/monitoring.js +86 -55
  170. package/dist/adapters/postgresql/tools/monitoring.js.map +1 -1
  171. package/dist/adapters/postgresql/tools/partitioning.d.ts.map +1 -1
  172. package/dist/adapters/postgresql/tools/partitioning.js +79 -15
  173. package/dist/adapters/postgresql/tools/partitioning.js.map +1 -1
  174. package/dist/adapters/postgresql/tools/partman/management.d.ts.map +1 -1
  175. package/dist/adapters/postgresql/tools/partman/management.js +43 -56
  176. package/dist/adapters/postgresql/tools/partman/management.js.map +1 -1
  177. package/dist/adapters/postgresql/tools/partman/operations.d.ts.map +1 -1
  178. package/dist/adapters/postgresql/tools/partman/operations.js +137 -24
  179. package/dist/adapters/postgresql/tools/partman/operations.js.map +1 -1
  180. package/dist/adapters/postgresql/tools/performance/analysis.d.ts.map +1 -1
  181. package/dist/adapters/postgresql/tools/performance/analysis.js +276 -165
  182. package/dist/adapters/postgresql/tools/performance/analysis.js.map +1 -1
  183. package/dist/adapters/postgresql/tools/performance/explain.d.ts.map +1 -1
  184. package/dist/adapters/postgresql/tools/performance/explain.js +61 -21
  185. package/dist/adapters/postgresql/tools/performance/explain.js.map +1 -1
  186. package/dist/adapters/postgresql/tools/performance/monitoring.d.ts.map +1 -1
  187. package/dist/adapters/postgresql/tools/performance/monitoring.js +52 -12
  188. package/dist/adapters/postgresql/tools/performance/monitoring.js.map +1 -1
  189. package/dist/adapters/postgresql/tools/performance/optimization.d.ts.map +1 -1
  190. package/dist/adapters/postgresql/tools/performance/optimization.js +92 -81
  191. package/dist/adapters/postgresql/tools/performance/optimization.js.map +1 -1
  192. package/dist/adapters/postgresql/tools/performance/stats.d.ts.map +1 -1
  193. package/dist/adapters/postgresql/tools/performance/stats.js +182 -60
  194. package/dist/adapters/postgresql/tools/performance/stats.js.map +1 -1
  195. package/dist/adapters/postgresql/tools/pgcrypto.d.ts.map +1 -1
  196. package/dist/adapters/postgresql/tools/pgcrypto.js +277 -102
  197. package/dist/adapters/postgresql/tools/pgcrypto.js.map +1 -1
  198. package/dist/adapters/postgresql/tools/postgis/advanced.d.ts.map +1 -1
  199. package/dist/adapters/postgresql/tools/postgis/advanced.js +298 -230
  200. package/dist/adapters/postgresql/tools/postgis/advanced.js.map +1 -1
  201. package/dist/adapters/postgresql/tools/postgis/basic.d.ts.map +1 -1
  202. package/dist/adapters/postgresql/tools/postgis/basic.js +370 -251
  203. package/dist/adapters/postgresql/tools/postgis/basic.js.map +1 -1
  204. package/dist/adapters/postgresql/tools/postgis/standalone.d.ts.map +1 -1
  205. package/dist/adapters/postgresql/tools/postgis/standalone.js +135 -51
  206. package/dist/adapters/postgresql/tools/postgis/standalone.js.map +1 -1
  207. package/dist/adapters/postgresql/tools/schema.d.ts.map +1 -1
  208. package/dist/adapters/postgresql/tools/schema.js +580 -233
  209. package/dist/adapters/postgresql/tools/schema.js.map +1 -1
  210. package/dist/adapters/postgresql/tools/stats/advanced.d.ts.map +1 -1
  211. package/dist/adapters/postgresql/tools/stats/advanced.js +567 -506
  212. package/dist/adapters/postgresql/tools/stats/advanced.js.map +1 -1
  213. package/dist/adapters/postgresql/tools/stats/basic.d.ts.map +1 -1
  214. package/dist/adapters/postgresql/tools/stats/basic.js +340 -316
  215. package/dist/adapters/postgresql/tools/stats/basic.js.map +1 -1
  216. package/dist/adapters/postgresql/tools/text.d.ts.map +1 -1
  217. package/dist/adapters/postgresql/tools/text.js +690 -337
  218. package/dist/adapters/postgresql/tools/text.js.map +1 -1
  219. package/dist/adapters/postgresql/tools/transactions.d.ts.map +1 -1
  220. package/dist/adapters/postgresql/tools/transactions.js +157 -50
  221. package/dist/adapters/postgresql/tools/transactions.js.map +1 -1
  222. package/dist/adapters/postgresql/tools/vector/advanced.d.ts.map +1 -1
  223. package/dist/adapters/postgresql/tools/vector/advanced.js +18 -0
  224. package/dist/adapters/postgresql/tools/vector/advanced.js.map +1 -1
  225. package/dist/adapters/postgresql/tools/vector/basic.d.ts.map +1 -1
  226. package/dist/adapters/postgresql/tools/vector/basic.js +100 -53
  227. package/dist/adapters/postgresql/tools/vector/basic.js.map +1 -1
  228. package/dist/auth/auth-context.d.ts +28 -0
  229. package/dist/auth/auth-context.d.ts.map +1 -0
  230. package/dist/auth/auth-context.js +37 -0
  231. package/dist/auth/auth-context.js.map +1 -0
  232. package/dist/auth/scope-map.d.ts +20 -0
  233. package/dist/auth/scope-map.d.ts.map +1 -0
  234. package/dist/auth/scope-map.js +40 -0
  235. package/dist/auth/scope-map.js.map +1 -0
  236. package/dist/auth/scopes.d.ts.map +1 -1
  237. package/dist/auth/scopes.js +2 -0
  238. package/dist/auth/scopes.js.map +1 -1
  239. package/dist/cli.js +1 -1
  240. package/dist/cli.js.map +1 -1
  241. package/dist/codemode/api.d.ts +1 -0
  242. package/dist/codemode/api.d.ts.map +1 -1
  243. package/dist/codemode/api.js +35 -1
  244. package/dist/codemode/api.js.map +1 -1
  245. package/dist/codemode/index.d.ts +0 -2
  246. package/dist/codemode/index.d.ts.map +1 -1
  247. package/dist/codemode/index.js +0 -4
  248. package/dist/codemode/index.js.map +1 -1
  249. package/dist/codemode/sandbox.d.ts +14 -1
  250. package/dist/codemode/sandbox.d.ts.map +1 -1
  251. package/dist/codemode/sandbox.js +58 -19
  252. package/dist/codemode/sandbox.js.map +1 -1
  253. package/dist/codemode/types.d.ts.map +1 -1
  254. package/dist/codemode/types.js +3 -0
  255. package/dist/codemode/types.js.map +1 -1
  256. package/dist/constants/ServerInstructions.d.ts +5 -1
  257. package/dist/constants/ServerInstructions.d.ts.map +1 -1
  258. package/dist/constants/ServerInstructions.js +117 -31
  259. package/dist/constants/ServerInstructions.js.map +1 -1
  260. package/dist/filtering/ToolConstants.d.ts +22 -19
  261. package/dist/filtering/ToolConstants.d.ts.map +1 -1
  262. package/dist/filtering/ToolConstants.js +48 -37
  263. package/dist/filtering/ToolConstants.js.map +1 -1
  264. package/dist/filtering/ToolFilter.d.ts.map +1 -1
  265. package/dist/filtering/ToolFilter.js +10 -13
  266. package/dist/filtering/ToolFilter.js.map +1 -1
  267. package/dist/pool/ConnectionPool.js +1 -1
  268. package/dist/pool/ConnectionPool.js.map +1 -1
  269. package/dist/transports/http.d.ts +1 -0
  270. package/dist/transports/http.d.ts.map +1 -1
  271. package/dist/transports/http.js +75 -21
  272. package/dist/transports/http.js.map +1 -1
  273. package/dist/types/filtering.d.ts +2 -2
  274. package/dist/types/filtering.d.ts.map +1 -1
  275. package/dist/utils/icons.d.ts.map +1 -1
  276. package/dist/utils/icons.js +5 -0
  277. package/dist/utils/icons.js.map +1 -1
  278. package/dist/utils/where-clause.d.ts.map +1 -1
  279. package/dist/utils/where-clause.js +24 -0
  280. package/dist/utils/where-clause.js.map +1 -1
  281. package/package.json +20 -13
  282. package/dist/codemode/sandbox-factory.d.ts +0 -72
  283. package/dist/codemode/sandbox-factory.d.ts.map +0 -1
  284. package/dist/codemode/sandbox-factory.js +0 -88
  285. package/dist/codemode/sandbox-factory.js.map +0 -1
  286. package/dist/codemode/worker-sandbox.d.ts +0 -82
  287. package/dist/codemode/worker-sandbox.d.ts.map +0 -1
  288. package/dist/codemode/worker-sandbox.js +0 -244
  289. package/dist/codemode/worker-sandbox.js.map +0 -1
  290. package/dist/codemode/worker-script.d.ts +0 -8
  291. package/dist/codemode/worker-script.d.ts.map +0 -1
  292. package/dist/codemode/worker-script.js +0 -113
  293. package/dist/codemode/worker-script.js.map +0 -1
@@ -3,9 +3,12 @@
3
3
  *
4
4
  * Advanced spatial tools: geocode, geo_transform, geo_index_optimize, geo_cluster.
5
5
  */
6
- import { z } from "zod";
6
+ import { z, ZodError } from "zod";
7
7
  import { readOnly } from "../../../../utils/annotations.js";
8
8
  import { getToolIcons } from "../../../../utils/icons.js";
9
+ import { formatPostgresError } from "../core/error-helpers.js";
10
+ import { sanitizeIdentifier, sanitizeTableName, } from "../../../../utils/identifiers.js";
11
+ import { sanitizeWhereClause } from "../../../../utils/where-clause.js";
9
12
  import { GeocodeSchemaBase, GeocodeSchema, GeoTransformSchemaBase, GeoTransformSchema, GeoClusterSchemaBase, GeoClusterSchema,
10
13
  // Output schemas
11
14
  GeocodeOutputSchema, GeoTransformOutputSchema, GeoIndexOptimizeOutputSchema, GeoClusterOutputSchema, } from "../../schemas/index.js";
@@ -19,27 +22,41 @@ export function createGeocodeTool(adapter) {
19
22
  annotations: readOnly("Geocode"),
20
23
  icons: getToolIcons("postgis", readOnly("Geocode")),
21
24
  handler: async (params, _context) => {
22
- const parsed = GeocodeSchema.parse(params ?? {});
23
- const srid = parsed.srid ?? 4326;
24
- const sql = `SELECT
25
+ try {
26
+ const parsed = GeocodeSchema.parse(params ?? {});
27
+ const srid = parsed.srid ?? 4326;
28
+ const sql = `SELECT
25
29
  ST_AsGeoJSON(ST_SetSRID(ST_MakePoint($1, $2), $3)) as geojson,
26
30
  ST_AsText(ST_SetSRID(ST_MakePoint($1, $2), $3)) as wkt`;
27
- const result = await adapter.executeQuery(sql, [
28
- parsed.lng,
29
- parsed.lat,
30
- srid,
31
- ]);
32
- // Add note about SRID for non-4326 cases
33
- const row = result.rows?.[0];
34
- if (row === undefined) {
35
- return {};
31
+ const result = await adapter.executeQuery(sql, [
32
+ parsed.lng,
33
+ parsed.lat,
34
+ srid,
35
+ ]);
36
+ // Add note about SRID for non-4326 cases
37
+ const row = result.rows?.[0];
38
+ if (row === undefined) {
39
+ return {};
40
+ }
41
+ const response = { ...row };
42
+ if (srid !== 4326) {
43
+ response["note"] =
44
+ `Coordinates are WGS84 lat/lng with SRID ${String(srid)} metadata. Use pg_geo_transform to convert to target CRS.`;
45
+ }
46
+ return response;
36
47
  }
37
- const response = { ...row };
38
- if (srid !== 4326) {
39
- response["note"] =
40
- `Coordinates are WGS84 lat/lng with SRID ${String(srid)} metadata. Use pg_geo_transform to convert to target CRS.`;
48
+ catch (error) {
49
+ if (error instanceof ZodError) {
50
+ return {
51
+ success: false,
52
+ error: error.issues.map((i) => i.message).join("; "),
53
+ };
54
+ }
55
+ return {
56
+ success: false,
57
+ error: formatPostgresError(error, { tool: "pg_geocode" }),
58
+ };
41
59
  }
42
- return response;
43
60
  },
44
61
  };
45
62
  }
@@ -56,84 +73,114 @@ export function createGeoTransformTool(adapter) {
56
73
  annotations: readOnly("Transform Geometry"),
57
74
  icons: getToolIcons("postgis", readOnly("Transform Geometry")),
58
75
  handler: async (params, _context) => {
59
- const parsed = GeoTransformSchema.parse(params ?? {});
60
- const schemaName = parsed.schema ?? "public";
61
- const qualifiedTable = schemaName !== "public"
62
- ? `"${schemaName}"."${parsed.table}"`
63
- : `"${parsed.table}"`;
64
- const columnName = `"${parsed.column}"`;
65
- // Auto-detect fromSrid from column metadata if not provided
66
- let fromSrid = parsed.fromSrid;
67
- if (fromSrid === 0) {
68
- const sridQuery = `
69
- SELECT srid FROM geometry_columns
70
- WHERE f_table_schema = $1 AND f_table_name = $2 AND f_geometry_column = $3
71
- UNION
72
- SELECT srid FROM geography_columns
73
- WHERE f_table_schema = $1 AND f_table_name = $2 AND f_geography_column = $3
74
- LIMIT 1
76
+ try {
77
+ const parsed = GeoTransformSchema.parse(params ?? {});
78
+ const schemaName = parsed.schema ?? "public";
79
+ const qualifiedTable = sanitizeTableName(parsed.table, schemaName !== "public" ? schemaName : undefined);
80
+ const columnName = sanitizeIdentifier(parsed.column);
81
+ // Auto-detect fromSrid from column metadata if not provided
82
+ let fromSrid = parsed.fromSrid;
83
+ if (fromSrid === 0) {
84
+ // Check if table exists before attempting SRID auto-detection
85
+ const tableCheckSql = `SELECT 1 FROM information_schema.tables WHERE table_schema = $1 AND table_name = $2`;
86
+ const tableCheckResult = await adapter.executeQuery(tableCheckSql, [
87
+ schemaName,
88
+ parsed.table,
89
+ ]);
90
+ if ((tableCheckResult.rows?.length ?? 0) === 0) {
91
+ return {
92
+ success: false,
93
+ error: `Table or view '${parsed.table}' not found. Use pg_list_tables to see available tables.`,
94
+ };
95
+ }
96
+ const sridQuery = `
97
+ SELECT srid FROM geometry_columns
98
+ WHERE f_table_schema = $1 AND f_table_name = $2 AND f_geometry_column = $3
99
+ UNION
100
+ SELECT srid FROM geography_columns
101
+ WHERE f_table_schema = $1 AND f_table_name = $2 AND f_geography_column = $3
102
+ LIMIT 1
103
+ `;
104
+ const sridResult = await adapter.executeQuery(sridQuery, [
105
+ schemaName,
106
+ parsed.table,
107
+ parsed.column,
108
+ ]);
109
+ const sridValue = sridResult.rows?.[0]?.["srid"];
110
+ if (sridValue !== undefined && sridValue !== null) {
111
+ fromSrid = Number(sridValue);
112
+ }
113
+ else {
114
+ return {
115
+ success: false,
116
+ error: `Could not auto-detect SRID for column "${parsed.column}" on table "${parsed.table}". Provide fromSrid (or sourceSrid) explicitly.`,
117
+ suggestion: `Use fromSrid: 4326 for WGS84/GPS coordinates, or fromSrid: 3857 for Web Mercator`,
118
+ };
119
+ }
120
+ }
121
+ const whereClause = parsed.where !== undefined
122
+ ? `WHERE ${sanitizeWhereClause(parsed.where)}`
123
+ : "";
124
+ // Default limit of 50 to prevent large payloads, use limit: 0 for all
125
+ const effectiveLimit = parsed.limit ?? 50;
126
+ const limitClause = effectiveLimit > 0 ? `LIMIT ${String(effectiveLimit)}` : "";
127
+ // Get non-geometry columns to avoid returning raw WKB
128
+ const colQuery = `
129
+ SELECT column_name FROM information_schema.columns
130
+ WHERE table_schema = $1 AND table_name = $2
131
+ AND udt_name NOT IN ('geometry', 'geography')
132
+ ORDER BY ordinal_position
75
133
  `;
76
- const sridResult = await adapter.executeQuery(sridQuery, [
134
+ const colResult = await adapter.executeQuery(colQuery, [
77
135
  schemaName,
78
136
  parsed.table,
79
- parsed.column,
80
137
  ]);
81
- const sridValue = sridResult.rows?.[0]?.["srid"];
82
- if (sridValue !== undefined && sridValue !== null) {
83
- fromSrid = Number(sridValue);
138
+ const nonGeomCols = (colResult.rows ?? [])
139
+ .map((row) => `"${String(row["column_name"])}"`)
140
+ .join(", ");
141
+ // Select non-geometry columns + transformed geometry representations
142
+ const selectCols = nonGeomCols.length > 0
143
+ ? `${nonGeomCols}, ST_AsGeoJSON(ST_Transform(ST_SetSRID(${columnName}, ${String(fromSrid)}), ${String(parsed.toSrid)})) as transformed_geojson, ST_AsText(ST_Transform(ST_SetSRID(${columnName}, ${String(fromSrid)}), ${String(parsed.toSrid)})) as transformed_wkt, ${String(parsed.toSrid)} as output_srid`
144
+ : `ST_AsGeoJSON(ST_Transform(ST_SetSRID(${columnName}, ${String(fromSrid)}), ${String(parsed.toSrid)})) as transformed_geojson, ST_AsText(ST_Transform(ST_SetSRID(${columnName}, ${String(fromSrid)}), ${String(parsed.toSrid)})) as transformed_wkt, ${String(parsed.toSrid)} as output_srid`;
145
+ const sql = `SELECT ${selectCols} FROM ${qualifiedTable} ${whereClause} ${limitClause}`;
146
+ const result = await adapter.executeQuery(sql);
147
+ // Build response with truncation indicators if default limit was applied
148
+ const response = {
149
+ results: result.rows,
150
+ count: result.rows?.length ?? 0,
151
+ fromSrid: fromSrid,
152
+ toSrid: parsed.toSrid,
153
+ ...(parsed.fromSrid === 0 && { autoDetectedSrid: true }),
154
+ };
155
+ // Check if results were truncated (works for both default and explicit limits)
156
+ if (effectiveLimit > 0) {
157
+ const countSql = `SELECT COUNT(*) as cnt FROM ${qualifiedTable} ${whereClause}`;
158
+ const countResult = await adapter.executeQuery(countSql);
159
+ const totalCount = Number(countResult.rows?.[0]?.["cnt"] ?? 0);
160
+ if (totalCount > effectiveLimit) {
161
+ response["truncated"] = true;
162
+ response["totalCount"] = totalCount;
163
+ response["limit"] = effectiveLimit;
164
+ }
84
165
  }
85
- else {
166
+ return response;
167
+ }
168
+ catch (error) {
169
+ if (error instanceof ZodError) {
86
170
  return {
87
171
  success: false,
88
- error: `Could not auto-detect SRID for column "${parsed.column}" on table "${parsed.table}". Provide fromSrid (or sourceSrid) explicitly.`,
89
- suggestion: `Use fromSrid: 4326 for WGS84/GPS coordinates, or fromSrid: 3857 for Web Mercator`,
172
+ error: error.issues.map((i) => i.message).join("; "),
90
173
  };
91
174
  }
175
+ return {
176
+ success: false,
177
+ error: formatPostgresError(error, {
178
+ tool: "pg_geo_transform",
179
+ table: params?.["table"] ??
180
+ undefined,
181
+ }),
182
+ };
92
183
  }
93
- const whereClause = parsed.where !== undefined ? `WHERE ${parsed.where}` : "";
94
- // Default limit of 50 to prevent large payloads, use limit: 0 for all
95
- const effectiveLimit = parsed.limit ?? 50;
96
- const limitClause = effectiveLimit > 0 ? `LIMIT ${String(effectiveLimit)}` : "";
97
- // Get non-geometry columns to avoid returning raw WKB
98
- const colQuery = `
99
- SELECT column_name FROM information_schema.columns
100
- WHERE table_schema = $1 AND table_name = $2
101
- AND udt_name NOT IN ('geometry', 'geography')
102
- ORDER BY ordinal_position
103
- `;
104
- const colResult = await adapter.executeQuery(colQuery, [
105
- schemaName,
106
- parsed.table,
107
- ]);
108
- const nonGeomCols = (colResult.rows ?? [])
109
- .map((row) => `"${String(row["column_name"])}"`)
110
- .join(", ");
111
- // Select non-geometry columns + transformed geometry representations
112
- const selectCols = nonGeomCols.length > 0
113
- ? `${nonGeomCols}, ST_AsGeoJSON(ST_Transform(ST_SetSRID(${columnName}, ${String(fromSrid)}), ${String(parsed.toSrid)})) as transformed_geojson, ST_AsText(ST_Transform(ST_SetSRID(${columnName}, ${String(fromSrid)}), ${String(parsed.toSrid)})) as transformed_wkt, ${String(parsed.toSrid)} as output_srid`
114
- : `ST_AsGeoJSON(ST_Transform(ST_SetSRID(${columnName}, ${String(fromSrid)}), ${String(parsed.toSrid)})) as transformed_geojson, ST_AsText(ST_Transform(ST_SetSRID(${columnName}, ${String(fromSrid)}), ${String(parsed.toSrid)})) as transformed_wkt, ${String(parsed.toSrid)} as output_srid`;
115
- const sql = `SELECT ${selectCols} FROM ${qualifiedTable} ${whereClause} ${limitClause}`;
116
- const result = await adapter.executeQuery(sql);
117
- // Build response with truncation indicators if default limit was applied
118
- const response = {
119
- results: result.rows,
120
- count: result.rows?.length ?? 0,
121
- fromSrid: fromSrid,
122
- toSrid: parsed.toSrid,
123
- ...(parsed.fromSrid === 0 && { autoDetectedSrid: true }),
124
- };
125
- // Check if results were truncated (works for both default and explicit limits)
126
- if (effectiveLimit > 0) {
127
- const countSql = `SELECT COUNT(*) as cnt FROM ${qualifiedTable} ${whereClause}`;
128
- const countResult = await adapter.executeQuery(countSql);
129
- const totalCount = Number(countResult.rows?.[0]?.["cnt"] ?? 0);
130
- if (totalCount > effectiveLimit) {
131
- response["truncated"] = true;
132
- response["totalCount"] = totalCount;
133
- response["limit"] = effectiveLimit;
134
- }
135
- }
136
- return response;
137
184
  },
138
185
  };
139
186
  }
@@ -159,7 +206,7 @@ export function createGeoIndexOptimizeTool(adapter) {
159
206
  const parsed = params;
160
207
  const schemaName = parsed.schema ?? "public";
161
208
  const indexQuery = `
162
- SELECT
209
+ SELECT
163
210
  c.relname as table_name,
164
211
  i.relname as index_name,
165
212
  a.attname as column_name,
@@ -178,13 +225,16 @@ export function createGeoIndexOptimizeTool(adapter) {
178
225
  WHERE n.nspname = $1
179
226
  AND (pg_get_indexdef(i.oid) LIKE '%gist%' OR pg_get_indexdef(i.oid) LIKE '%spgist%')
180
227
  AND t.typname IN ('geometry', 'geography')
181
- ${parsed.table !== undefined ? `AND c.relname = '${parsed.table}'` : ""}
228
+ ${parsed.table !== undefined ? `AND c.relname = $2` : ""}
182
229
  ORDER BY index_size_bytes DESC
183
230
  `;
231
+ const indexParams = [schemaName];
232
+ if (parsed.table !== undefined)
233
+ indexParams.push(parsed.table);
184
234
  const [indexes, tableStats] = await Promise.all([
185
- adapter.executeQuery(indexQuery, [schemaName]),
235
+ adapter.executeQuery(indexQuery, indexParams),
186
236
  adapter.executeQuery(`
187
- SELECT
237
+ SELECT
188
238
  c.relname as table_name,
189
239
  n_live_tup as row_count,
190
240
  pg_size_pretty(pg_table_size(c.oid)) as table_size
@@ -192,14 +242,14 @@ export function createGeoIndexOptimizeTool(adapter) {
192
242
  JOIN pg_class c ON c.relname = t.relname
193
243
  JOIN pg_namespace n ON n.oid = c.relnamespace
194
244
  WHERE n.nspname = $1
195
- ${parsed.table !== undefined ? `AND c.relname = '${parsed.table}'` : ""}
245
+ ${parsed.table !== undefined ? `AND c.relname = $${String(parsed.table !== undefined ? 2 : 1)}` : ""}
196
246
  AND EXISTS (
197
247
  SELECT 1 FROM information_schema.columns ic
198
- WHERE ic.table_schema = n.nspname
199
- AND ic.table_name = c.relname
248
+ WHERE ic.table_schema = n.nspname
249
+ AND ic.table_name = c.relname
200
250
  AND ic.udt_name IN ('geometry', 'geography')
201
251
  )
202
- `, [schemaName]),
252
+ `, indexParams),
203
253
  ]);
204
254
  const recommendations = [];
205
255
  for (const idx of indexes.rows ?? []) {
@@ -267,151 +317,169 @@ export function createGeoClusterTool(adapter) {
267
317
  annotations: readOnly("Geo Cluster"),
268
318
  icons: getToolIcons("postgis", readOnly("Geo Cluster")),
269
319
  handler: async (params, _context) => {
270
- const parsed = GeoClusterSchema.parse(params ?? {});
271
- const method = parsed.method ?? "dbscan";
272
- const schemaName = parsed.schema ?? "public";
273
- const qualifiedTable = schemaName !== "public"
274
- ? `"${schemaName}"."${parsed.table}"`
275
- : `"${parsed.table}"`;
276
- const whereClause = parsed.where !== undefined ? `WHERE ${parsed.where}` : "";
277
- const limitClause = parsed.limit !== undefined && parsed.limit > 0
278
- ? `LIMIT ${String(parsed.limit)}`
279
- : "";
280
- // Track warning if K > N
281
- let warning;
282
- // For K-Means, validate and adjust numClusters
283
- let effectiveNumClusters = parsed.numClusters ?? 5;
284
- let rowCount = 0;
285
- if (method === "kmeans") {
286
- // Validate numClusters > 0
287
- if (effectiveNumClusters <= 0) {
288
- return {
289
- error: `numClusters must be greater than 0 (received: ${String(effectiveNumClusters)}).`,
290
- method,
291
- table: parsed.table,
292
- numClusters: effectiveNumClusters,
293
- suggestion: "Provide a positive integer for numClusters (e.g., numClusters: 3)",
294
- };
320
+ try {
321
+ const parsed = GeoClusterSchema.parse(params ?? {});
322
+ const method = parsed.method ?? "dbscan";
323
+ const schemaName = parsed.schema ?? "public";
324
+ const qualifiedTable = sanitizeTableName(parsed.table, schemaName !== "public" ? schemaName : undefined);
325
+ const whereClause = parsed.where !== undefined
326
+ ? `WHERE ${sanitizeWhereClause(parsed.where)}`
327
+ : "";
328
+ const limitClause = parsed.limit !== undefined && parsed.limit > 0
329
+ ? `LIMIT ${String(parsed.limit)}`
330
+ : "";
331
+ // Track warning if K > N
332
+ let warning;
333
+ // For K-Means, validate and adjust numClusters
334
+ let effectiveNumClusters = parsed.numClusters ?? 5;
335
+ let rowCount;
336
+ if (method === "kmeans") {
337
+ // Validate numClusters > 0
338
+ if (effectiveNumClusters <= 0) {
339
+ return {
340
+ error: `numClusters must be greater than 0 (received: ${String(effectiveNumClusters)}).`,
341
+ method,
342
+ table: parsed.table,
343
+ numClusters: effectiveNumClusters,
344
+ suggestion: "Provide a positive integer for numClusters (e.g., numClusters: 3)",
345
+ };
346
+ }
347
+ const countResult = await adapter.executeQuery(`SELECT COUNT(*) as cnt FROM ${qualifiedTable} ${whereClause}`);
348
+ rowCount = Number(countResult.rows?.[0]?.["cnt"] ?? 0);
349
+ if (rowCount === 0) {
350
+ return {
351
+ error: `No rows found in table ${parsed.table}${whereClause !== "" ? " matching filter" : ""}. K-Means requires at least 1 row.`,
352
+ method,
353
+ table: parsed.table,
354
+ rowCount: 0,
355
+ };
356
+ }
357
+ // Clamp K to row count and warn if exceeded
358
+ if (effectiveNumClusters > rowCount) {
359
+ warning = `Requested ${String(parsed.numClusters)} clusters but only ${String(rowCount)} rows available. Using ${String(rowCount)} clusters instead.`;
360
+ effectiveNumClusters = rowCount;
361
+ }
295
362
  }
296
- const countResult = await adapter.executeQuery(`SELECT COUNT(*) as cnt FROM ${qualifiedTable} ${whereClause}`);
297
- rowCount = Number(countResult.rows?.[0]?.["cnt"] ?? 0);
298
- if (rowCount === 0) {
299
- return {
300
- error: `No rows found in table ${parsed.table}${whereClause !== "" ? " matching filter" : ""}. K-Means requires at least 1 row.`,
301
- method,
302
- table: parsed.table,
303
- rowCount: 0,
304
- };
363
+ let clusterFunction;
364
+ if (method === "kmeans") {
365
+ clusterFunction = `ST_ClusterKMeans("${parsed.column}", ${String(effectiveNumClusters)}) OVER ()`;
305
366
  }
306
- // Clamp K to row count and warn if exceeded
307
- if (effectiveNumClusters > rowCount) {
308
- warning = `Requested ${String(parsed.numClusters)} clusters but only ${String(rowCount)} rows available. Using ${String(rowCount)} clusters instead.`;
309
- effectiveNumClusters = rowCount;
367
+ else {
368
+ const eps = parsed.eps ?? 100;
369
+ const minPoints = parsed.minPoints ?? 3;
370
+ clusterFunction = `ST_ClusterDBSCAN("${parsed.column}", ${String(eps)}, ${String(minPoints)}) OVER ()`;
310
371
  }
311
- }
312
- let clusterFunction;
313
- if (method === "kmeans") {
314
- clusterFunction = `ST_ClusterKMeans("${parsed.column}", ${String(effectiveNumClusters)}) OVER ()`;
315
- }
316
- else {
317
- const eps = parsed.eps ?? 100;
318
- const minPoints = parsed.minPoints ?? 3;
319
- clusterFunction = `ST_ClusterDBSCAN("${parsed.column}", ${String(eps)}, ${String(minPoints)}) OVER ()`;
320
- }
321
- const sql = `
322
- WITH clustered AS (
323
- SELECT
324
- *,
325
- ${clusterFunction} as cluster_id
326
- FROM ${qualifiedTable}
327
- ${whereClause}
328
- )
329
- SELECT
330
- cluster_id,
331
- COUNT(*) as point_count,
332
- ST_AsGeoJSON(ST_Centroid(ST_Collect("${parsed.column}"))) as centroid,
333
- ST_AsGeoJSON(ST_ConvexHull(ST_Collect("${parsed.column}"))) as hull
334
- FROM clustered
335
- WHERE cluster_id IS NOT NULL
336
- GROUP BY cluster_id
337
- ORDER BY point_count DESC
338
- ${limitClause}
339
- `;
340
- const [clustersResult, summaryResult] = await Promise.all([
341
- adapter.executeQuery(sql),
342
- adapter.executeQuery(`
343
- WITH clustered AS (
344
- SELECT ${clusterFunction} as cluster_id
345
- FROM ${qualifiedTable}
346
- ${whereClause}
347
- )
348
- SELECT
349
- COUNT(DISTINCT cluster_id) as num_clusters,
350
- COUNT(*) FILTER (WHERE cluster_id IS NULL) as noise_points,
351
- COUNT(*) as total_points
352
- FROM clustered
353
- `),
354
- ]);
355
- // Normalize cluster point_count to numbers
356
- const normalizedClusters = (clustersResult.rows ?? []).map((row) => ({
357
- ...row,
358
- point_count: Number(row["point_count"]),
359
- }));
360
- // Normalize summary values to numbers for consistency
361
- const rawSummary = summaryResult.rows?.[0] ?? {};
362
- const normalizedSummary = {
363
- num_clusters: Number(rawSummary["num_clusters"] ?? 0),
364
- noise_points: Number(rawSummary["noise_points"] ?? 0),
365
- total_points: Number(rawSummary["total_points"] ?? 0),
366
- };
367
- // Build response
368
- const response = {
369
- method,
370
- parameters: method === "kmeans"
371
- ? { numClusters: effectiveNumClusters }
372
- : { eps: parsed.eps ?? 100, minPoints: parsed.minPoints ?? 3 },
373
- summary: normalizedSummary,
374
- clusters: normalizedClusters,
375
- };
376
- // Add warning if K was clamped
377
- if (warning !== undefined) {
378
- response["warning"] = warning;
379
- response["requestedClusters"] = parsed.numClusters;
380
- response["actualClusters"] = effectiveNumClusters;
381
- }
382
- // Add contextual hints based on method and results
383
- const numClusters = normalizedSummary.num_clusters;
384
- const noisePoints = normalizedSummary.noise_points;
385
- const totalPoints = normalizedSummary.total_points;
386
- if (method === "dbscan") {
387
- const eps = parsed.eps ?? 100;
388
- const minPoints = parsed.minPoints ?? 3;
389
- // Provide hints about DBSCAN parameter trade-offs
390
- const hints = [];
391
- if (numClusters === 1 && totalPoints > 1) {
392
- hints.push(`All ${String(totalPoints)} points formed a single cluster. Consider decreasing eps (currently ${String(eps)}m) to create more distinct clusters.`);
372
+ const sql = `
373
+ WITH clustered AS (
374
+ SELECT
375
+ *,
376
+ ${clusterFunction} as cluster_id
377
+ FROM ${qualifiedTable}
378
+ ${whereClause}
379
+ )
380
+ SELECT
381
+ cluster_id,
382
+ COUNT(*) as point_count,
383
+ ST_AsGeoJSON(ST_Centroid(ST_Collect("${parsed.column}"))) as centroid,
384
+ ST_AsGeoJSON(ST_ConvexHull(ST_Collect("${parsed.column}"))) as hull
385
+ FROM clustered
386
+ WHERE cluster_id IS NOT NULL
387
+ GROUP BY cluster_id
388
+ ORDER BY point_count DESC
389
+ ${limitClause}
390
+ `;
391
+ const [clustersResult, summaryResult] = await Promise.all([
392
+ adapter.executeQuery(sql),
393
+ adapter.executeQuery(`
394
+ WITH clustered AS (
395
+ SELECT ${clusterFunction} as cluster_id
396
+ FROM ${qualifiedTable}
397
+ ${whereClause}
398
+ )
399
+ SELECT
400
+ COUNT(DISTINCT cluster_id) as num_clusters,
401
+ COUNT(*) FILTER (WHERE cluster_id IS NULL) as noise_points,
402
+ COUNT(*) as total_points
403
+ FROM clustered
404
+ `),
405
+ ]);
406
+ // Normalize cluster point_count to numbers
407
+ const normalizedClusters = (clustersResult.rows ?? []).map((row) => ({
408
+ ...row,
409
+ point_count: Number(row["point_count"]),
410
+ }));
411
+ // Normalize summary values to numbers for consistency
412
+ const rawSummary = summaryResult.rows?.[0] ?? {};
413
+ const normalizedSummary = {
414
+ num_clusters: Number(rawSummary["num_clusters"] ?? 0),
415
+ noise_points: Number(rawSummary["noise_points"] ?? 0),
416
+ total_points: Number(rawSummary["total_points"] ?? 0),
417
+ };
418
+ // Build response
419
+ const response = {
420
+ method,
421
+ parameters: method === "kmeans"
422
+ ? { numClusters: effectiveNumClusters }
423
+ : { eps: parsed.eps ?? 100, minPoints: parsed.minPoints ?? 3 },
424
+ summary: normalizedSummary,
425
+ clusters: normalizedClusters,
426
+ };
427
+ // Add warning if K was clamped
428
+ if (warning !== undefined) {
429
+ response["warning"] = warning;
430
+ response["requestedClusters"] = parsed.numClusters;
431
+ response["actualClusters"] = effectiveNumClusters;
393
432
  }
394
- if (noisePoints > 0 && noisePoints > totalPoints * 0.5) {
395
- hints.push(`${String(noisePoints)} of ${String(totalPoints)} points (${String(Math.round((noisePoints / totalPoints) * 100))}%) are noise. Consider increasing eps or decreasing minPoints (currently ${String(minPoints)}).`);
433
+ // Add contextual hints based on method and results
434
+ const numClusters = normalizedSummary.num_clusters;
435
+ const noisePoints = normalizedSummary.noise_points;
436
+ const totalPoints = normalizedSummary.total_points;
437
+ if (method === "dbscan") {
438
+ const eps = parsed.eps ?? 100;
439
+ const minPoints = parsed.minPoints ?? 3;
440
+ // Provide hints about DBSCAN parameter trade-offs
441
+ const hints = [];
442
+ if (numClusters === 1 && totalPoints > 1) {
443
+ hints.push(`All ${String(totalPoints)} points formed a single cluster. Consider decreasing eps (currently ${String(eps)}m) to create more distinct clusters.`);
444
+ }
445
+ if (noisePoints > 0 && noisePoints > totalPoints * 0.5) {
446
+ hints.push(`${String(noisePoints)} of ${String(totalPoints)} points (${String(Math.round((noisePoints / totalPoints) * 100))}%) are noise. Consider increasing eps or decreasing minPoints (currently ${String(minPoints)}).`);
447
+ }
448
+ if (numClusters === 0 && totalPoints > 0) {
449
+ hints.push(`No clusters formed - all points are noise. Try increasing eps (currently ${String(eps)}m) or decreasing minPoints (currently ${String(minPoints)}).`);
450
+ }
451
+ response["notes"] =
452
+ "Noise points (cluster_id = NULL) are points not belonging to any cluster";
453
+ if (hints.length > 0) {
454
+ response["hints"] = hints;
455
+ }
456
+ response["parameterGuide"] = {
457
+ eps: `Distance threshold in meters. Larger values group more distant points together.`,
458
+ minPoints: `Minimum points required to form a cluster. Higher values create fewer, denser clusters.`,
459
+ };
396
460
  }
397
- if (numClusters === 0 && totalPoints > 0) {
398
- hints.push(`No clusters formed - all points are noise. Try increasing eps (currently ${String(eps)}m) or decreasing minPoints (currently ${String(minPoints)}).`);
461
+ else {
462
+ response["notes"] =
463
+ "K-Means will always assign all points to a cluster";
399
464
  }
400
- response["notes"] =
401
- "Noise points (cluster_id = NULL) are points not belonging to any cluster";
402
- if (hints.length > 0) {
403
- response["hints"] = hints;
465
+ return response;
466
+ }
467
+ catch (error) {
468
+ if (error instanceof ZodError) {
469
+ return {
470
+ success: false,
471
+ error: error.issues.map((i) => i.message).join("; "),
472
+ };
404
473
  }
405
- response["parameterGuide"] = {
406
- eps: `Distance threshold in meters. Larger values group more distant points together.`,
407
- minPoints: `Minimum points required to form a cluster. Higher values create fewer, denser clusters.`,
474
+ return {
475
+ success: false,
476
+ error: formatPostgresError(error, {
477
+ tool: "pg_geo_cluster",
478
+ table: params?.["table"] ??
479
+ undefined,
480
+ }),
408
481
  };
409
482
  }
410
- else {
411
- response["notes"] =
412
- "K-Means will always assign all points to a cluster";
413
- }
414
- return response;
415
483
  },
416
484
  };
417
485
  }