db-mcp 1.0.1

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 (208) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +860 -0
  3. package/dist/adapters/DatabaseAdapter.d.ts +141 -0
  4. package/dist/adapters/DatabaseAdapter.d.ts.map +1 -0
  5. package/dist/adapters/DatabaseAdapter.js +131 -0
  6. package/dist/adapters/DatabaseAdapter.js.map +1 -0
  7. package/dist/adapters/sqlite/SchemaManager.d.ts +58 -0
  8. package/dist/adapters/sqlite/SchemaManager.d.ts.map +1 -0
  9. package/dist/adapters/sqlite/SchemaManager.js +187 -0
  10. package/dist/adapters/sqlite/SchemaManager.js.map +1 -0
  11. package/dist/adapters/sqlite/SqliteAdapter.d.ts +161 -0
  12. package/dist/adapters/sqlite/SqliteAdapter.d.ts.map +1 -0
  13. package/dist/adapters/sqlite/SqliteAdapter.js +741 -0
  14. package/dist/adapters/sqlite/SqliteAdapter.js.map +1 -0
  15. package/dist/adapters/sqlite/index.d.ts +9 -0
  16. package/dist/adapters/sqlite/index.d.ts.map +1 -0
  17. package/dist/adapters/sqlite/index.js +8 -0
  18. package/dist/adapters/sqlite/index.js.map +1 -0
  19. package/dist/adapters/sqlite/json-utils.d.ts +100 -0
  20. package/dist/adapters/sqlite/json-utils.d.ts.map +1 -0
  21. package/dist/adapters/sqlite/json-utils.js +274 -0
  22. package/dist/adapters/sqlite/json-utils.js.map +1 -0
  23. package/dist/adapters/sqlite/output-schemas.d.ts +1187 -0
  24. package/dist/adapters/sqlite/output-schemas.d.ts.map +1 -0
  25. package/dist/adapters/sqlite/output-schemas.js +1337 -0
  26. package/dist/adapters/sqlite/output-schemas.js.map +1 -0
  27. package/dist/adapters/sqlite/prompts.d.ts +13 -0
  28. package/dist/adapters/sqlite/prompts.d.ts.map +1 -0
  29. package/dist/adapters/sqlite/prompts.js +605 -0
  30. package/dist/adapters/sqlite/prompts.js.map +1 -0
  31. package/dist/adapters/sqlite/resources.d.ts +13 -0
  32. package/dist/adapters/sqlite/resources.d.ts.map +1 -0
  33. package/dist/adapters/sqlite/resources.js +251 -0
  34. package/dist/adapters/sqlite/resources.js.map +1 -0
  35. package/dist/adapters/sqlite/tools/admin.d.ts +14 -0
  36. package/dist/adapters/sqlite/tools/admin.d.ts.map +1 -0
  37. package/dist/adapters/sqlite/tools/admin.js +788 -0
  38. package/dist/adapters/sqlite/tools/admin.js.map +1 -0
  39. package/dist/adapters/sqlite/tools/core.d.ts +25 -0
  40. package/dist/adapters/sqlite/tools/core.d.ts.map +1 -0
  41. package/dist/adapters/sqlite/tools/core.js +359 -0
  42. package/dist/adapters/sqlite/tools/core.js.map +1 -0
  43. package/dist/adapters/sqlite/tools/fts.d.ts +13 -0
  44. package/dist/adapters/sqlite/tools/fts.d.ts.map +1 -0
  45. package/dist/adapters/sqlite/tools/fts.js +347 -0
  46. package/dist/adapters/sqlite/tools/fts.js.map +1 -0
  47. package/dist/adapters/sqlite/tools/geo.d.ts +14 -0
  48. package/dist/adapters/sqlite/tools/geo.d.ts.map +1 -0
  49. package/dist/adapters/sqlite/tools/geo.js +252 -0
  50. package/dist/adapters/sqlite/tools/geo.js.map +1 -0
  51. package/dist/adapters/sqlite/tools/index.d.ts +30 -0
  52. package/dist/adapters/sqlite/tools/index.d.ts.map +1 -0
  53. package/dist/adapters/sqlite/tools/index.js +61 -0
  54. package/dist/adapters/sqlite/tools/index.js.map +1 -0
  55. package/dist/adapters/sqlite/tools/json-helpers.d.ts +14 -0
  56. package/dist/adapters/sqlite/tools/json-helpers.d.ts.map +1 -0
  57. package/dist/adapters/sqlite/tools/json-helpers.js +477 -0
  58. package/dist/adapters/sqlite/tools/json-helpers.js.map +1 -0
  59. package/dist/adapters/sqlite/tools/json-operations.d.ts +14 -0
  60. package/dist/adapters/sqlite/tools/json-operations.d.ts.map +1 -0
  61. package/dist/adapters/sqlite/tools/json-operations.js +839 -0
  62. package/dist/adapters/sqlite/tools/json-operations.js.map +1 -0
  63. package/dist/adapters/sqlite/tools/stats.d.ts +15 -0
  64. package/dist/adapters/sqlite/tools/stats.d.ts.map +1 -0
  65. package/dist/adapters/sqlite/tools/stats.js +1219 -0
  66. package/dist/adapters/sqlite/tools/stats.js.map +1 -0
  67. package/dist/adapters/sqlite/tools/text.d.ts +14 -0
  68. package/dist/adapters/sqlite/tools/text.d.ts.map +1 -0
  69. package/dist/adapters/sqlite/tools/text.js +1141 -0
  70. package/dist/adapters/sqlite/tools/text.js.map +1 -0
  71. package/dist/adapters/sqlite/tools/vector.d.ts +14 -0
  72. package/dist/adapters/sqlite/tools/vector.d.ts.map +1 -0
  73. package/dist/adapters/sqlite/tools/vector.js +613 -0
  74. package/dist/adapters/sqlite/tools/vector.js.map +1 -0
  75. package/dist/adapters/sqlite/tools/virtual.d.ts +13 -0
  76. package/dist/adapters/sqlite/tools/virtual.d.ts.map +1 -0
  77. package/dist/adapters/sqlite/tools/virtual.js +930 -0
  78. package/dist/adapters/sqlite/tools/virtual.js.map +1 -0
  79. package/dist/adapters/sqlite/types.d.ts +207 -0
  80. package/dist/adapters/sqlite/types.d.ts.map +1 -0
  81. package/dist/adapters/sqlite/types.js +186 -0
  82. package/dist/adapters/sqlite/types.js.map +1 -0
  83. package/dist/adapters/sqlite-native/NativeSqliteAdapter.d.ts +163 -0
  84. package/dist/adapters/sqlite-native/NativeSqliteAdapter.d.ts.map +1 -0
  85. package/dist/adapters/sqlite-native/NativeSqliteAdapter.js +748 -0
  86. package/dist/adapters/sqlite-native/NativeSqliteAdapter.js.map +1 -0
  87. package/dist/adapters/sqlite-native/index.d.ts +11 -0
  88. package/dist/adapters/sqlite-native/index.d.ts.map +1 -0
  89. package/dist/adapters/sqlite-native/index.js +11 -0
  90. package/dist/adapters/sqlite-native/index.js.map +1 -0
  91. package/dist/adapters/sqlite-native/tools/spatialite.d.ts +19 -0
  92. package/dist/adapters/sqlite-native/tools/spatialite.d.ts.map +1 -0
  93. package/dist/adapters/sqlite-native/tools/spatialite.js +628 -0
  94. package/dist/adapters/sqlite-native/tools/spatialite.js.map +1 -0
  95. package/dist/adapters/sqlite-native/tools/transactions.d.ts +12 -0
  96. package/dist/adapters/sqlite-native/tools/transactions.d.ts.map +1 -0
  97. package/dist/adapters/sqlite-native/tools/transactions.js +255 -0
  98. package/dist/adapters/sqlite-native/tools/transactions.js.map +1 -0
  99. package/dist/adapters/sqlite-native/tools/window.d.ts +12 -0
  100. package/dist/adapters/sqlite-native/tools/window.d.ts.map +1 -0
  101. package/dist/adapters/sqlite-native/tools/window.js +370 -0
  102. package/dist/adapters/sqlite-native/tools/window.js.map +1 -0
  103. package/dist/auth/AuthorizationServerDiscovery.d.ts +90 -0
  104. package/dist/auth/AuthorizationServerDiscovery.d.ts.map +1 -0
  105. package/dist/auth/AuthorizationServerDiscovery.js +204 -0
  106. package/dist/auth/AuthorizationServerDiscovery.js.map +1 -0
  107. package/dist/auth/OAuthResourceServer.d.ts +65 -0
  108. package/dist/auth/OAuthResourceServer.d.ts.map +1 -0
  109. package/dist/auth/OAuthResourceServer.js +121 -0
  110. package/dist/auth/OAuthResourceServer.js.map +1 -0
  111. package/dist/auth/TokenValidator.d.ts +60 -0
  112. package/dist/auth/TokenValidator.d.ts.map +1 -0
  113. package/dist/auth/TokenValidator.js +235 -0
  114. package/dist/auth/TokenValidator.js.map +1 -0
  115. package/dist/auth/errors.d.ts +74 -0
  116. package/dist/auth/errors.d.ts.map +1 -0
  117. package/dist/auth/errors.js +133 -0
  118. package/dist/auth/errors.js.map +1 -0
  119. package/dist/auth/index.d.ts +13 -0
  120. package/dist/auth/index.d.ts.map +1 -0
  121. package/dist/auth/index.js +15 -0
  122. package/dist/auth/index.js.map +1 -0
  123. package/dist/auth/middleware.d.ts +81 -0
  124. package/dist/auth/middleware.d.ts.map +1 -0
  125. package/dist/auth/middleware.js +291 -0
  126. package/dist/auth/middleware.js.map +1 -0
  127. package/dist/auth/scopes.d.ts +136 -0
  128. package/dist/auth/scopes.d.ts.map +1 -0
  129. package/dist/auth/scopes.js +349 -0
  130. package/dist/auth/scopes.js.map +1 -0
  131. package/dist/auth/types.d.ts +257 -0
  132. package/dist/auth/types.d.ts.map +1 -0
  133. package/dist/auth/types.js +8 -0
  134. package/dist/auth/types.js.map +1 -0
  135. package/dist/cli.d.ts +8 -0
  136. package/dist/cli.d.ts.map +1 -0
  137. package/dist/cli.js +236 -0
  138. package/dist/cli.js.map +1 -0
  139. package/dist/constants/ServerInstructions.d.ts +45 -0
  140. package/dist/constants/ServerInstructions.d.ts.map +1 -0
  141. package/dist/constants/ServerInstructions.js +356 -0
  142. package/dist/constants/ServerInstructions.js.map +1 -0
  143. package/dist/filtering/ToolConstants.d.ts +34 -0
  144. package/dist/filtering/ToolConstants.d.ts.map +1 -0
  145. package/dist/filtering/ToolConstants.js +174 -0
  146. package/dist/filtering/ToolConstants.js.map +1 -0
  147. package/dist/filtering/ToolFilter.d.ts +82 -0
  148. package/dist/filtering/ToolFilter.d.ts.map +1 -0
  149. package/dist/filtering/ToolFilter.js +296 -0
  150. package/dist/filtering/ToolFilter.js.map +1 -0
  151. package/dist/index.d.ts +13 -0
  152. package/dist/index.d.ts.map +1 -0
  153. package/dist/index.js +17 -0
  154. package/dist/index.js.map +1 -0
  155. package/dist/server/McpServer.d.ts +61 -0
  156. package/dist/server/McpServer.d.ts.map +1 -0
  157. package/dist/server/McpServer.js +270 -0
  158. package/dist/server/McpServer.js.map +1 -0
  159. package/dist/transports/http.d.ts +134 -0
  160. package/dist/transports/http.d.ts.map +1 -0
  161. package/dist/transports/http.js +516 -0
  162. package/dist/transports/http.js.map +1 -0
  163. package/dist/transports/index.d.ts +5 -0
  164. package/dist/transports/index.d.ts.map +1 -0
  165. package/dist/transports/index.js +5 -0
  166. package/dist/transports/index.js.map +1 -0
  167. package/dist/types/index.d.ts +380 -0
  168. package/dist/types/index.d.ts.map +1 -0
  169. package/dist/types/index.js +68 -0
  170. package/dist/types/index.js.map +1 -0
  171. package/dist/utils/annotations.d.ts +44 -0
  172. package/dist/utils/annotations.d.ts.map +1 -0
  173. package/dist/utils/annotations.js +77 -0
  174. package/dist/utils/annotations.js.map +1 -0
  175. package/dist/utils/errors.d.ts +155 -0
  176. package/dist/utils/errors.d.ts.map +1 -0
  177. package/dist/utils/errors.js +329 -0
  178. package/dist/utils/errors.js.map +1 -0
  179. package/dist/utils/identifiers.d.ts +121 -0
  180. package/dist/utils/identifiers.d.ts.map +1 -0
  181. package/dist/utils/identifiers.js +319 -0
  182. package/dist/utils/identifiers.js.map +1 -0
  183. package/dist/utils/index.d.ts +7 -0
  184. package/dist/utils/index.d.ts.map +1 -0
  185. package/dist/utils/index.js +7 -0
  186. package/dist/utils/index.js.map +1 -0
  187. package/dist/utils/insightsManager.d.ts +39 -0
  188. package/dist/utils/insightsManager.d.ts.map +1 -0
  189. package/dist/utils/insightsManager.js +63 -0
  190. package/dist/utils/insightsManager.js.map +1 -0
  191. package/dist/utils/logger.d.ts +189 -0
  192. package/dist/utils/logger.d.ts.map +1 -0
  193. package/dist/utils/logger.js +394 -0
  194. package/dist/utils/logger.js.map +1 -0
  195. package/dist/utils/progress-utils.d.ts +54 -0
  196. package/dist/utils/progress-utils.d.ts.map +1 -0
  197. package/dist/utils/progress-utils.js +74 -0
  198. package/dist/utils/progress-utils.js.map +1 -0
  199. package/dist/utils/resourceAnnotations.d.ts +36 -0
  200. package/dist/utils/resourceAnnotations.d.ts.map +1 -0
  201. package/dist/utils/resourceAnnotations.js +57 -0
  202. package/dist/utils/resourceAnnotations.js.map +1 -0
  203. package/dist/utils/where-clause.d.ts +41 -0
  204. package/dist/utils/where-clause.d.ts.map +1 -0
  205. package/dist/utils/where-clause.js +116 -0
  206. package/dist/utils/where-clause.js.map +1 -0
  207. package/package.json +83 -0
  208. package/server.json +53 -0
@@ -0,0 +1,1141 @@
1
+ /**
2
+ * SQLite Text Processing Tools
3
+ *
4
+ * String manipulation and pattern matching:
5
+ * regex, split, concat, format, fuzzy match, phonetic, normalize, validate.
6
+ * 13 tools total.
7
+ */
8
+ import { z } from "zod";
9
+ import { readOnly, write } from "../../../utils/annotations.js";
10
+ import { validateWhereClause, sanitizeIdentifier, } from "../../../utils/index.js";
11
+ import { RegexMatchOutputSchema, TextSplitOutputSchema, RegexReplaceOutputSchema, } from "../output-schemas.js";
12
+ // Text tool schemas
13
+ const RegexExtractSchema = z.object({
14
+ table: z.string().describe("Table name"),
15
+ column: z.string().describe("Column to extract from"),
16
+ pattern: z.string().describe("Regular expression pattern"),
17
+ groupIndex: z.number().optional().default(0).describe("Capture group index"),
18
+ whereClause: z.string().optional(),
19
+ limit: z.number().optional().default(100),
20
+ });
21
+ const RegexMatchSchema = z.object({
22
+ table: z.string().describe("Table name"),
23
+ column: z.string().describe("Column to match"),
24
+ pattern: z.string().describe("Regular expression pattern"),
25
+ whereClause: z.string().optional(),
26
+ limit: z.number().optional().default(100),
27
+ });
28
+ const TextSplitSchema = z.object({
29
+ table: z.string().describe("Table name"),
30
+ column: z.string().describe("Column to split"),
31
+ delimiter: z.string().describe("Delimiter string"),
32
+ whereClause: z.string().optional(),
33
+ limit: z.number().optional().default(100),
34
+ });
35
+ const TextConcatSchema = z.object({
36
+ table: z.string().describe("Table name"),
37
+ columns: z.array(z.string()).describe("Columns to concatenate"),
38
+ separator: z
39
+ .string()
40
+ .optional()
41
+ .default("")
42
+ .describe("Separator between values"),
43
+ whereClause: z.string().optional(),
44
+ limit: z.number().optional().default(100),
45
+ });
46
+ const TextReplaceSchema = z.object({
47
+ table: z.string().describe("Table name"),
48
+ column: z.string().describe("Column to update"),
49
+ searchPattern: z.string().describe("Text to search for"),
50
+ replaceWith: z.string().describe("Replacement text"),
51
+ whereClause: z.string().describe("WHERE clause"),
52
+ });
53
+ const TextTrimSchema = z.object({
54
+ table: z.string().describe("Table name"),
55
+ column: z.string().describe("Column to trim"),
56
+ mode: z.enum(["both", "left", "right"]).optional().default("both"),
57
+ whereClause: z.string().optional(),
58
+ limit: z.number().optional().default(100),
59
+ });
60
+ const TextCaseSchema = z.object({
61
+ table: z.string().describe("Table name"),
62
+ column: z.string().describe("Column to transform"),
63
+ mode: z.enum(["upper", "lower"]).describe("Case transformation"),
64
+ whereClause: z.string().optional(),
65
+ limit: z.number().optional().default(100),
66
+ });
67
+ const TextSubstringSchema = z.object({
68
+ table: z.string().describe("Table name"),
69
+ column: z.string().describe("Column to extract from"),
70
+ start: z.number().describe("Start position (1-indexed)"),
71
+ length: z.number().optional().describe("Number of characters"),
72
+ whereClause: z.string().optional(),
73
+ limit: z.number().optional().default(100),
74
+ });
75
+ // New text tool schemas
76
+ const FuzzyMatchSchema = z.object({
77
+ table: z.string().describe("Table name"),
78
+ column: z.string().describe("Column to search"),
79
+ search: z.string().describe("Search string"),
80
+ maxDistance: z
81
+ .number()
82
+ .optional()
83
+ .default(3)
84
+ .describe("Maximum Levenshtein distance"),
85
+ tokenize: z
86
+ .boolean()
87
+ .optional()
88
+ .default(true)
89
+ .describe("Split column values into words and match against tokens (default: true). Set false to match entire column value."),
90
+ limit: z.number().optional().default(10),
91
+ });
92
+ const PhoneticMatchSchema = z.object({
93
+ table: z.string().describe("Table name"),
94
+ column: z.string().describe("Column to search"),
95
+ search: z.string().describe("Search string"),
96
+ algorithm: z.enum(["soundex", "metaphone"]).optional().default("soundex"),
97
+ limit: z.number().optional().default(100),
98
+ includeRowData: z
99
+ .boolean()
100
+ .optional()
101
+ .default(true)
102
+ .describe("Include full row data in results (default: true)"),
103
+ });
104
+ const TextNormalizeSchema = z.object({
105
+ table: z.string().describe("Table name"),
106
+ column: z.string().describe("Column to normalize"),
107
+ mode: z
108
+ .enum(["nfc", "nfd", "nfkc", "nfkd", "strip_accents"])
109
+ .describe("Normalization mode"),
110
+ whereClause: z.string().optional(),
111
+ limit: z.number().optional().default(100),
112
+ });
113
+ const TextValidateSchema = z.object({
114
+ table: z.string().describe("Table name"),
115
+ column: z.string().describe("Column to validate"),
116
+ pattern: z
117
+ .enum(["email", "phone", "url", "uuid", "ipv4", "custom"])
118
+ .describe("Validation pattern"),
119
+ customPattern: z
120
+ .string()
121
+ .optional()
122
+ .describe("Custom regex (required if pattern=custom)"),
123
+ whereClause: z.string().optional(),
124
+ limit: z.number().optional().default(100),
125
+ });
126
+ const AdvancedSearchSchema = z.object({
127
+ table: z.string().describe("Table name"),
128
+ column: z.string().describe("Column to search"),
129
+ searchTerm: z.string().describe("Search term"),
130
+ techniques: z
131
+ .array(z.enum(["exact", "fuzzy", "phonetic"]))
132
+ .optional()
133
+ .default(["exact", "fuzzy", "phonetic"])
134
+ .describe("Search techniques to use"),
135
+ fuzzyThreshold: z
136
+ .number()
137
+ .optional()
138
+ .default(0.6)
139
+ .describe("Fuzzy match similarity threshold (0-1). Lower values are more lenient: 0.3-0.4 for loose matching (e.g., 'laptob' matches 'laptop'), 0.6-0.8 for strict matching."),
140
+ whereClause: z.string().optional(),
141
+ limit: z.number().optional().default(100),
142
+ });
143
+ /**
144
+ * Get all text processing tools
145
+ */
146
+ export function getTextTools(adapter) {
147
+ return [
148
+ createRegexExtractTool(adapter),
149
+ createRegexMatchTool(adapter),
150
+ createTextSplitTool(adapter),
151
+ createTextConcatTool(adapter),
152
+ createTextReplaceTool(adapter),
153
+ createTextTrimTool(adapter),
154
+ createTextCaseTool(adapter),
155
+ createTextSubstringTool(adapter),
156
+ createFuzzyMatchTool(adapter),
157
+ createPhoneticMatchTool(adapter),
158
+ createTextNormalizeTool(adapter),
159
+ createTextValidateTool(adapter),
160
+ createAdvancedSearchTool(adapter),
161
+ ];
162
+ }
163
+ /**
164
+ * Extract text using regex pattern
165
+ * Note: SQLite doesn't have native regex, we do this in JS
166
+ */
167
+ function createRegexExtractTool(adapter) {
168
+ return {
169
+ name: "sqlite_regex_extract",
170
+ description: "Extract text matching a regex pattern. Processed in JavaScript after fetching data.",
171
+ group: "text",
172
+ inputSchema: RegexExtractSchema,
173
+ outputSchema: RegexMatchOutputSchema,
174
+ requiredScopes: ["read"],
175
+ annotations: readOnly("Regex Extract"),
176
+ handler: async (params, _context) => {
177
+ const input = RegexExtractSchema.parse(params);
178
+ // Validate and quote identifiers
179
+ const table = sanitizeIdentifier(input.table);
180
+ const column = sanitizeIdentifier(input.column);
181
+ let sql = `SELECT rowid as id, ${column} as value FROM ${table}`;
182
+ if (input.whereClause) {
183
+ validateWhereClause(input.whereClause);
184
+ sql += ` WHERE ${input.whereClause}`;
185
+ }
186
+ sql += ` LIMIT ${input.limit}`;
187
+ const result = await adapter.executeReadQuery(sql);
188
+ // Apply regex in JavaScript
189
+ const regex = new RegExp(input.pattern);
190
+ const extracts = (result.rows ?? [])
191
+ .map((row) => {
192
+ const rawValue = row["value"];
193
+ const value = typeof rawValue === "string"
194
+ ? rawValue
195
+ : JSON.stringify(rawValue ?? "");
196
+ const match = regex.exec(value);
197
+ // Safely coerce rowid to number, defaulting to row index or 0
198
+ const rawRowid = row["id"];
199
+ const rowid = typeof rawRowid === "number"
200
+ ? rawRowid
201
+ : typeof rawRowid === "string"
202
+ ? parseInt(rawRowid, 10) || 0
203
+ : 0;
204
+ return {
205
+ rowid,
206
+ original: value,
207
+ extracted: match ? (match[input.groupIndex] ?? match[0]) : null,
208
+ };
209
+ })
210
+ .filter((r) => r.extracted !== null);
211
+ return {
212
+ success: true,
213
+ rowCount: extracts.length,
214
+ matches: extracts,
215
+ };
216
+ },
217
+ };
218
+ }
219
+ /**
220
+ * Match rows using regex pattern
221
+ */
222
+ function createRegexMatchTool(adapter) {
223
+ return {
224
+ name: "sqlite_regex_match",
225
+ description: "Find rows where column matches a regex pattern. Processed in JavaScript.",
226
+ group: "text",
227
+ inputSchema: RegexMatchSchema,
228
+ outputSchema: RegexMatchOutputSchema,
229
+ requiredScopes: ["read"],
230
+ annotations: readOnly("Regex Match"),
231
+ handler: async (params, _context) => {
232
+ const input = RegexMatchSchema.parse(params);
233
+ // Validate and quote identifiers
234
+ const table = sanitizeIdentifier(input.table);
235
+ const column = sanitizeIdentifier(input.column);
236
+ let sql = `SELECT rowid as id, ${column} as value FROM ${table}`;
237
+ if (input.whereClause) {
238
+ validateWhereClause(input.whereClause);
239
+ sql += ` WHERE ${input.whereClause}`;
240
+ }
241
+ sql += ` LIMIT ${input.limit}`;
242
+ const result = await adapter.executeReadQuery(sql);
243
+ // Apply regex in JavaScript
244
+ const regex = new RegExp(input.pattern);
245
+ const matches = (result.rows ?? [])
246
+ .filter((row) => {
247
+ const rawValue = row["value"];
248
+ const value = typeof rawValue === "string"
249
+ ? rawValue
250
+ : JSON.stringify(rawValue ?? "");
251
+ return regex.test(value);
252
+ })
253
+ .map((row) => {
254
+ // Ensure rowid is a number for output schema compliance
255
+ const rawRowid = row["id"];
256
+ const rowid = typeof rawRowid === "number"
257
+ ? rawRowid
258
+ : typeof rawRowid === "string"
259
+ ? parseInt(rawRowid, 10) || 0
260
+ : 0;
261
+ return { ...row, rowid };
262
+ });
263
+ return {
264
+ success: true,
265
+ rowCount: matches.length,
266
+ matches,
267
+ };
268
+ },
269
+ };
270
+ }
271
+ /**
272
+ * Split text into array
273
+ */
274
+ function createTextSplitTool(adapter) {
275
+ return {
276
+ name: "sqlite_text_split",
277
+ description: "Split a text column by delimiter into array results.",
278
+ group: "text",
279
+ inputSchema: TextSplitSchema,
280
+ outputSchema: TextSplitOutputSchema,
281
+ requiredScopes: ["read"],
282
+ annotations: readOnly("Text Split"),
283
+ handler: async (params, _context) => {
284
+ const input = TextSplitSchema.parse(params);
285
+ // Validate and quote identifiers
286
+ const table = sanitizeIdentifier(input.table);
287
+ const column = sanitizeIdentifier(input.column);
288
+ let sql = `SELECT rowid as id, ${column} as value FROM ${table}`;
289
+ if (input.whereClause) {
290
+ validateWhereClause(input.whereClause);
291
+ sql += ` WHERE ${input.whereClause}`;
292
+ }
293
+ sql += ` LIMIT ${input.limit}`;
294
+ const result = await adapter.executeReadQuery(sql);
295
+ // Split in JavaScript - return per-row results for traceability
296
+ const rows = (result.rows ?? []).map((row) => {
297
+ const rawRowid = row["id"];
298
+ const rowid = typeof rawRowid === "number"
299
+ ? rawRowid
300
+ : typeof rawRowid === "string"
301
+ ? parseInt(rawRowid, 10) || 0
302
+ : 0;
303
+ const rawValue = row["value"];
304
+ const original = typeof rawValue === "string"
305
+ ? rawValue
306
+ : rawValue === null || rawValue === undefined
307
+ ? ""
308
+ : JSON.stringify(rawValue);
309
+ const parts = original.split(input.delimiter);
310
+ return {
311
+ rowid,
312
+ original: rawValue === null ? null : original,
313
+ parts,
314
+ };
315
+ });
316
+ return {
317
+ success: true,
318
+ rowCount: rows.length,
319
+ rows,
320
+ };
321
+ },
322
+ };
323
+ }
324
+ /**
325
+ * Concatenate columns
326
+ */
327
+ function createTextConcatTool(adapter) {
328
+ return {
329
+ name: "sqlite_text_concat",
330
+ description: "Concatenate multiple columns with optional separator.",
331
+ group: "text",
332
+ inputSchema: TextConcatSchema,
333
+ requiredScopes: ["read"],
334
+ annotations: readOnly("Text Concat"),
335
+ handler: async (params, _context) => {
336
+ const input = TextConcatSchema.parse(params);
337
+ // Validate and quote identifiers
338
+ const table = sanitizeIdentifier(input.table);
339
+ const quotedCols = input.columns.map((c) => sanitizeIdentifier(c));
340
+ // Build concatenation expression using || operator
341
+ const sep = input.separator.replace(/'/g, "''");
342
+ // Build: COALESCE(col1, '') || 'sep' || COALESCE(col2, '') || ...
343
+ const concatExpr = quotedCols
344
+ .map((c) => `COALESCE(${c}, '')`)
345
+ .join(` || '${sep}' || `);
346
+ let sql = `SELECT ${concatExpr} as concatenated FROM ${table}`;
347
+ if (input.whereClause) {
348
+ validateWhereClause(input.whereClause);
349
+ sql += ` WHERE ${input.whereClause}`;
350
+ }
351
+ sql += ` LIMIT ${input.limit}`;
352
+ const result = await adapter.executeReadQuery(sql);
353
+ return {
354
+ success: true,
355
+ rowCount: result.rows?.length ?? 0,
356
+ values: result.rows?.map((r) => r["concatenated"]),
357
+ };
358
+ },
359
+ };
360
+ }
361
+ /**
362
+ * Replace text in column
363
+ */
364
+ function createTextReplaceTool(adapter) {
365
+ return {
366
+ name: "sqlite_text_replace",
367
+ description: "Replace text in a column using SQLite replace() function.",
368
+ group: "text",
369
+ inputSchema: TextReplaceSchema,
370
+ outputSchema: RegexReplaceOutputSchema,
371
+ requiredScopes: ["write"],
372
+ annotations: write("Text Replace"),
373
+ handler: async (params, _context) => {
374
+ const input = TextReplaceSchema.parse(params);
375
+ // Validate and quote identifiers
376
+ const table = sanitizeIdentifier(input.table);
377
+ const column = sanitizeIdentifier(input.column);
378
+ const search = input.searchPattern.replace(/'/g, "''");
379
+ const replace = input.replaceWith.replace(/'/g, "''");
380
+ validateWhereClause(input.whereClause);
381
+ const sql = `UPDATE ${table} SET ${column} = replace(${column}, '${search}', '${replace}') WHERE ${input.whereClause}`;
382
+ const result = await adapter.executeWriteQuery(sql);
383
+ return {
384
+ success: true,
385
+ rowsAffected: result.rowsAffected,
386
+ };
387
+ },
388
+ };
389
+ }
390
+ /**
391
+ * Trim whitespace
392
+ */
393
+ function createTextTrimTool(adapter) {
394
+ return {
395
+ name: "sqlite_text_trim",
396
+ description: "Trim whitespace from text column values.",
397
+ group: "text",
398
+ inputSchema: TextTrimSchema,
399
+ requiredScopes: ["read"],
400
+ annotations: readOnly("Text Trim"),
401
+ handler: async (params, _context) => {
402
+ const input = TextTrimSchema.parse(params);
403
+ // Validate and quote identifiers
404
+ const table = sanitizeIdentifier(input.table);
405
+ const column = sanitizeIdentifier(input.column);
406
+ let trimFunc;
407
+ switch (input.mode) {
408
+ case "left":
409
+ trimFunc = "ltrim";
410
+ break;
411
+ case "right":
412
+ trimFunc = "rtrim";
413
+ break;
414
+ default:
415
+ trimFunc = "trim";
416
+ }
417
+ let sql = `SELECT rowid, ${column} as original, ${trimFunc}(${column}) as trimmed FROM ${table}`;
418
+ if (input.whereClause) {
419
+ validateWhereClause(input.whereClause);
420
+ sql += ` WHERE ${input.whereClause}`;
421
+ }
422
+ sql += ` LIMIT ${input.limit}`;
423
+ const result = await adapter.executeReadQuery(sql);
424
+ return {
425
+ success: true,
426
+ rowCount: result.rows?.length ?? 0,
427
+ results: result.rows,
428
+ };
429
+ },
430
+ };
431
+ }
432
+ /**
433
+ * Change text case
434
+ */
435
+ function createTextCaseTool(adapter) {
436
+ return {
437
+ name: "sqlite_text_case",
438
+ description: "Convert text to uppercase or lowercase.",
439
+ group: "text",
440
+ inputSchema: TextCaseSchema,
441
+ requiredScopes: ["read"],
442
+ annotations: readOnly("Text Case"),
443
+ handler: async (params, _context) => {
444
+ const input = TextCaseSchema.parse(params);
445
+ // Validate and quote identifiers
446
+ const table = sanitizeIdentifier(input.table);
447
+ const column = sanitizeIdentifier(input.column);
448
+ const caseFunc = input.mode === "upper" ? "upper" : "lower";
449
+ let sql = `SELECT rowid, ${column} as original, ${caseFunc}(${column}) as transformed FROM ${table}`;
450
+ if (input.whereClause) {
451
+ validateWhereClause(input.whereClause);
452
+ sql += ` WHERE ${input.whereClause}`;
453
+ }
454
+ sql += ` LIMIT ${input.limit}`;
455
+ const result = await adapter.executeReadQuery(sql);
456
+ return {
457
+ success: true,
458
+ rowCount: result.rows?.length ?? 0,
459
+ results: result.rows,
460
+ };
461
+ },
462
+ };
463
+ }
464
+ /**
465
+ * Extract substring
466
+ */
467
+ function createTextSubstringTool(adapter) {
468
+ return {
469
+ name: "sqlite_text_substring",
470
+ description: "Extract a substring from text column using substr().",
471
+ group: "text",
472
+ inputSchema: TextSubstringSchema,
473
+ requiredScopes: ["read"],
474
+ annotations: readOnly("Text Substring"),
475
+ handler: async (params, _context) => {
476
+ const input = TextSubstringSchema.parse(params);
477
+ // Validate and quote identifiers
478
+ const table = sanitizeIdentifier(input.table);
479
+ const column = sanitizeIdentifier(input.column);
480
+ const substrExpr = input.length !== undefined
481
+ ? `substr(${column}, ${input.start}, ${input.length})`
482
+ : `substr(${column}, ${input.start})`;
483
+ let sql = `SELECT rowid, ${column} as original, ${substrExpr} as substring FROM ${table}`;
484
+ if (input.whereClause) {
485
+ validateWhereClause(input.whereClause);
486
+ sql += ` WHERE ${input.whereClause}`;
487
+ }
488
+ sql += ` LIMIT ${input.limit}`;
489
+ const result = await adapter.executeReadQuery(sql);
490
+ return {
491
+ success: true,
492
+ rowCount: result.rows?.length ?? 0,
493
+ results: result.rows,
494
+ };
495
+ },
496
+ };
497
+ }
498
+ // =============================================================================
499
+ // New Text Tools: Fuzzy, Phonetic, Normalize, Validate
500
+ // =============================================================================
501
+ /**
502
+ * Calculate Levenshtein distance between two strings
503
+ */
504
+ function levenshtein(a, b) {
505
+ const aLower = a.toLowerCase();
506
+ const bLower = b.toLowerCase();
507
+ // Handle edge cases
508
+ if (aLower === bLower)
509
+ return 0;
510
+ if (aLower.length === 0)
511
+ return bLower.length;
512
+ if (bLower.length === 0)
513
+ return aLower.length;
514
+ // Create matrix with proper initialization
515
+ const rows = bLower.length + 1;
516
+ const cols = aLower.length + 1;
517
+ const matrix = [];
518
+ for (let i = 0; i < rows; i++) {
519
+ matrix.push(new Array(cols).fill(0));
520
+ }
521
+ // Initialize first column and row
522
+ for (let i = 0; i < rows; i++) {
523
+ const row = matrix[i];
524
+ if (row)
525
+ row[0] = i;
526
+ }
527
+ for (let j = 0; j < cols; j++) {
528
+ const firstRow = matrix[0];
529
+ if (firstRow)
530
+ firstRow[j] = j;
531
+ }
532
+ // Fill in the rest of the matrix
533
+ for (let i = 1; i < rows; i++) {
534
+ for (let j = 1; j < cols; j++) {
535
+ const currentRow = matrix[i];
536
+ const prevRow = matrix[i - 1];
537
+ if (!currentRow || !prevRow)
538
+ continue;
539
+ const cost = bLower[i - 1] === aLower[j - 1] ? 0 : 1;
540
+ const del = (prevRow[j] ?? 0) + 1;
541
+ const ins = (currentRow[j - 1] ?? 0) + 1;
542
+ const sub = (prevRow[j - 1] ?? 0) + cost;
543
+ currentRow[j] = Math.min(del, ins, sub);
544
+ }
545
+ }
546
+ const lastRow = matrix[bLower.length];
547
+ return lastRow?.[aLower.length] ?? 0;
548
+ }
549
+ /**
550
+ * Simple Metaphone implementation
551
+ */
552
+ function metaphone(word) {
553
+ const vowels = "AEIOU";
554
+ let result = "";
555
+ const w = word.toUpperCase().replace(/[^A-Z]/g, "");
556
+ for (let i = 0; i < w.length && result.length < 4; i++) {
557
+ const c = w.charAt(i);
558
+ const prev = i > 0 ? w.charAt(i - 1) : "";
559
+ const next = i < w.length - 1 ? w.charAt(i + 1) : "";
560
+ // Skip duplicate adjacent letters
561
+ if (c === prev && c !== "C")
562
+ continue;
563
+ // Skip vowels except at start
564
+ if (vowels.includes(c) && i > 0)
565
+ continue;
566
+ // Consonant rules (simplified)
567
+ if (c === "B" && i === w.length - 1 && prev === "M")
568
+ continue;
569
+ if (c === "C") {
570
+ if (next === "H") {
571
+ result += "X";
572
+ i++;
573
+ }
574
+ else if ("IEY".includes(next))
575
+ result += "S";
576
+ else
577
+ result += "K";
578
+ }
579
+ else if (c === "D") {
580
+ const afterNext = w.charAt(i + 2);
581
+ if (next === "G" && "IEY".includes(afterNext)) {
582
+ result += "J";
583
+ i++;
584
+ }
585
+ else
586
+ result += "T";
587
+ }
588
+ else if (c === "G") {
589
+ if (next === "H")
590
+ continue;
591
+ if ("IEY".includes(next))
592
+ result += "J";
593
+ else
594
+ result += "K";
595
+ }
596
+ else if (c === "K" && prev === "C")
597
+ continue;
598
+ else if (c === "P" && next === "H") {
599
+ result += "F";
600
+ i++;
601
+ }
602
+ else if (c === "Q")
603
+ result += "K";
604
+ else if (c === "S" && next === "H") {
605
+ result += "X";
606
+ i++;
607
+ }
608
+ else if (c === "T" && next === "H") {
609
+ result += "0";
610
+ i++;
611
+ }
612
+ else if (c === "W" && vowels.includes(next))
613
+ result += "W";
614
+ else if (c === "X")
615
+ result += "KS";
616
+ else if (c === "Z")
617
+ result += "S";
618
+ else if (!"HW".includes(c))
619
+ result += c;
620
+ }
621
+ return result;
622
+ }
623
+ /**
624
+ * Strip accents from text
625
+ */
626
+ function stripAccents(text) {
627
+ return text.normalize("NFD").replace(/[\u0300-\u036f]/g, "");
628
+ }
629
+ /**
630
+ * Simple Soundex implementation for phonetic matching
631
+ */
632
+ function soundex(word) {
633
+ if (!word)
634
+ return "0000";
635
+ const upper = word.toUpperCase().replace(/[^A-Z]/g, "");
636
+ if (!upper)
637
+ return "0000";
638
+ const first = upper[0] ?? "0";
639
+ const mapping = {
640
+ B: "1",
641
+ F: "1",
642
+ P: "1",
643
+ V: "1",
644
+ C: "2",
645
+ G: "2",
646
+ J: "2",
647
+ K: "2",
648
+ Q: "2",
649
+ S: "2",
650
+ X: "2",
651
+ Z: "2",
652
+ D: "3",
653
+ T: "3",
654
+ L: "4",
655
+ M: "5",
656
+ N: "5",
657
+ R: "6",
658
+ };
659
+ let result = first;
660
+ for (let i = 1; i < upper.length && result.length < 4; i++) {
661
+ const char = upper[i];
662
+ if (char && mapping[char]) {
663
+ const code = mapping[char];
664
+ if (code && !result.endsWith(code)) {
665
+ result += code;
666
+ }
667
+ }
668
+ }
669
+ return (result + "000").slice(0, 4);
670
+ }
671
+ /**
672
+ * Validation patterns
673
+ */
674
+ const VALIDATION_PATTERNS = {
675
+ email: /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/,
676
+ phone: /^[+]?[(]?[0-9]{1,4}[)]?[-\s./0-9]{7,}$/,
677
+ url: /^https?:\/\/[^\s/$.?#].[^\s]*$/i,
678
+ uuid: /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i,
679
+ ipv4: /^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/,
680
+ };
681
+ /**
682
+ * Fuzzy match using Levenshtein distance
683
+ */
684
+ function createFuzzyMatchTool(adapter) {
685
+ return {
686
+ name: "sqlite_fuzzy_match",
687
+ description: "Find fuzzy matches using Levenshtein distance. By default, splits values into tokens and matches against each word (use tokenize:false to match entire value). Use maxDistance 1-3 for similar-length strings.",
688
+ group: "text",
689
+ inputSchema: FuzzyMatchSchema,
690
+ outputSchema: z.object({
691
+ success: z.boolean(),
692
+ matchCount: z.number(),
693
+ tokenized: z.boolean(),
694
+ matches: z.array(z.object({
695
+ value: z.string(),
696
+ matchedToken: z.string().optional(),
697
+ tokenDistance: z.number().optional(),
698
+ distance: z.number(),
699
+ })),
700
+ }),
701
+ requiredScopes: ["read"],
702
+ annotations: readOnly("Fuzzy Match"),
703
+ handler: async (params, _context) => {
704
+ const input = FuzzyMatchSchema.parse(params);
705
+ // Validate and quote identifiers
706
+ const table = sanitizeIdentifier(input.table);
707
+ const column = sanitizeIdentifier(input.column);
708
+ const sql = `SELECT ${column} FROM ${table} WHERE ${column} IS NOT NULL LIMIT 1000`;
709
+ const result = await adapter.executeReadQuery(sql);
710
+ const matches = [];
711
+ for (const row of result.rows ?? []) {
712
+ const rawValue = row[input.column];
713
+ const value = typeof rawValue === "string"
714
+ ? rawValue
715
+ : JSON.stringify(rawValue ?? "");
716
+ if (input.tokenize) {
717
+ // Token-based matching: split into words and find best match
718
+ const tokens = value.split(/\s+/).filter((t) => t.length > 0);
719
+ let bestToken = "";
720
+ let bestDistance = Infinity;
721
+ for (const token of tokens) {
722
+ const dist = levenshtein(input.search, token);
723
+ if (dist < bestDistance) {
724
+ bestDistance = dist;
725
+ bestToken = token;
726
+ }
727
+ }
728
+ if (bestDistance <= input.maxDistance) {
729
+ matches.push({
730
+ value,
731
+ matchedToken: bestToken,
732
+ tokenDistance: bestDistance,
733
+ distance: bestDistance,
734
+ });
735
+ }
736
+ }
737
+ else {
738
+ // Legacy behavior: match against entire column value
739
+ const distance = levenshtein(input.search, value);
740
+ if (distance <= input.maxDistance) {
741
+ matches.push({ value, distance });
742
+ }
743
+ }
744
+ }
745
+ // Sort by distance (ascending) and limit
746
+ matches.sort((a, b) => a.distance - b.distance);
747
+ const limited = matches.slice(0, input.limit);
748
+ return {
749
+ success: true,
750
+ matchCount: limited.length,
751
+ tokenized: input.tokenize,
752
+ matches: limited,
753
+ };
754
+ },
755
+ };
756
+ }
757
+ /**
758
+ * Phonetic match using Soundex or Metaphone
759
+ */
760
+ function createPhoneticMatchTool(adapter) {
761
+ return {
762
+ name: "sqlite_phonetic_match",
763
+ description: "Find phonetically similar values using Soundex (SQLite native) or Metaphone algorithm.",
764
+ group: "text",
765
+ inputSchema: PhoneticMatchSchema,
766
+ outputSchema: z.object({
767
+ success: z.boolean(),
768
+ searchCode: z.string(),
769
+ matchCount: z.number(),
770
+ matches: z.array(z.object({
771
+ value: z.string(),
772
+ phoneticCode: z.string(),
773
+ row: z.record(z.string(), z.unknown()).optional(),
774
+ })),
775
+ }),
776
+ requiredScopes: ["read"],
777
+ annotations: readOnly("Phonetic Match"),
778
+ handler: async (params, _context) => {
779
+ const input = PhoneticMatchSchema.parse(params);
780
+ // Validate and quote identifiers
781
+ const table = sanitizeIdentifier(input.table);
782
+ const column = sanitizeIdentifier(input.column);
783
+ const searchCode = input.algorithm === "metaphone"
784
+ ? metaphone(input.search)
785
+ : soundex(input.search); // Compute locally to ensure it's always available
786
+ let sql;
787
+ if (input.algorithm === "soundex") {
788
+ // Try SQLite's native soundex function first, fall back to JS implementation
789
+ sql = `SELECT *, soundex(${column}) as _phonetic FROM ${table} WHERE soundex(${column}) = soundex('${input.search.replace(/'/g, "''")}') LIMIT ${input.limit}`;
790
+ try {
791
+ const result = await adapter.executeReadQuery(sql);
792
+ const matches = (result.rows ?? []).map((row) => {
793
+ const rawValue = row[input.column];
794
+ const rawPhonetic = row["_phonetic"];
795
+ const match = {
796
+ value: typeof rawValue === "string"
797
+ ? rawValue
798
+ : JSON.stringify(rawValue ?? ""),
799
+ phoneticCode: typeof rawPhonetic === "string" ? rawPhonetic : "",
800
+ };
801
+ if (input.includeRowData) {
802
+ match.row = row;
803
+ }
804
+ return match;
805
+ });
806
+ return {
807
+ success: true,
808
+ searchCode, // Use pre-computed local soundex code
809
+ matchCount: matches.length,
810
+ matches,
811
+ };
812
+ }
813
+ catch (error) {
814
+ // If SQLite soundex() is unavailable (WASM mode), fall back to JS implementation
815
+ if (error instanceof Error &&
816
+ error.message.toLowerCase().includes("no such function: soundex")) {
817
+ // Fall through to JS-based soundex matching below
818
+ sql = `SELECT * FROM ${table} WHERE ${column} IS NOT NULL LIMIT 1000`;
819
+ const result = await adapter.executeReadQuery(sql);
820
+ const matches = [];
821
+ for (const row of result.rows ?? []) {
822
+ const rawValue = row[input.column];
823
+ const value = typeof rawValue === "string"
824
+ ? rawValue
825
+ : JSON.stringify(rawValue ?? "");
826
+ const code = soundex(value); // Use JS-based soundex
827
+ if (code === searchCode) {
828
+ const match = { value, phoneticCode: code };
829
+ if (input.includeRowData) {
830
+ match.row = row;
831
+ }
832
+ matches.push(match);
833
+ }
834
+ }
835
+ return {
836
+ success: true,
837
+ searchCode,
838
+ matchCount: matches.length,
839
+ matches: matches.slice(0, input.limit),
840
+ };
841
+ }
842
+ throw error;
843
+ }
844
+ }
845
+ else {
846
+ // Metaphone in JS
847
+ sql = `SELECT * FROM ${table} WHERE ${column} IS NOT NULL LIMIT 1000`;
848
+ const result = await adapter.executeReadQuery(sql);
849
+ const matches = [];
850
+ for (const row of result.rows ?? []) {
851
+ const rawValue = row[input.column];
852
+ const value = typeof rawValue === "string"
853
+ ? rawValue
854
+ : JSON.stringify(rawValue ?? "");
855
+ const code = metaphone(value);
856
+ if (code === searchCode) {
857
+ const match = { value, phoneticCode: code };
858
+ if (input.includeRowData) {
859
+ match.row = row;
860
+ }
861
+ matches.push(match);
862
+ }
863
+ }
864
+ return {
865
+ success: true,
866
+ searchCode,
867
+ matchCount: matches.length,
868
+ matches: matches.slice(0, input.limit),
869
+ };
870
+ }
871
+ },
872
+ };
873
+ }
874
+ /**
875
+ * Normalize text (Unicode normalization or accent stripping)
876
+ */
877
+ function createTextNormalizeTool(adapter) {
878
+ return {
879
+ name: "sqlite_text_normalize",
880
+ description: "Normalize text using Unicode normalization (NFC, NFD, NFKC, NFKD) or strip accents.",
881
+ group: "text",
882
+ inputSchema: TextNormalizeSchema,
883
+ outputSchema: z.object({
884
+ success: z.boolean(),
885
+ rowCount: z.number(),
886
+ rows: z.array(z.object({
887
+ original: z.string(),
888
+ normalized: z.string(),
889
+ })),
890
+ }),
891
+ requiredScopes: ["read"],
892
+ annotations: readOnly("Text Normalize"),
893
+ handler: async (params, _context) => {
894
+ const input = TextNormalizeSchema.parse(params);
895
+ // Validate and quote identifiers
896
+ const table = sanitizeIdentifier(input.table);
897
+ const column = sanitizeIdentifier(input.column);
898
+ let sql = `SELECT ${column} as original FROM ${table}`;
899
+ if (input.whereClause) {
900
+ validateWhereClause(input.whereClause);
901
+ sql += ` WHERE ${input.whereClause}`;
902
+ }
903
+ sql += ` LIMIT ${input.limit}`;
904
+ const result = await adapter.executeReadQuery(sql);
905
+ const rows = (result.rows ?? []).map((row) => {
906
+ const rawOriginal = row["original"];
907
+ const original = typeof rawOriginal === "string"
908
+ ? rawOriginal
909
+ : JSON.stringify(rawOriginal ?? "");
910
+ let normalized;
911
+ if (input.mode === "strip_accents") {
912
+ normalized = stripAccents(original);
913
+ }
914
+ else {
915
+ normalized = original.normalize(input.mode.toUpperCase());
916
+ }
917
+ return { original, normalized };
918
+ });
919
+ return {
920
+ success: true,
921
+ rowCount: rows.length,
922
+ rows,
923
+ };
924
+ },
925
+ };
926
+ }
927
+ /**
928
+ * Validate text against common patterns
929
+ */
930
+ function createTextValidateTool(adapter) {
931
+ return {
932
+ name: "sqlite_text_validate",
933
+ description: "Validate text values against patterns: email, phone, URL, UUID, IPv4, or custom regex.",
934
+ group: "text",
935
+ inputSchema: TextValidateSchema,
936
+ outputSchema: z.object({
937
+ success: z.boolean(),
938
+ totalRows: z.number(),
939
+ validCount: z.number(),
940
+ invalidCount: z.number(),
941
+ invalidRows: z.array(z.object({
942
+ value: z.string().nullable(),
943
+ rowid: z.number().optional(),
944
+ })),
945
+ }),
946
+ requiredScopes: ["read"],
947
+ annotations: readOnly("Text Validate"),
948
+ handler: async (params, _context) => {
949
+ const input = TextValidateSchema.parse(params);
950
+ // Validate and quote identifiers
951
+ const table = sanitizeIdentifier(input.table);
952
+ const column = sanitizeIdentifier(input.column);
953
+ // Get validation pattern
954
+ let pattern;
955
+ if (input.pattern === "custom") {
956
+ if (!input.customPattern) {
957
+ throw new Error("customPattern is required when pattern='custom'");
958
+ }
959
+ // Normalize pattern: handle common JSON double-escaping issues
960
+ // e.g., "\\." in JSON becomes "\." in JavaScript string, but some clients
961
+ // may send "\\\\." which becomes "\\" - normalize excessive backslashes
962
+ const normalizedPattern = input.customPattern.replace(/\\\\/g, "\\");
963
+ try {
964
+ pattern = new RegExp(normalizedPattern);
965
+ }
966
+ catch {
967
+ throw new Error(`Invalid regex pattern: ${input.customPattern} (normalized to: ${normalizedPattern})`);
968
+ }
969
+ }
970
+ else {
971
+ const foundPattern = VALIDATION_PATTERNS[input.pattern];
972
+ if (!foundPattern) {
973
+ throw new Error(`Unknown pattern: ${input.pattern}`);
974
+ }
975
+ pattern = foundPattern;
976
+ }
977
+ let sql = `SELECT rowid, ${column} as value FROM ${table}`;
978
+ if (input.whereClause) {
979
+ validateWhereClause(input.whereClause);
980
+ sql += ` WHERE ${input.whereClause}`;
981
+ }
982
+ sql += ` LIMIT ${input.limit}`;
983
+ const result = await adapter.executeReadQuery(sql);
984
+ const invalidRows = [];
985
+ let validCount = 0;
986
+ for (const row of result.rows ?? []) {
987
+ const rawValue = row["value"];
988
+ // Handle null/empty values with user-friendly display
989
+ const value = rawValue === null || rawValue === undefined
990
+ ? ""
991
+ : typeof rawValue === "string"
992
+ ? rawValue
993
+ : JSON.stringify(rawValue);
994
+ // Display actual value: null for null/undefined, otherwise the value (truncated if long)
995
+ const displayValue = rawValue === null || rawValue === undefined
996
+ ? null
997
+ : value.length > 100
998
+ ? value.slice(0, 100) + "..."
999
+ : value;
1000
+ if (pattern.test(value)) {
1001
+ validCount++;
1002
+ }
1003
+ else {
1004
+ const rowid = row["rowid"];
1005
+ if (typeof rowid === "number") {
1006
+ invalidRows.push({ value: displayValue, rowid });
1007
+ }
1008
+ else {
1009
+ invalidRows.push({ value: displayValue });
1010
+ }
1011
+ }
1012
+ }
1013
+ return {
1014
+ success: true,
1015
+ totalRows: result.rows?.length ?? 0,
1016
+ validCount,
1017
+ invalidCount: invalidRows.length,
1018
+ invalidRows,
1019
+ };
1020
+ },
1021
+ };
1022
+ }
1023
+ /**
1024
+ * Output schema for advanced search
1025
+ */
1026
+ const AdvancedSearchOutputSchema = z.object({
1027
+ success: z.boolean(),
1028
+ searchTerm: z.string(),
1029
+ techniques: z.array(z.string()),
1030
+ matchCount: z.number(),
1031
+ matches: z.array(z.object({
1032
+ rowid: z.number(),
1033
+ text: z.string(),
1034
+ matchTypes: z.array(z.string()),
1035
+ bestScore: z.number(),
1036
+ bestType: z.string(),
1037
+ })),
1038
+ });
1039
+ /**
1040
+ * Advanced search combining multiple text processing techniques
1041
+ */
1042
+ function createAdvancedSearchTool(adapter) {
1043
+ return {
1044
+ name: "sqlite_advanced_search",
1045
+ description: "Advanced search combining exact, fuzzy (Levenshtein), and phonetic (Soundex) matching",
1046
+ group: "text",
1047
+ inputSchema: AdvancedSearchSchema,
1048
+ outputSchema: AdvancedSearchOutputSchema,
1049
+ requiredScopes: ["read"],
1050
+ annotations: readOnly("Advanced Search"),
1051
+ handler: async (params, _context) => {
1052
+ const input = AdvancedSearchSchema.parse(params);
1053
+ // Validate and quote identifiers
1054
+ const table = sanitizeIdentifier(input.table);
1055
+ const column = sanitizeIdentifier(input.column);
1056
+ // Fetch candidate rows
1057
+ let whereClause = "";
1058
+ if (input.whereClause) {
1059
+ validateWhereClause(input.whereClause);
1060
+ whereClause = ` AND ${input.whereClause}`;
1061
+ }
1062
+ const query = `SELECT rowid as id, ${column} AS value FROM ${table} WHERE ${column} IS NOT NULL${whereClause} LIMIT 1000`;
1063
+ const result = await adapter.executeQuery(query);
1064
+ if (!result.rows || result.rows.length === 0) {
1065
+ return {
1066
+ success: true,
1067
+ searchTerm: input.searchTerm,
1068
+ techniques: input.techniques,
1069
+ matchCount: 0,
1070
+ matches: [],
1071
+ };
1072
+ }
1073
+ const searchLower = input.searchTerm.toLowerCase();
1074
+ const searchSoundex = soundex(input.searchTerm);
1075
+ const allMatches = [];
1076
+ for (const row of result.rows) {
1077
+ const rawValue = row["value"];
1078
+ const text = typeof rawValue === "string"
1079
+ ? rawValue
1080
+ : typeof rawValue === "number"
1081
+ ? String(rawValue)
1082
+ : "";
1083
+ const textLower = text.toLowerCase();
1084
+ const matches = [];
1085
+ // Exact match (case-insensitive substring)
1086
+ if (input.techniques.includes("exact")) {
1087
+ if (textLower.includes(searchLower)) {
1088
+ matches.push({ type: "exact", score: 1.0 });
1089
+ }
1090
+ }
1091
+ // Fuzzy match (Levenshtein ratio)
1092
+ if (input.techniques.includes("fuzzy")) {
1093
+ const distance = levenshtein(input.searchTerm, text);
1094
+ const maxLen = Math.max(input.searchTerm.length, text.length);
1095
+ const similarity = maxLen === 0 ? 1 : 1 - distance / maxLen;
1096
+ if (similarity >= input.fuzzyThreshold) {
1097
+ matches.push({ type: "fuzzy", score: similarity });
1098
+ }
1099
+ }
1100
+ // Phonetic match (Soundex)
1101
+ if (input.techniques.includes("phonetic")) {
1102
+ const words = text.split(/\s+/);
1103
+ for (const word of words) {
1104
+ if (soundex(word) === searchSoundex) {
1105
+ matches.push({ type: "phonetic", score: 0.8 });
1106
+ break;
1107
+ }
1108
+ }
1109
+ }
1110
+ if (matches.length > 0) {
1111
+ const best = matches.reduce((a, b) => (a.score > b.score ? a : b));
1112
+ // Safely coerce rowid to number, defaulting to 0 if undefined/null
1113
+ const rawRowid = row["id"];
1114
+ const rowid = typeof rawRowid === "number"
1115
+ ? rawRowid
1116
+ : typeof rawRowid === "string"
1117
+ ? parseInt(rawRowid, 10) || 0
1118
+ : 0;
1119
+ allMatches.push({
1120
+ rowid,
1121
+ text: text.length > 100 ? text.slice(0, 100) + "..." : text,
1122
+ matchTypes: matches.map((m) => m.type),
1123
+ bestScore: Math.round(best.score * 1000) / 1000,
1124
+ bestType: best.type,
1125
+ });
1126
+ }
1127
+ }
1128
+ // Sort by score and limit
1129
+ allMatches.sort((a, b) => b.bestScore - a.bestScore);
1130
+ const limited = allMatches.slice(0, input.limit);
1131
+ return {
1132
+ success: true,
1133
+ searchTerm: input.searchTerm,
1134
+ techniques: input.techniques,
1135
+ matchCount: limited.length,
1136
+ matches: limited,
1137
+ };
1138
+ },
1139
+ };
1140
+ }
1141
+ //# sourceMappingURL=text.js.map