@influxdata/influxdb3-mcp-server 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/CHANGELOG.md +194 -0
  2. package/Dockerfile +22 -0
  3. package/LICENSE +6 -0
  4. package/LICENSE-APACHE.txt +201 -0
  5. package/LICENSE-MIT.txt +25 -0
  6. package/README.md +318 -0
  7. package/build/config.js +85 -0
  8. package/build/helpers/enums/influx-product-types.enum.js +8 -0
  9. package/build/index.js +33 -0
  10. package/build/prompts/index.js +98 -0
  11. package/build/resources/index.js +223 -0
  12. package/build/server/index.js +104 -0
  13. package/build/services/base-connection.service.js +318 -0
  14. package/build/services/cloud-token-management.service.js +179 -0
  15. package/build/services/context-file.service.js +97 -0
  16. package/build/services/database-management.service.js +549 -0
  17. package/build/services/help.service.js +241 -0
  18. package/build/services/http-client.service.js +109 -0
  19. package/build/services/influxdb-master.service.js +117 -0
  20. package/build/services/query.service.js +499 -0
  21. package/build/services/serverless-schema-management.service.js +266 -0
  22. package/build/services/token-management.service.js +215 -0
  23. package/build/services/write.service.js +153 -0
  24. package/build/tools/categories/cloud-token.tools.js +321 -0
  25. package/build/tools/categories/database.tools.js +299 -0
  26. package/build/tools/categories/health.tools.js +75 -0
  27. package/build/tools/categories/help.tools.js +104 -0
  28. package/build/tools/categories/query.tools.js +180 -0
  29. package/build/tools/categories/schema.tools.js +308 -0
  30. package/build/tools/categories/token.tools.js +378 -0
  31. package/build/tools/categories/write.tools.js +104 -0
  32. package/build/tools/index.js +27 -0
  33. package/env.example +17 -0
  34. package/example-cloud-dedicated.mcp.json +15 -0
  35. package/example-cloud-serverless.mcp.json +13 -0
  36. package/example-clustered.mcp.json +14 -0
  37. package/example-docker.mcp.json +25 -0
  38. package/example-local.mcp.json +13 -0
  39. package/example-npx.mcp.json +13 -0
  40. package/package.json +78 -0
@@ -0,0 +1,499 @@
1
+ /**
2
+ * InfluxDB Query Service
3
+ *
4
+ * Handles query operations using InfluxDB v3 SQL API
5
+ */
6
+ import { InfluxProductType } from "../helpers/enums/influx-product-types.enum.js";
7
+ export class QueryService {
8
+ baseService;
9
+ constructor(baseService) {
10
+ this.baseService = baseService;
11
+ }
12
+ /**
13
+ * Execute SQL query (single entrypoint for all product types)
14
+ * For core/enterprise: HTTP API
15
+ * For cloud-dedicated: influxdb3 client
16
+ * For clustered: HTTP API (/query)
17
+ */
18
+ async executeQuery(query, database, options = {}) {
19
+ this.baseService.validateDataCapabilities();
20
+ const format = options.format ?? "json";
21
+ const connectionInfo = this.baseService.getConnectionInfo();
22
+ switch (connectionInfo.type) {
23
+ case InfluxProductType.CloudDedicated:
24
+ return this.executeCloudDedicatedQuery(query, database);
25
+ case InfluxProductType.Clustered:
26
+ return this.executeClusteredQuery(query, database);
27
+ case InfluxProductType.CloudServerless:
28
+ return this.executeCloudServerlessQuery(query, database);
29
+ case InfluxProductType.Core:
30
+ case InfluxProductType.Enterprise:
31
+ return this.executeCoreEnterpriseQuery(query, database, format);
32
+ default:
33
+ throw new Error(`Unsupported InfluxDB product type: ${connectionInfo.type}`);
34
+ }
35
+ }
36
+ /**
37
+ * Query for core/enterprise (HTTP API)
38
+ */
39
+ async executeCoreEnterpriseQuery(query, database, format) {
40
+ try {
41
+ const httpClient = this.baseService.getInfluxHttpClient();
42
+ const payload = {
43
+ db: database,
44
+ q: query,
45
+ format: format,
46
+ };
47
+ let acceptHeader = "application/json";
48
+ switch (format) {
49
+ case "json":
50
+ acceptHeader = "application/json";
51
+ break;
52
+ case "csv":
53
+ acceptHeader = "text/csv";
54
+ break;
55
+ case "parquet":
56
+ acceptHeader = "application/vnd.apache.parquet";
57
+ break;
58
+ case "jsonl":
59
+ acceptHeader = "application/json";
60
+ break;
61
+ case "pretty":
62
+ acceptHeader = "application/json";
63
+ break;
64
+ }
65
+ const response = await httpClient.post("/api/v3/query_sql", payload, {
66
+ headers: {
67
+ "Content-Type": "application/json",
68
+ Accept: acceptHeader,
69
+ },
70
+ });
71
+ return response;
72
+ }
73
+ catch (error) {
74
+ this.handleQueryError(error);
75
+ }
76
+ }
77
+ /**
78
+ * Query for cloud-dedicated/clustered (influxdb3 client)
79
+ */
80
+ async executeCloudDedicatedQuery(query, database) {
81
+ try {
82
+ const client = this.baseService.getClient();
83
+ if (!client)
84
+ throw new Error("InfluxDB client not initialized");
85
+ const result = client.queryPoints(query, database, { type: "sql" });
86
+ const rows = [];
87
+ for await (const row of result) {
88
+ rows.push(row);
89
+ }
90
+ return rows;
91
+ }
92
+ catch (error) {
93
+ this.handleQueryError(error);
94
+ }
95
+ }
96
+ /**
97
+ * Query for clustered (HTTP API)
98
+ */
99
+ async executeClusteredQuery(query, database) {
100
+ try {
101
+ const httpClient = this.baseService.getInfluxHttpClient();
102
+ const response = await httpClient.get("/query", {
103
+ params: {
104
+ db: database,
105
+ q: query,
106
+ },
107
+ });
108
+ return response;
109
+ }
110
+ catch (error) {
111
+ this.handleQueryError(error);
112
+ }
113
+ }
114
+ /**
115
+ * Query for cloud-serverless (influxdb3 client)
116
+ */
117
+ async executeCloudServerlessQuery(query, database) {
118
+ try {
119
+ const client = this.baseService.getClient();
120
+ if (!client)
121
+ throw new Error("InfluxDB client not initialized");
122
+ const result = client.queryPoints(query, database, { type: "sql" });
123
+ const rows = [];
124
+ for await (const row of result) {
125
+ rows.push(row);
126
+ }
127
+ return rows;
128
+ }
129
+ catch (error) {
130
+ this.handleQueryError(error);
131
+ }
132
+ }
133
+ /**
134
+ * Centralized error handler for query methods
135
+ */
136
+ handleQueryError(error) {
137
+ const errorMessage = error.response?.data?.message ||
138
+ error.response?.data?.error ||
139
+ (typeof error.response?.data === "string" ? error.response.data : null) ||
140
+ error.response?.statusText ||
141
+ error.message;
142
+ const statusCode = error.response?.status;
143
+ console.error(`Status: ${statusCode} \n Message: ${errorMessage}`);
144
+ switch (statusCode) {
145
+ case 400:
146
+ throw new Error(`Bad request: ${errorMessage}`);
147
+ case 401:
148
+ throw new Error(`Unauthorized: ${errorMessage}`);
149
+ case 403:
150
+ throw new Error(`Access denied: ${errorMessage}`);
151
+ case 404:
152
+ throw new Error(`Database not found: ${errorMessage}`);
153
+ case 405:
154
+ throw new Error(`Method not allowed: ${errorMessage}`);
155
+ case 422:
156
+ throw new Error(`Unprocessable entity: ${errorMessage}`);
157
+ default:
158
+ throw new Error(`Query failed: ${errorMessage}`);
159
+ }
160
+ }
161
+ /**
162
+ * Get all measurements/tables in a database
163
+ * Uses SHOW MEASUREMENTS for cloud-dedicated/clustered (HTTP), information_schema for others
164
+ */
165
+ async getMeasurements(database) {
166
+ this.baseService.validateDataCapabilities();
167
+ const connectionInfo = this.baseService.getConnectionInfo();
168
+ switch (connectionInfo.type) {
169
+ case InfluxProductType.CloudDedicated:
170
+ return this.getMeasurementsCloudDedicated(database);
171
+ case InfluxProductType.Clustered:
172
+ return this.getMeasurementsClustered(database);
173
+ case InfluxProductType.CloudServerless:
174
+ return this.getMeasurementsCloudServerless(database);
175
+ case InfluxProductType.Core:
176
+ case InfluxProductType.Enterprise:
177
+ return this.getMeasurementsCoreEnterprise(database);
178
+ default:
179
+ throw new Error(`Unsupported InfluxDB product type: ${connectionInfo.type}`);
180
+ }
181
+ }
182
+ /**
183
+ * Get measurements for cloud-dedicated/clustered (HTTP client with InfluxQL)
184
+ */
185
+ async getMeasurementsCloudDedicated(database) {
186
+ try {
187
+ const httpClient = this.baseService.getInfluxHttpClient();
188
+ const response = await httpClient.get("/query", {
189
+ params: {
190
+ db: database,
191
+ q: "SHOW MEASUREMENTS",
192
+ },
193
+ });
194
+ if (response.results &&
195
+ response.results[0] &&
196
+ response.results[0].series) {
197
+ const series = response.results[0].series[0];
198
+ if (series.name === "measurements" && series.values) {
199
+ return series.values.map((value) => ({ name: value[0] }));
200
+ }
201
+ }
202
+ return [];
203
+ }
204
+ catch (error) {
205
+ throw new Error(`Failed to get measurements: ${error.message}`);
206
+ }
207
+ }
208
+ /**
209
+ * Get measurements for clustered (HTTP client with InfluxQL)
210
+ */
211
+ async getMeasurementsClustered(database) {
212
+ try {
213
+ const httpClient = this.baseService.getInfluxHttpClient();
214
+ const response = await httpClient.get("/query", {
215
+ params: {
216
+ db: database,
217
+ q: "SHOW MEASUREMENTS",
218
+ },
219
+ });
220
+ if (response.results &&
221
+ response.results[0] &&
222
+ response.results[0].series) {
223
+ const series = response.results[0].series[0];
224
+ if (series.name === "measurements" && series.values) {
225
+ return series.values.map((value) => ({ name: value[0] }));
226
+ }
227
+ }
228
+ return [];
229
+ }
230
+ catch (error) {
231
+ throw new Error(`Failed to get measurements: ${error.message}`);
232
+ }
233
+ }
234
+ /**
235
+ * Get measurements for core/enterprise
236
+ */
237
+ async getMeasurementsCoreEnterprise(database) {
238
+ try {
239
+ const query = "SELECT DISTINCT table_name FROM information_schema.columns WHERE table_schema = 'iox'";
240
+ const result = await this.executeQuery(query, database, {
241
+ format: "json",
242
+ });
243
+ if (Array.isArray(result)) {
244
+ return result.map((row) => ({ name: row.table_name }));
245
+ }
246
+ return result;
247
+ }
248
+ catch (error) {
249
+ throw new Error(`Failed to get measurements: ${error.message}`);
250
+ }
251
+ }
252
+ /**
253
+ * Get measurements for cloud-serverless
254
+ * Parses the Cloud Serverless response format with _fields arrays
255
+ */
256
+ async getMeasurementsCloudServerless(database) {
257
+ try {
258
+ const query = "SELECT DISTINCT table_name FROM information_schema.columns WHERE table_schema = 'iox'";
259
+ const result = await this.executeQuery(query, database, {
260
+ format: "json",
261
+ });
262
+ if (Array.isArray(result)) {
263
+ return result
264
+ .map((row) => {
265
+ // Cloud Serverless format: { "_fields": { "table_name": ["string", "actual_value"] } }
266
+ const tableName = row._fields?.table_name?.[1];
267
+ return { name: tableName };
268
+ })
269
+ .filter((item) => item.name); // Filter out undefined names
270
+ }
271
+ return [];
272
+ }
273
+ catch (error) {
274
+ throw new Error(`Failed to get measurements: ${error.message}`);
275
+ }
276
+ }
277
+ /**
278
+ * Get schema information for a measurement/table
279
+ * Uses SHOW FIELD KEYS + SHOW TAG KEYS for cloud-dedicated/clustered (HTTP), information_schema for others
280
+ */
281
+ async getMeasurementSchema(measurement, database) {
282
+ this.baseService.validateDataCapabilities();
283
+ const connectionInfo = this.baseService.getConnectionInfo();
284
+ switch (connectionInfo.type) {
285
+ case InfluxProductType.CloudDedicated:
286
+ return this.getMeasurementSchemaCloudDedicated(measurement, database);
287
+ case InfluxProductType.Clustered:
288
+ return this.getMeasurementSchemaClustered(measurement, database);
289
+ case InfluxProductType.CloudServerless:
290
+ return this.getMeasurementSchemaCloudServerless(measurement, database);
291
+ case InfluxProductType.Core:
292
+ case InfluxProductType.Enterprise:
293
+ return this.getMeasurementSchemaCoreEnterprise(measurement, database);
294
+ default:
295
+ throw new Error(`Unsupported InfluxDB product type: ${connectionInfo.type}`);
296
+ }
297
+ }
298
+ /**
299
+ * Get measurement schema for cloud-dedicated/clustered (HTTP client with InfluxQL)
300
+ */
301
+ async getMeasurementSchemaCloudDedicated(measurement, database) {
302
+ try {
303
+ const httpClient = this.baseService.getInfluxHttpClient();
304
+ const fieldKeysResponse = await httpClient.get("/query", {
305
+ params: {
306
+ db: database,
307
+ q: `SHOW FIELD KEYS FROM ${measurement}`,
308
+ },
309
+ });
310
+ const tagKeysResponse = await httpClient.get("/query", {
311
+ params: {
312
+ db: database,
313
+ q: `SHOW TAG KEYS FROM ${measurement}`,
314
+ },
315
+ });
316
+ const columns = [];
317
+ if (fieldKeysResponse.results &&
318
+ fieldKeysResponse.results[0] &&
319
+ fieldKeysResponse.results[0].series) {
320
+ const fieldSeries = fieldKeysResponse.results[0].series[0];
321
+ if (fieldSeries && fieldSeries.values) {
322
+ fieldSeries.values.forEach((value) => {
323
+ columns.push({
324
+ name: value[0],
325
+ type: value[1],
326
+ category: "field",
327
+ });
328
+ });
329
+ }
330
+ }
331
+ if (tagKeysResponse.results &&
332
+ tagKeysResponse.results[0] &&
333
+ tagKeysResponse.results[0].series) {
334
+ const tagSeries = tagKeysResponse.results[0].series[0];
335
+ if (tagSeries && tagSeries.values) {
336
+ tagSeries.values.forEach((value) => {
337
+ columns.push({
338
+ name: value[0],
339
+ type: "string",
340
+ category: "tag",
341
+ });
342
+ });
343
+ }
344
+ }
345
+ columns.unshift({
346
+ name: "time",
347
+ type: "timestamp",
348
+ category: "time",
349
+ });
350
+ return { columns };
351
+ }
352
+ catch (error) {
353
+ if (error.response?.status === 404 ||
354
+ error.message.includes("not found")) {
355
+ throw new Error(`Measurement '${measurement}' does not exist in database '${database}'`);
356
+ }
357
+ throw new Error(`Failed to get schema for measurement '${measurement}': ${error.message}`);
358
+ }
359
+ }
360
+ /**
361
+ * Get measurement schema for clustered (HTTP client with InfluxQL)
362
+ */
363
+ async getMeasurementSchemaClustered(measurement, database) {
364
+ try {
365
+ const httpClient = this.baseService.getInfluxHttpClient();
366
+ const fieldKeysResponse = await httpClient.get("/query", {
367
+ params: {
368
+ db: database,
369
+ q: `SHOW FIELD KEYS FROM ${measurement}`,
370
+ },
371
+ });
372
+ const tagKeysResponse = await httpClient.get("/query", {
373
+ params: {
374
+ db: database,
375
+ q: `SHOW TAG KEYS FROM ${measurement}`,
376
+ },
377
+ });
378
+ const columns = [];
379
+ if (fieldKeysResponse.results &&
380
+ fieldKeysResponse.results[0] &&
381
+ fieldKeysResponse.results[0].series) {
382
+ const fieldSeries = fieldKeysResponse.results[0].series[0];
383
+ if (fieldSeries && fieldSeries.values) {
384
+ fieldSeries.values.forEach((value) => {
385
+ columns.push({
386
+ name: value[0],
387
+ type: value[1],
388
+ category: "field",
389
+ });
390
+ });
391
+ }
392
+ }
393
+ if (tagKeysResponse.results &&
394
+ tagKeysResponse.results[0] &&
395
+ tagKeysResponse.results[0].series) {
396
+ const tagSeries = tagKeysResponse.results[0].series[0];
397
+ if (tagSeries && tagSeries.values) {
398
+ tagSeries.values.forEach((value) => {
399
+ columns.push({
400
+ name: value[0],
401
+ type: "string",
402
+ category: "tag",
403
+ });
404
+ });
405
+ }
406
+ }
407
+ columns.unshift({
408
+ name: "time",
409
+ type: "timestamp",
410
+ category: "time",
411
+ });
412
+ return { columns };
413
+ }
414
+ catch (error) {
415
+ if (error.response?.status === 404 ||
416
+ error.message.includes("not found")) {
417
+ throw new Error(`Measurement '${measurement}' does not exist in database '${database}'`);
418
+ }
419
+ throw new Error(`Failed to get schema for measurement '${measurement}': ${error.message}`);
420
+ }
421
+ }
422
+ /**
423
+ * Get measurement schema for core/enterprise
424
+ */
425
+ async getMeasurementSchemaCoreEnterprise(measurement, database) {
426
+ try {
427
+ const query = `SELECT column_name, data_type FROM information_schema.columns WHERE table_name = '${measurement}' AND table_schema = 'iox'`;
428
+ const result = await this.executeQuery(query, database, {
429
+ format: "json",
430
+ });
431
+ if (Array.isArray(result)) {
432
+ const columns = result.map((row) => {
433
+ let category = "field";
434
+ if (row.column_name === "time") {
435
+ category = "time";
436
+ }
437
+ else if (row.data_type === "string" || row.data_type === "text") {
438
+ category = "tag";
439
+ }
440
+ return {
441
+ name: row.column_name,
442
+ type: row.data_type,
443
+ category,
444
+ };
445
+ });
446
+ return { columns };
447
+ }
448
+ return result;
449
+ }
450
+ catch (error) {
451
+ if (error.message.includes("not found")) {
452
+ throw new Error(`Table '${measurement}' does not exist in database '${database}'`);
453
+ }
454
+ throw new Error(`Failed to get schema for measurement '${measurement}': ${error.message}`);
455
+ }
456
+ }
457
+ /**
458
+ * Get measurement schema for cloud-serverless
459
+ * Parses the Cloud Serverless response format with _fields arrays
460
+ */
461
+ async getMeasurementSchemaCloudServerless(measurement, database) {
462
+ try {
463
+ const query = `SELECT column_name, data_type FROM information_schema.columns WHERE table_name = '${measurement}' AND table_schema = 'iox'`;
464
+ const result = await this.executeQuery(query, database, {
465
+ format: "json",
466
+ });
467
+ if (Array.isArray(result)) {
468
+ const columns = result
469
+ .map((row) => {
470
+ const columnName = row._fields?.column_name?.[1];
471
+ const dataType = row._fields?.data_type?.[1];
472
+ let category = "field";
473
+ if (columnName === "time") {
474
+ category = "time";
475
+ }
476
+ else if (dataType?.includes("Dictionary") ||
477
+ dataType === "string" ||
478
+ dataType === "text") {
479
+ category = "tag";
480
+ }
481
+ return {
482
+ name: columnName,
483
+ type: dataType,
484
+ category,
485
+ };
486
+ })
487
+ .filter((col) => col.name);
488
+ return { columns };
489
+ }
490
+ return { columns: [] };
491
+ }
492
+ catch (error) {
493
+ if (error.message.includes("not found")) {
494
+ throw new Error(`Table '${measurement}' does not exist in database '${database}'`);
495
+ }
496
+ throw new Error(`Failed to get schema for measurement '${measurement}': ${error.message}`);
497
+ }
498
+ }
499
+ }