@havesomecode/kibana-mcp-server 0.1.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.
@@ -0,0 +1,383 @@
1
+ import { encodeQueryCursor } from "./cursor.js";
2
+ function asRecord(value) {
3
+ return value && typeof value === "object" ? value : {};
4
+ }
5
+ function asArray(value) {
6
+ return Array.isArray(value) ? value : [];
7
+ }
8
+ function extractTotal(rawResponse) {
9
+ const hits = asRecord(rawResponse.hits);
10
+ const total = hits.total;
11
+ if (typeof total === "number") {
12
+ return total;
13
+ }
14
+ if (total && typeof total === "object") {
15
+ const totalValue = total.value;
16
+ return typeof totalValue === "number" ? totalValue : 0;
17
+ }
18
+ return 0;
19
+ }
20
+ function extractSummary(rawDocument, defaultTextFields) {
21
+ for (const field of [...defaultTextFields, "message", "log", "event.original"]) {
22
+ const candidate = rawDocument[field];
23
+ if (typeof candidate === "string" && candidate.trim() !== "") {
24
+ return candidate;
25
+ }
26
+ }
27
+ return JSON.stringify(rawDocument).slice(0, 240);
28
+ }
29
+ function extractSelectedFields(rawDocument, evidenceFields) {
30
+ return evidenceFields.reduce((selected, fieldName) => {
31
+ if (fieldName in rawDocument) {
32
+ selected[fieldName] = rawDocument[fieldName];
33
+ }
34
+ return selected;
35
+ }, {});
36
+ }
37
+ function extractNestedMatches(hitRecord) {
38
+ const innerHits = asRecord(hitRecord.inner_hits);
39
+ return Object.entries(innerHits)
40
+ .map(([path, nestedHitEnvelope]) => {
41
+ const hits = asArray(asRecord(asRecord(nestedHitEnvelope).hits).hits);
42
+ const documents = hits.map((nestedHit) => asRecord(asRecord(nestedHit)._source));
43
+ if (documents.length === 0) {
44
+ return null;
45
+ }
46
+ return {
47
+ path,
48
+ documents,
49
+ };
50
+ })
51
+ .filter((nestedMatch) => nestedMatch !== null);
52
+ }
53
+ function normalizeHits(result) {
54
+ const rawResponse = result.rawResponse;
55
+ const hitsEnvelope = asRecord(rawResponse.hits);
56
+ const rawHits = asArray(hitsEnvelope.hits);
57
+ const normalizedHits = rawHits.map((hit) => normalizeRawHit(result, asRecord(hit)));
58
+ return {
59
+ total: extractTotal(rawResponse),
60
+ hits: normalizedHits,
61
+ };
62
+ }
63
+ function normalizeRawHit(result, hitRecord) {
64
+ const rawDocument = asRecord(hitRecord._source);
65
+ const nestedMatches = extractNestedMatches(hitRecord);
66
+ return {
67
+ source_id: result.source.id,
68
+ timestamp: typeof rawDocument[result.source.timeField] === "string"
69
+ ? rawDocument[result.source.timeField]
70
+ : null,
71
+ summary: extractSummary(rawDocument, result.source.defaultTextFields),
72
+ document_id: typeof hitRecord._id === "string" ? hitRecord._id : null,
73
+ index: typeof hitRecord._index === "string" ? hitRecord._index : null,
74
+ selected_fields: extractSelectedFields(rawDocument, result.source.evidenceFields),
75
+ ...(nestedMatches.length > 0
76
+ ? {
77
+ nested_matches: nestedMatches,
78
+ }
79
+ : {}),
80
+ raw_document: rawDocument,
81
+ };
82
+ }
83
+ function buildNextCursor(plan, executions, hitsCount, totalCount) {
84
+ if (plan.mode !== "hits" ||
85
+ plan.sourceQueries.length !== 1 ||
86
+ hitsCount === 0 ||
87
+ totalCount <= hitsCount) {
88
+ return undefined;
89
+ }
90
+ const execution = executions[0];
91
+ const rawHits = asArray(asRecord(asRecord(execution?.rawResponse.hits).hits));
92
+ const lastHit = rawHits[Math.min(rawHits.length, hitsCount) - 1];
93
+ const sortValues = asArray(asRecord(lastHit).sort);
94
+ if (sortValues.length === 0) {
95
+ return undefined;
96
+ }
97
+ return encodeQueryCursor({
98
+ source_id: execution.source.id,
99
+ sort: plan.sort,
100
+ sort_by: plan.sourceQueries[0]?.resolvedSortBy ?? plan.sortBy ?? execution.source.timeField,
101
+ values: sortValues,
102
+ });
103
+ }
104
+ function compareSortValues(left, right, direction) {
105
+ if (left == null && right == null) {
106
+ return 0;
107
+ }
108
+ if (left == null) {
109
+ return 1;
110
+ }
111
+ if (right == null) {
112
+ return -1;
113
+ }
114
+ let comparison = 0;
115
+ if (typeof left === "number" && typeof right === "number") {
116
+ comparison = left - right;
117
+ }
118
+ else if (typeof left === "boolean" && typeof right === "boolean") {
119
+ comparison = Number(left) - Number(right);
120
+ }
121
+ else {
122
+ comparison = String(left).localeCompare(String(right), undefined, {
123
+ numeric: true,
124
+ sensitivity: "base",
125
+ });
126
+ }
127
+ return direction === "asc" ? comparison : -comparison;
128
+ }
129
+ function sortHitsGlobally(plan, hits) {
130
+ const resolvedSortByBySource = new Map(plan.sourceQueries.map((sourceQuery) => [sourceQuery.source.id, sourceQuery.resolvedSortBy]));
131
+ return [...hits].sort((left, right) => {
132
+ const leftSortField = resolvedSortByBySource.get(left.source_id);
133
+ const rightSortField = resolvedSortByBySource.get(right.source_id);
134
+ const leftSortValue = leftSortField ? left.raw_document[leftSortField] : left.timestamp;
135
+ const rightSortValue = rightSortField ? right.raw_document[rightSortField] : right.timestamp;
136
+ const comparison = compareSortValues(leftSortValue, rightSortValue, plan.sort);
137
+ if (comparison !== 0) {
138
+ return comparison;
139
+ }
140
+ return compareSortValues(left.timestamp, right.timestamp, plan.sort);
141
+ });
142
+ }
143
+ function normalizeHistogram(result) {
144
+ const histogram = asRecord(asRecord(rawResponseWithAggs(result)).aggs).histogram ??
145
+ asRecord(asRecord(rawResponseWithAggs(result)).aggregations).histogram;
146
+ const buckets = asArray(asRecord(histogram).buckets);
147
+ return buckets.map((bucket) => {
148
+ const bucketRecord = asRecord(bucket);
149
+ return {
150
+ key: typeof bucketRecord.key === "number" || typeof bucketRecord.key === "string"
151
+ ? bucketRecord.key
152
+ : "",
153
+ key_as_string: typeof bucketRecord.key_as_string === "string" ? bucketRecord.key_as_string : undefined,
154
+ count: typeof bucketRecord.doc_count === "number" ? bucketRecord.doc_count : 0,
155
+ };
156
+ });
157
+ }
158
+ function normalizeTerms(result) {
159
+ const groups = asRecord(asRecord(rawResponseWithAggs(result)).aggs).groups ??
160
+ asRecord(asRecord(rawResponseWithAggs(result)).aggregations).groups;
161
+ const buckets = asArray(asRecord(groups).buckets);
162
+ return buckets.map((bucket) => {
163
+ const bucketRecord = asRecord(bucket);
164
+ return {
165
+ key: String(bucketRecord.key ?? ""),
166
+ count: typeof bucketRecord.doc_count === "number" ? bucketRecord.doc_count : 0,
167
+ };
168
+ });
169
+ }
170
+ function normalizeStatsSummary(statsRecord, percentilesRecord) {
171
+ const percentileValues = asRecord(percentilesRecord.values);
172
+ return {
173
+ count: typeof statsRecord.count === "number" ? statsRecord.count : 0,
174
+ min: typeof statsRecord.min === "number" ? statsRecord.min : null,
175
+ max: typeof statsRecord.max === "number" ? statsRecord.max : null,
176
+ avg: typeof statsRecord.avg === "number" ? statsRecord.avg : null,
177
+ sum: typeof statsRecord.sum === "number" ? statsRecord.sum : 0,
178
+ p50: typeof percentileValues["50.0"] === "number" ? percentileValues["50.0"] : null,
179
+ p95: typeof percentileValues["95.0"] === "number" ? percentileValues["95.0"] : null,
180
+ p99: typeof percentileValues["99.0"] === "number" ? percentileValues["99.0"] : null,
181
+ };
182
+ }
183
+ function normalizeStats(plan, result) {
184
+ const aggregations = asRecord(rawResponseWithAggs(result).aggs).stats_summary
185
+ ? asRecord(rawResponseWithAggs(result).aggs)
186
+ : asRecord(rawResponseWithAggs(result).aggregations);
187
+ return {
188
+ source_id: result.source.id,
189
+ field: plan.statsField ?? "",
190
+ summary: normalizeStatsSummary(asRecord(aggregations.stats_summary), asRecord(aggregations.stats_percentiles)),
191
+ };
192
+ }
193
+ function normalizeGroupedTopHits(plan, result) {
194
+ const groups = asRecord(asRecord(rawResponseWithAggs(result)).aggs).groups ??
195
+ asRecord(asRecord(rawResponseWithAggs(result)).aggregations).groups;
196
+ const buckets = asArray(asRecord(groups).buckets);
197
+ return {
198
+ source_id: result.source.id,
199
+ group_by: plan.groupBy ?? "",
200
+ buckets: buckets.map((bucket) => {
201
+ const bucketRecord = asRecord(bucket);
202
+ const topHits = asArray(asRecord(asRecord(bucketRecord.top_hits).hits).hits);
203
+ return {
204
+ key: String(bucketRecord.key ?? ""),
205
+ count: typeof bucketRecord.doc_count === "number" ? bucketRecord.doc_count : 0,
206
+ hits: topHits.map((hit) => normalizeRawHit(result, asRecord(hit))),
207
+ };
208
+ }),
209
+ };
210
+ }
211
+ function rawResponseWithAggs(result) {
212
+ return result.rawResponse;
213
+ }
214
+ export function normalizeQueryResponse(plan, executions) {
215
+ const resolvedFilters = plan.sourceQueries.flatMap((sourceQuery) => sourceQuery.resolvedFilters);
216
+ const advisories = plan.sourceQueries.flatMap((sourceQuery) => sourceQuery.advisories);
217
+ if (plan.mode === "hits") {
218
+ const normalized = executions.map(normalizeHits);
219
+ const allHits = normalized.flatMap((entry) => entry.hits);
220
+ const hits = sortHitsGlobally(plan, allHits).slice(0, plan.limit);
221
+ const nextCursor = buildNextCursor(plan, executions, hits.length, normalized.reduce((sum, entry) => sum + entry.total, 0));
222
+ return {
223
+ query_echo: {
224
+ source_ids: plan.sourceIds,
225
+ start_time: plan.startTime,
226
+ end_time: plan.endTime,
227
+ text: plan.text,
228
+ filters: resolvedFilters,
229
+ ...(plan.nestedFilters && plan.nestedFilters.length > 0
230
+ ? { nested_filters: plan.nestedFilters }
231
+ : {}),
232
+ ...(plan.cursor ? { cursor: plan.cursor } : {}),
233
+ mode: plan.mode,
234
+ sort: plan.sort,
235
+ sort_by: plan.sortBy,
236
+ resolved_sort_by_by_source: plan.sourceQueries.map((sourceQuery) => ({
237
+ source_id: sourceQuery.source.id,
238
+ resolved_sort_by: sourceQuery.resolvedSortBy,
239
+ })),
240
+ ...(advisories.length > 0 ? { advisories } : {}),
241
+ limit: plan.limit,
242
+ truncated: allHits.length > plan.limit,
243
+ },
244
+ total: normalized.reduce((sum, entry) => sum + entry.total, 0),
245
+ ...(nextCursor ? { next_cursor: nextCursor } : {}),
246
+ hits,
247
+ };
248
+ }
249
+ if (plan.mode === "count") {
250
+ const countsBySource = executions.map((execution) => ({
251
+ source_id: execution.source.id,
252
+ count: extractTotal(execution.rawResponse),
253
+ }));
254
+ return {
255
+ query_echo: {
256
+ source_ids: plan.sourceIds,
257
+ start_time: plan.startTime,
258
+ end_time: plan.endTime,
259
+ text: plan.text,
260
+ filters: resolvedFilters,
261
+ ...(plan.nestedFilters && plan.nestedFilters.length > 0
262
+ ? { nested_filters: plan.nestedFilters }
263
+ : {}),
264
+ ...(plan.cursor ? { cursor: plan.cursor } : {}),
265
+ mode: plan.mode,
266
+ sort: plan.sort,
267
+ sort_by: plan.sortBy,
268
+ ...(advisories.length > 0 ? { advisories } : {}),
269
+ limit: plan.limit,
270
+ truncated: false,
271
+ },
272
+ total: countsBySource.reduce((sum, entry) => sum + entry.count, 0),
273
+ counts_by_source: countsBySource,
274
+ };
275
+ }
276
+ if (plan.mode === "histogram") {
277
+ const histograms = executions.map((execution) => ({
278
+ source_id: execution.source.id,
279
+ buckets: normalizeHistogram(execution),
280
+ }));
281
+ return {
282
+ query_echo: {
283
+ source_ids: plan.sourceIds,
284
+ start_time: plan.startTime,
285
+ end_time: plan.endTime,
286
+ text: plan.text,
287
+ filters: resolvedFilters,
288
+ ...(plan.nestedFilters && plan.nestedFilters.length > 0
289
+ ? { nested_filters: plan.nestedFilters }
290
+ : {}),
291
+ ...(plan.cursor ? { cursor: plan.cursor } : {}),
292
+ mode: plan.mode,
293
+ sort: plan.sort,
294
+ sort_by: plan.sortBy,
295
+ ...(advisories.length > 0 ? { advisories } : {}),
296
+ limit: plan.limit,
297
+ histogram_interval: plan.histogramInterval,
298
+ truncated: false,
299
+ },
300
+ total: histograms.reduce((sum, histogram) => sum + histogram.buckets.reduce((bucketSum, bucket) => bucketSum + bucket.count, 0), 0),
301
+ histograms,
302
+ };
303
+ }
304
+ if (plan.mode === "stats") {
305
+ const stats = executions.map((execution) => normalizeStats(plan, execution));
306
+ return {
307
+ query_echo: {
308
+ source_ids: plan.sourceIds,
309
+ start_time: plan.startTime,
310
+ end_time: plan.endTime,
311
+ text: plan.text,
312
+ filters: resolvedFilters,
313
+ ...(plan.nestedFilters && plan.nestedFilters.length > 0
314
+ ? { nested_filters: plan.nestedFilters }
315
+ : {}),
316
+ ...(plan.cursor ? { cursor: plan.cursor } : {}),
317
+ mode: plan.mode,
318
+ sort: plan.sort,
319
+ sort_by: plan.sortBy,
320
+ ...(advisories.length > 0 ? { advisories } : {}),
321
+ limit: plan.limit,
322
+ stats_field: plan.statsField,
323
+ group_by: plan.groupBy,
324
+ truncated: false,
325
+ },
326
+ total: stats.reduce((sum, entry) => sum + entry.summary.count, 0),
327
+ stats,
328
+ };
329
+ }
330
+ if (plan.mode === "grouped_top_hits") {
331
+ const groupedHits = executions.map((execution) => normalizeGroupedTopHits(plan, execution));
332
+ return {
333
+ query_echo: {
334
+ source_ids: plan.sourceIds,
335
+ start_time: plan.startTime,
336
+ end_time: plan.endTime,
337
+ text: plan.text,
338
+ filters: resolvedFilters,
339
+ ...(plan.nestedFilters && plan.nestedFilters.length > 0
340
+ ? { nested_filters: plan.nestedFilters }
341
+ : {}),
342
+ ...(plan.cursor ? { cursor: plan.cursor } : {}),
343
+ mode: plan.mode,
344
+ sort: plan.sort,
345
+ sort_by: plan.sortBy,
346
+ ...(advisories.length > 0 ? { advisories } : {}),
347
+ limit: plan.limit,
348
+ top_hits_size: plan.topHitsSize,
349
+ group_by: plan.groupBy,
350
+ truncated: false,
351
+ },
352
+ total: groupedHits.reduce((sum, entry) => sum + entry.buckets.reduce((bucketSum, bucket) => bucketSum + bucket.count, 0), 0),
353
+ grouped_hits: groupedHits,
354
+ };
355
+ }
356
+ const groups = executions.map((execution) => ({
357
+ source_id: execution.source.id,
358
+ field: plan.groupBy ?? "",
359
+ buckets: normalizeTerms(execution),
360
+ }));
361
+ return {
362
+ query_echo: {
363
+ source_ids: plan.sourceIds,
364
+ start_time: plan.startTime,
365
+ end_time: plan.endTime,
366
+ text: plan.text,
367
+ filters: resolvedFilters,
368
+ ...(plan.nestedFilters && plan.nestedFilters.length > 0
369
+ ? { nested_filters: plan.nestedFilters }
370
+ : {}),
371
+ ...(plan.cursor ? { cursor: plan.cursor } : {}),
372
+ mode: plan.mode,
373
+ sort: plan.sort,
374
+ sort_by: plan.sortBy,
375
+ ...(advisories.length > 0 ? { advisories } : {}),
376
+ limit: plan.limit,
377
+ group_by: plan.groupBy,
378
+ truncated: false,
379
+ },
380
+ total: groups.reduce((sum, group) => sum + group.buckets.reduce((bucketSum, bucket) => bucketSum + bucket.count, 0), 0),
381
+ groups,
382
+ };
383
+ }
@@ -0,0 +1,91 @@
1
+ function normalizeFieldName(value) {
2
+ return value.trim().toLowerCase();
3
+ }
4
+ function buildFieldMap(fields) {
5
+ return new Map(fields.map((field) => [field.name, field]));
6
+ }
7
+ function linkFieldRelationships(fields) {
8
+ const fieldMap = buildFieldMap(fields);
9
+ const subfieldsByParent = new Map();
10
+ for (const field of fields) {
11
+ const parent = field.multi_field_parent ??
12
+ (field.name.includes(".") ? field.name.slice(0, field.name.lastIndexOf(".")) : undefined);
13
+ if (!parent) {
14
+ continue;
15
+ }
16
+ const subfields = subfieldsByParent.get(parent) ?? [];
17
+ subfields.push(field.name);
18
+ subfieldsByParent.set(parent, subfields);
19
+ }
20
+ return fields.map((field) => {
21
+ const subfields = [
22
+ ...new Set([...(field.subfields ?? []), ...(subfieldsByParent.get(field.name) ?? [])]),
23
+ ];
24
+ const keywordSubfield = subfields
25
+ .map((subfieldName) => fieldMap.get(subfieldName))
26
+ .find((candidate) => candidate?.type === "keyword" && candidate.aggregatable);
27
+ return {
28
+ ...field,
29
+ subfields,
30
+ preferred_exact_field: field.preferred_exact_field ??
31
+ (field.type === "text" ? keywordSubfield?.name : field.preferred_exact_field),
32
+ };
33
+ });
34
+ }
35
+ function mergeFieldHints(source, fields) {
36
+ const byName = buildFieldMap(fields);
37
+ for (const fieldHint of source.fieldHints) {
38
+ const existing = byName.get(fieldHint.name);
39
+ if (existing) {
40
+ existing.aliases = [...new Set([...(existing.aliases ?? []), ...(fieldHint.aliases ?? [])])];
41
+ existing.description = existing.description ?? fieldHint.description;
42
+ continue;
43
+ }
44
+ byName.set(fieldHint.name, {
45
+ name: fieldHint.name,
46
+ type: fieldHint.type,
47
+ description: fieldHint.description,
48
+ aliases: fieldHint.aliases ?? [],
49
+ subfields: [],
50
+ });
51
+ }
52
+ return [...byName.values()].sort((left, right) => left.name.localeCompare(right.name));
53
+ }
54
+ export class SchemaCatalog {
55
+ client;
56
+ cache = new Map();
57
+ constructor(client) {
58
+ this.client = client;
59
+ }
60
+ async getFields(source) {
61
+ const existing = this.cache.get(source.id);
62
+ if (existing) {
63
+ return existing;
64
+ }
65
+ const pending = this.client
66
+ .describeFields(source)
67
+ .then((fields) => linkFieldRelationships(mergeFieldHints(source, fields)));
68
+ this.cache.set(source.id, pending);
69
+ return pending;
70
+ }
71
+ filterFields(fields, query, limit = 100) {
72
+ const normalizedQuery = query ? normalizeFieldName(query) : undefined;
73
+ const filtered = normalizedQuery
74
+ ? fields.filter((field) => {
75
+ const haystacks = [
76
+ field.name,
77
+ field.type ?? "",
78
+ field.description ?? "",
79
+ field.nested_path ?? "",
80
+ field.object_array_path ?? "",
81
+ field.multi_field_parent ?? "",
82
+ field.preferred_exact_field ?? "",
83
+ ...(field.aliases ?? []),
84
+ ...field.subfields,
85
+ ];
86
+ return haystacks.some((value) => normalizeFieldName(value).includes(normalizedQuery));
87
+ })
88
+ : fields;
89
+ return filtered.slice(0, limit);
90
+ }
91
+ }