@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.
- package/LICENSE +21 -0
- package/README.md +486 -0
- package/config/sources.example.json +97 -0
- package/dist/src/cli.js +160 -0
- package/dist/src/config.js +204 -0
- package/dist/src/index.js +10 -0
- package/dist/src/kibana_client.js +441 -0
- package/dist/src/mcp_entry.js +6 -0
- package/dist/src/mcp_runtime.js +23 -0
- package/dist/src/profile_paths.js +36 -0
- package/dist/src/profile_store.js +136 -0
- package/dist/src/query/compiler.js +430 -0
- package/dist/src/query/cursor.js +7 -0
- package/dist/src/query/normalize.js +383 -0
- package/dist/src/schema_catalog.js +91 -0
- package/dist/src/secret_store.js +261 -0
- package/dist/src/server.js +87 -0
- package/dist/src/setup_flow.js +160 -0
- package/dist/src/source_catalog.js +47 -0
- package/dist/src/tools/configure.js +53 -0
- package/dist/src/tools/describe_fields.js +55 -0
- package/dist/src/tools/discover.js +47 -0
- package/dist/src/tools/filter.js +141 -0
- package/dist/src/tools/query.js +287 -0
- package/dist/src/types.js +1 -0
- package/docs/project/compatibility-matrix.md +22 -0
- package/docs/project/distribution-strategy.md +78 -0
- package/docs/project/npm-publishing.md +57 -0
- package/docs/project/release-checklist.md +51 -0
- package/docs/project/support-policy.md +52 -0
- package/package.json +65 -0
- package/plugins/kibana-log-investigation/.codex-plugin/plugin.json +31 -0
- package/plugins/kibana-log-investigation/.mcp.json +10 -0
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
export const discoverInputSchema = z.object({
|
|
3
|
+
query: z.string().trim().min(1).optional(),
|
|
4
|
+
limit: z.number().int().positive().max(100).default(20),
|
|
5
|
+
});
|
|
6
|
+
export const discoverOutputSchema = z.object({
|
|
7
|
+
total: z.number().int().nonnegative(),
|
|
8
|
+
sources: z.array(z.object({
|
|
9
|
+
id: z.string(),
|
|
10
|
+
name: z.string(),
|
|
11
|
+
description: z.string().optional(),
|
|
12
|
+
tags: z.array(z.string()),
|
|
13
|
+
time_field: z.string(),
|
|
14
|
+
field_hints: z.array(z.object({
|
|
15
|
+
name: z.string(),
|
|
16
|
+
type: z.string().optional(),
|
|
17
|
+
description: z.string().optional(),
|
|
18
|
+
aliases: z.array(z.string()),
|
|
19
|
+
})),
|
|
20
|
+
})),
|
|
21
|
+
});
|
|
22
|
+
export function executeDiscover(input, sourceCatalog) {
|
|
23
|
+
const sources = sourceCatalog.list(input.query, input.limit);
|
|
24
|
+
return {
|
|
25
|
+
total: sources.length,
|
|
26
|
+
sources,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
export function formatDiscoverResult(result) {
|
|
30
|
+
if (result.sources.length === 0) {
|
|
31
|
+
return "No sources matched the requested discovery query.";
|
|
32
|
+
}
|
|
33
|
+
return result.sources
|
|
34
|
+
.map((source) => {
|
|
35
|
+
const fieldPreview = source.field_hints.length > 0
|
|
36
|
+
? source.field_hints.map((fieldHint) => fieldHint.name).join(", ")
|
|
37
|
+
: "no field hints configured";
|
|
38
|
+
return `${source.id}: ${source.name} [${source.tags.join(", ")}] time_field=${source.time_field} fields=${fieldPreview}`;
|
|
39
|
+
})
|
|
40
|
+
.join("\n");
|
|
41
|
+
}
|
|
42
|
+
export function createDiscoverCallToolResult(result) {
|
|
43
|
+
return {
|
|
44
|
+
content: [{ type: "text", text: formatDiscoverResult(result) }],
|
|
45
|
+
structuredContent: result,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { executeQuery, queryOutputSchema } from "./query.js";
|
|
3
|
+
const exactFilterSchema = z.object({
|
|
4
|
+
field: z.string().min(1),
|
|
5
|
+
value: z.union([z.string(), z.number(), z.boolean()]),
|
|
6
|
+
});
|
|
7
|
+
const nestedExactFilterSchema = z.object({
|
|
8
|
+
path: z.string().min(1),
|
|
9
|
+
field: z.string().min(1),
|
|
10
|
+
value: z.union([z.string(), z.number(), z.boolean()]),
|
|
11
|
+
});
|
|
12
|
+
export const filterInputSchema = z
|
|
13
|
+
.object({
|
|
14
|
+
source_ids: z.array(z.string().min(1)).min(1),
|
|
15
|
+
start_time: z.string().datetime({ offset: true }),
|
|
16
|
+
end_time: z.string().datetime({ offset: true }),
|
|
17
|
+
field: z.string().min(1),
|
|
18
|
+
value: z.union([z.string(), z.number(), z.boolean()]),
|
|
19
|
+
filters: z.array(exactFilterSchema).default([]),
|
|
20
|
+
nested_filters: z.array(nestedExactFilterSchema).default([]),
|
|
21
|
+
cursor: z.string().min(1).optional(),
|
|
22
|
+
mode: z
|
|
23
|
+
.enum(["hits", "count", "histogram", "terms", "stats", "grouped_top_hits"])
|
|
24
|
+
.default("hits"),
|
|
25
|
+
sort: z.enum(["asc", "desc"]).default("desc"),
|
|
26
|
+
sort_by: z.string().min(1).optional(),
|
|
27
|
+
limit: z.number().int().positive().max(1000).default(100),
|
|
28
|
+
extract_nested: z.boolean().default(false),
|
|
29
|
+
stats_field: z.string().min(1).optional(),
|
|
30
|
+
top_hits_size: z.number().int().positive().max(100).default(1),
|
|
31
|
+
histogram_interval: z.string().min(1).optional(),
|
|
32
|
+
group_by: z.string().min(1).optional(),
|
|
33
|
+
})
|
|
34
|
+
.superRefine((value, ctx) => {
|
|
35
|
+
if (Date.parse(value.start_time) >= Date.parse(value.end_time)) {
|
|
36
|
+
ctx.addIssue({
|
|
37
|
+
code: z.ZodIssueCode.custom,
|
|
38
|
+
path: ["end_time"],
|
|
39
|
+
message: "end_time must be later than start_time",
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
if (value.mode === "histogram" && !value.histogram_interval) {
|
|
43
|
+
ctx.addIssue({
|
|
44
|
+
code: z.ZodIssueCode.custom,
|
|
45
|
+
path: ["histogram_interval"],
|
|
46
|
+
message: "histogram_interval is required when mode is 'histogram'",
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
if ((value.mode === "terms" || value.mode === "grouped_top_hits") && !value.group_by) {
|
|
50
|
+
ctx.addIssue({
|
|
51
|
+
code: z.ZodIssueCode.custom,
|
|
52
|
+
path: ["group_by"],
|
|
53
|
+
message: `group_by is required when mode is '${value.mode}'`,
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
if (!["hits", "grouped_top_hits"].includes(value.mode) && value.sort_by) {
|
|
57
|
+
ctx.addIssue({
|
|
58
|
+
code: z.ZodIssueCode.custom,
|
|
59
|
+
path: ["sort_by"],
|
|
60
|
+
message: "sort_by is only supported when mode is 'hits' or 'grouped_top_hits'",
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
if (value.cursor && value.mode !== "hits") {
|
|
64
|
+
ctx.addIssue({
|
|
65
|
+
code: z.ZodIssueCode.custom,
|
|
66
|
+
path: ["cursor"],
|
|
67
|
+
message: "cursor is only supported when mode is 'hits'",
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
if (value.cursor && value.source_ids.length !== 1) {
|
|
71
|
+
ctx.addIssue({
|
|
72
|
+
code: z.ZodIssueCode.custom,
|
|
73
|
+
path: ["cursor"],
|
|
74
|
+
message: "cursor pagination is only supported for single-source hits queries",
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
if (value.mode === "stats" && !value.stats_field) {
|
|
78
|
+
ctx.addIssue({
|
|
79
|
+
code: z.ZodIssueCode.custom,
|
|
80
|
+
path: ["stats_field"],
|
|
81
|
+
message: "stats_field is required when mode is 'stats'",
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
if (value.mode === "grouped_top_hits" && !value.sort_by) {
|
|
85
|
+
ctx.addIssue({
|
|
86
|
+
code: z.ZodIssueCode.custom,
|
|
87
|
+
path: ["sort_by"],
|
|
88
|
+
message: "sort_by is required when mode is 'grouped_top_hits'",
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
export const filterOutputSchema = queryOutputSchema;
|
|
93
|
+
export async function executeFilter(input, sourceCatalog, kibanaClient, options) {
|
|
94
|
+
return executeQuery({
|
|
95
|
+
source_ids: input.source_ids,
|
|
96
|
+
start_time: input.start_time,
|
|
97
|
+
end_time: input.end_time,
|
|
98
|
+
filters: [{ field: input.field, value: input.value }, ...input.filters],
|
|
99
|
+
nested_filters: input.nested_filters,
|
|
100
|
+
cursor: input.cursor,
|
|
101
|
+
mode: input.mode,
|
|
102
|
+
sort: input.sort,
|
|
103
|
+
sort_by: input.sort_by,
|
|
104
|
+
limit: input.limit,
|
|
105
|
+
extract_nested: input.extract_nested,
|
|
106
|
+
stats_field: input.stats_field,
|
|
107
|
+
top_hits_size: input.top_hits_size,
|
|
108
|
+
histogram_interval: input.histogram_interval,
|
|
109
|
+
group_by: input.group_by,
|
|
110
|
+
}, sourceCatalog, kibanaClient, {
|
|
111
|
+
resolveFieldAliases: false,
|
|
112
|
+
resolvePreferredExactFields: true,
|
|
113
|
+
schemaCatalog: options?.schemaCatalog,
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
export function formatFilterResult(result) {
|
|
117
|
+
if (result.hits) {
|
|
118
|
+
const preview = result.hits
|
|
119
|
+
.slice(0, 5)
|
|
120
|
+
.map((hit) => `[${hit.timestamp ?? "no-timestamp"}] ${hit.source_id}: ${hit.summary}`)
|
|
121
|
+
.join("\n");
|
|
122
|
+
return `Returned ${result.hits.length} exact-field matches (total=${result.total}).\n${preview}`;
|
|
123
|
+
}
|
|
124
|
+
if (result.counts_by_source) {
|
|
125
|
+
return result.counts_by_source.map((count) => `${count.source_id}: ${count.count}`).join("\n");
|
|
126
|
+
}
|
|
127
|
+
if (result.histograms) {
|
|
128
|
+
return result.histograms
|
|
129
|
+
.map((histogram) => `${histogram.source_id}: ${histogram.buckets.length} histogram buckets`)
|
|
130
|
+
.join("\n");
|
|
131
|
+
}
|
|
132
|
+
return (result.groups ?? [])
|
|
133
|
+
.map((group) => `${group.source_id} grouped by ${group.field}: ${group.buckets.length} buckets`)
|
|
134
|
+
.join("\n");
|
|
135
|
+
}
|
|
136
|
+
export function createFilterCallToolResult(result) {
|
|
137
|
+
return {
|
|
138
|
+
content: [{ type: "text", text: formatFilterResult(result) }],
|
|
139
|
+
structuredContent: result,
|
|
140
|
+
};
|
|
141
|
+
}
|
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { compileQueryPlan } from "../query/compiler.js";
|
|
3
|
+
import { normalizeQueryResponse } from "../query/normalize.js";
|
|
4
|
+
const queryFilterSchema = z.object({
|
|
5
|
+
field: z.string().min(1),
|
|
6
|
+
value: z.union([z.string(), z.number(), z.boolean()]),
|
|
7
|
+
});
|
|
8
|
+
const nestedQueryFilterSchema = z.object({
|
|
9
|
+
path: z.string().min(1),
|
|
10
|
+
field: z.string().min(1),
|
|
11
|
+
value: z.union([z.string(), z.number(), z.boolean()]),
|
|
12
|
+
});
|
|
13
|
+
export const queryInputSchema = z
|
|
14
|
+
.object({
|
|
15
|
+
source_ids: z.array(z.string().min(1)).min(1),
|
|
16
|
+
start_time: z.string().datetime({ offset: true }),
|
|
17
|
+
end_time: z.string().datetime({ offset: true }),
|
|
18
|
+
text: z.string().trim().min(1).optional(),
|
|
19
|
+
filters: z.array(queryFilterSchema).default([]),
|
|
20
|
+
nested_filters: z.array(nestedQueryFilterSchema).default([]),
|
|
21
|
+
cursor: z.string().min(1).optional(),
|
|
22
|
+
mode: z
|
|
23
|
+
.enum(["hits", "count", "histogram", "terms", "stats", "grouped_top_hits"])
|
|
24
|
+
.default("hits"),
|
|
25
|
+
sort: z.enum(["asc", "desc"]).default("desc"),
|
|
26
|
+
sort_by: z.string().min(1).optional(),
|
|
27
|
+
limit: z.number().int().positive().max(1000).default(100),
|
|
28
|
+
extract_nested: z.boolean().default(false),
|
|
29
|
+
stats_field: z.string().min(1).optional(),
|
|
30
|
+
top_hits_size: z.number().int().positive().max(100).default(1),
|
|
31
|
+
histogram_interval: z.string().min(1).optional(),
|
|
32
|
+
group_by: z.string().min(1).optional(),
|
|
33
|
+
})
|
|
34
|
+
.superRefine((value, ctx) => {
|
|
35
|
+
if (Date.parse(value.start_time) >= Date.parse(value.end_time)) {
|
|
36
|
+
ctx.addIssue({
|
|
37
|
+
code: z.ZodIssueCode.custom,
|
|
38
|
+
path: ["end_time"],
|
|
39
|
+
message: "end_time must be later than start_time",
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
if (value.mode === "histogram" && !value.histogram_interval) {
|
|
43
|
+
ctx.addIssue({
|
|
44
|
+
code: z.ZodIssueCode.custom,
|
|
45
|
+
path: ["histogram_interval"],
|
|
46
|
+
message: "histogram_interval is required when mode is 'histogram'",
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
if ((value.mode === "terms" || value.mode === "grouped_top_hits") && !value.group_by) {
|
|
50
|
+
ctx.addIssue({
|
|
51
|
+
code: z.ZodIssueCode.custom,
|
|
52
|
+
path: ["group_by"],
|
|
53
|
+
message: `group_by is required when mode is '${value.mode}'`,
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
if (!["hits", "grouped_top_hits"].includes(value.mode) && value.sort_by) {
|
|
57
|
+
ctx.addIssue({
|
|
58
|
+
code: z.ZodIssueCode.custom,
|
|
59
|
+
path: ["sort_by"],
|
|
60
|
+
message: "sort_by is only supported when mode is 'hits' or 'grouped_top_hits'",
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
if (value.cursor && value.mode !== "hits") {
|
|
64
|
+
ctx.addIssue({
|
|
65
|
+
code: z.ZodIssueCode.custom,
|
|
66
|
+
path: ["cursor"],
|
|
67
|
+
message: "cursor is only supported when mode is 'hits'",
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
if (value.cursor && value.source_ids.length !== 1) {
|
|
71
|
+
ctx.addIssue({
|
|
72
|
+
code: z.ZodIssueCode.custom,
|
|
73
|
+
path: ["cursor"],
|
|
74
|
+
message: "cursor pagination is only supported for single-source hits queries",
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
if (value.mode === "stats" && !value.stats_field) {
|
|
78
|
+
ctx.addIssue({
|
|
79
|
+
code: z.ZodIssueCode.custom,
|
|
80
|
+
path: ["stats_field"],
|
|
81
|
+
message: "stats_field is required when mode is 'stats'",
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
if (value.mode === "grouped_top_hits" && !value.sort_by) {
|
|
85
|
+
ctx.addIssue({
|
|
86
|
+
code: z.ZodIssueCode.custom,
|
|
87
|
+
path: ["sort_by"],
|
|
88
|
+
message: "sort_by is required when mode is 'grouped_top_hits'",
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
export const queryOutputSchema = z.object({
|
|
93
|
+
query_echo: z.object({
|
|
94
|
+
source_ids: z.array(z.string()),
|
|
95
|
+
start_time: z.string(),
|
|
96
|
+
end_time: z.string(),
|
|
97
|
+
text: z.string().optional(),
|
|
98
|
+
filters: z.array(z.object({
|
|
99
|
+
field: z.string(),
|
|
100
|
+
value: z.union([z.string(), z.number(), z.boolean()]),
|
|
101
|
+
resolved_field: z.string(),
|
|
102
|
+
})),
|
|
103
|
+
nested_filters: z
|
|
104
|
+
.array(z.object({
|
|
105
|
+
path: z.string(),
|
|
106
|
+
field: z.string(),
|
|
107
|
+
value: z.union([z.string(), z.number(), z.boolean()]),
|
|
108
|
+
resolved_field: z.string(),
|
|
109
|
+
query_strategy: z.enum(["nested", "flat_object_path"]).optional(),
|
|
110
|
+
}))
|
|
111
|
+
.optional(),
|
|
112
|
+
cursor: z.string().optional(),
|
|
113
|
+
mode: z.enum(["hits", "count", "histogram", "terms", "stats", "grouped_top_hits"]),
|
|
114
|
+
sort: z.enum(["asc", "desc"]),
|
|
115
|
+
sort_by: z.string().optional(),
|
|
116
|
+
resolved_sort_by_by_source: z
|
|
117
|
+
.array(z.object({
|
|
118
|
+
source_id: z.string(),
|
|
119
|
+
resolved_sort_by: z.string(),
|
|
120
|
+
}))
|
|
121
|
+
.optional(),
|
|
122
|
+
advisories: z
|
|
123
|
+
.array(z.object({
|
|
124
|
+
kind: z.enum(["preferred_exact_field", "schema_unavailable", "non_nested_object_array"]),
|
|
125
|
+
source_id: z.string(),
|
|
126
|
+
purpose: z.enum(["filter", "sort", "group_by"]),
|
|
127
|
+
requested_field: z.string(),
|
|
128
|
+
resolved_field: z.string(),
|
|
129
|
+
reason: z.string(),
|
|
130
|
+
}))
|
|
131
|
+
.optional(),
|
|
132
|
+
limit: z.number(),
|
|
133
|
+
stats_field: z.string().optional(),
|
|
134
|
+
top_hits_size: z.number().optional(),
|
|
135
|
+
histogram_interval: z.string().optional(),
|
|
136
|
+
group_by: z.string().optional(),
|
|
137
|
+
truncated: z.boolean(),
|
|
138
|
+
}),
|
|
139
|
+
total: z.number(),
|
|
140
|
+
next_cursor: z.string().optional(),
|
|
141
|
+
hits: z
|
|
142
|
+
.array(z.object({
|
|
143
|
+
source_id: z.string(),
|
|
144
|
+
timestamp: z.string().nullable(),
|
|
145
|
+
summary: z.string(),
|
|
146
|
+
document_id: z.string().nullable(),
|
|
147
|
+
index: z.string().nullable(),
|
|
148
|
+
selected_fields: z.record(z.string(), z.unknown()),
|
|
149
|
+
nested_matches: z
|
|
150
|
+
.array(z.object({
|
|
151
|
+
path: z.string(),
|
|
152
|
+
documents: z.array(z.record(z.string(), z.unknown())),
|
|
153
|
+
}))
|
|
154
|
+
.optional(),
|
|
155
|
+
raw_document: z.record(z.string(), z.unknown()),
|
|
156
|
+
}))
|
|
157
|
+
.optional(),
|
|
158
|
+
counts_by_source: z
|
|
159
|
+
.array(z.object({
|
|
160
|
+
source_id: z.string(),
|
|
161
|
+
count: z.number(),
|
|
162
|
+
}))
|
|
163
|
+
.optional(),
|
|
164
|
+
histograms: z
|
|
165
|
+
.array(z.object({
|
|
166
|
+
source_id: z.string(),
|
|
167
|
+
buckets: z.array(z.object({
|
|
168
|
+
key: z.union([z.number(), z.string()]),
|
|
169
|
+
key_as_string: z.string().optional(),
|
|
170
|
+
count: z.number(),
|
|
171
|
+
})),
|
|
172
|
+
}))
|
|
173
|
+
.optional(),
|
|
174
|
+
groups: z
|
|
175
|
+
.array(z.object({
|
|
176
|
+
source_id: z.string(),
|
|
177
|
+
field: z.string(),
|
|
178
|
+
buckets: z.array(z.object({
|
|
179
|
+
key: z.string(),
|
|
180
|
+
count: z.number(),
|
|
181
|
+
})),
|
|
182
|
+
}))
|
|
183
|
+
.optional(),
|
|
184
|
+
stats: z
|
|
185
|
+
.array(z.object({
|
|
186
|
+
source_id: z.string(),
|
|
187
|
+
field: z.string(),
|
|
188
|
+
summary: z.object({
|
|
189
|
+
count: z.number(),
|
|
190
|
+
min: z.number().nullable(),
|
|
191
|
+
max: z.number().nullable(),
|
|
192
|
+
avg: z.number().nullable(),
|
|
193
|
+
sum: z.number(),
|
|
194
|
+
p50: z.number().nullable(),
|
|
195
|
+
p95: z.number().nullable(),
|
|
196
|
+
p99: z.number().nullable(),
|
|
197
|
+
}),
|
|
198
|
+
}))
|
|
199
|
+
.optional(),
|
|
200
|
+
grouped_hits: z
|
|
201
|
+
.array(z.object({
|
|
202
|
+
source_id: z.string(),
|
|
203
|
+
group_by: z.string(),
|
|
204
|
+
buckets: z.array(z.object({
|
|
205
|
+
key: z.string(),
|
|
206
|
+
count: z.number(),
|
|
207
|
+
hits: z.array(z.object({
|
|
208
|
+
source_id: z.string(),
|
|
209
|
+
timestamp: z.string().nullable(),
|
|
210
|
+
summary: z.string(),
|
|
211
|
+
document_id: z.string().nullable(),
|
|
212
|
+
index: z.string().nullable(),
|
|
213
|
+
selected_fields: z.record(z.string(), z.unknown()),
|
|
214
|
+
nested_matches: z
|
|
215
|
+
.array(z.object({
|
|
216
|
+
path: z.string(),
|
|
217
|
+
documents: z.array(z.record(z.string(), z.unknown())),
|
|
218
|
+
}))
|
|
219
|
+
.optional(),
|
|
220
|
+
raw_document: z.record(z.string(), z.unknown()),
|
|
221
|
+
})),
|
|
222
|
+
})),
|
|
223
|
+
}))
|
|
224
|
+
.optional(),
|
|
225
|
+
});
|
|
226
|
+
export async function executeQuery(input, sourceCatalog, kibanaClient, options) {
|
|
227
|
+
const sources = sourceCatalog.getRequiredSources(input.source_ids);
|
|
228
|
+
const schemaCatalog = options?.schemaCatalog;
|
|
229
|
+
const usesSchemaAwareFieldResolution = (options?.resolveFieldAliases !== false || options?.resolvePreferredExactFields !== false) &&
|
|
230
|
+
(input.filters.length > 0 || Boolean(input.sort_by) || Boolean(input.group_by));
|
|
231
|
+
const needsSchemaResolution = usesSchemaAwareFieldResolution || input.nested_filters.length > 0;
|
|
232
|
+
const sourceSchemas = new Map();
|
|
233
|
+
const sourceSchemaErrors = new Map();
|
|
234
|
+
if (schemaCatalog && needsSchemaResolution) {
|
|
235
|
+
for (const source of sources) {
|
|
236
|
+
try {
|
|
237
|
+
sourceSchemas.set(source.id, await schemaCatalog.getFields(source));
|
|
238
|
+
}
|
|
239
|
+
catch (error) {
|
|
240
|
+
sourceSchemaErrors.set(source.id, error instanceof Error ? error.message : String(error));
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
const plan = compileQueryPlan(input, sources, {
|
|
245
|
+
...options,
|
|
246
|
+
...(sourceSchemas.size > 0 ? { sourceSchemas } : {}),
|
|
247
|
+
...(sourceSchemaErrors.size > 0 ? { sourceSchemaErrors } : {}),
|
|
248
|
+
});
|
|
249
|
+
const executions = await kibanaClient.executeMany(plan.sourceQueries);
|
|
250
|
+
return queryOutputSchema.parse(normalizeQueryResponse(plan, executions));
|
|
251
|
+
}
|
|
252
|
+
export function formatQueryResult(result) {
|
|
253
|
+
if (result.hits) {
|
|
254
|
+
const preview = result.hits
|
|
255
|
+
.slice(0, 5)
|
|
256
|
+
.map((hit) => `[${hit.timestamp ?? "no-timestamp"}] ${hit.source_id}: ${hit.summary}`)
|
|
257
|
+
.join("\n");
|
|
258
|
+
return `Returned ${result.hits.length} hits (total=${result.total}).\n${preview}`;
|
|
259
|
+
}
|
|
260
|
+
if (result.counts_by_source) {
|
|
261
|
+
return result.counts_by_source.map((count) => `${count.source_id}: ${count.count}`).join("\n");
|
|
262
|
+
}
|
|
263
|
+
if (result.histograms) {
|
|
264
|
+
return result.histograms
|
|
265
|
+
.map((histogram) => `${histogram.source_id}: ${histogram.buckets.length} histogram buckets`)
|
|
266
|
+
.join("\n");
|
|
267
|
+
}
|
|
268
|
+
if (result.stats) {
|
|
269
|
+
return result.stats
|
|
270
|
+
.map((entry) => `${entry.source_id} ${entry.field}: p95=${entry.summary.p95 ?? "n/a"}`)
|
|
271
|
+
.join("\n");
|
|
272
|
+
}
|
|
273
|
+
if (result.grouped_hits) {
|
|
274
|
+
return result.grouped_hits
|
|
275
|
+
.map((entry) => `${entry.source_id} grouped top hits: ${entry.buckets.length} buckets`)
|
|
276
|
+
.join("\n");
|
|
277
|
+
}
|
|
278
|
+
return (result.groups ?? [])
|
|
279
|
+
.map((group) => `${group.source_id} grouped by ${group.field}: ${group.buckets.length} buckets`)
|
|
280
|
+
.join("\n");
|
|
281
|
+
}
|
|
282
|
+
export function createQueryCallToolResult(result) {
|
|
283
|
+
return {
|
|
284
|
+
content: [{ type: "text", text: formatQueryResult(result) }],
|
|
285
|
+
structuredContent: result,
|
|
286
|
+
};
|
|
287
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Compatibility Matrix
|
|
3
|
+
status: active
|
|
4
|
+
updated: 2026-04-06
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Compatibility Matrix
|
|
8
|
+
|
|
9
|
+
This matrix distinguishes guaranteed support from best-effort behavior. Each claim includes an evidence basis so readers know what is tested vs inferred.
|
|
10
|
+
|
|
11
|
+
| Area | Status | Evidence basis | Notes |
|
|
12
|
+
| --- | --- | --- | --- |
|
|
13
|
+
| Node.js 22+ runtime | Guaranteed | Verified in CI via `.node-version` | Required for build and runtime. |
|
|
14
|
+
| Repo-local Codex plugin install | Guaranteed | Verified via `npm run verify` and entrypoint checks | Requires `npm install` + `npm run build`. |
|
|
15
|
+
| Fully autonomous plugin install by prompting alone across Codex model variants | Best-effort | Field reports show model-dependent behavior | Manual install from the local marketplace is the supported fallback when the agent cannot complete the UI step itself. |
|
|
16
|
+
| `kibana_internal_search_es` backend | Guaranteed | Verified in tests | Primary search transport. |
|
|
17
|
+
| `elasticsearch_search` backend | Guaranteed | Verified in tests | Direct Elasticsearch-compatible search path. |
|
|
18
|
+
| Schema metadata endpoints | Best-effort | Known 404s in some deployments | `describe_fields` may fail if endpoints are blocked. |
|
|
19
|
+
| Nested query features | Best-effort | Depends on schema availability | Requires schema metadata for validation. |
|
|
20
|
+
| Public package install | Planned | Not yet verified | Enabled after artifact verification and release automation. |
|
|
21
|
+
|
|
22
|
+
Upgrade a row from best-effort to guaranteed only after verifying it in CI or in a reproducible environment test and documenting the evidence here.
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Distribution Strategy
|
|
3
|
+
status: active
|
|
4
|
+
updated: 2026-04-09
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Distribution Strategy
|
|
8
|
+
|
|
9
|
+
## Goals
|
|
10
|
+
|
|
11
|
+
- Make the MCP installable by AI agents without manual explanation.
|
|
12
|
+
- Preserve the repo-local Codex plugin workflow as the guaranteed path.
|
|
13
|
+
- Offer a public distribution path once the artifact boundary is verified.
|
|
14
|
+
|
|
15
|
+
## Supported Distribution Paths
|
|
16
|
+
|
|
17
|
+
### 1. Repo-local Codex plugin (guaranteed)
|
|
18
|
+
|
|
19
|
+
This is the primary, always-supported path:
|
|
20
|
+
|
|
21
|
+
1. Clone the repo.
|
|
22
|
+
2. `npm install`
|
|
23
|
+
3. `npm run build`
|
|
24
|
+
4. Install the `Kibana Log Investigation` plugin from the repo marketplace in Codex.
|
|
25
|
+
|
|
26
|
+
This path is required for development and is the baseline for support.
|
|
27
|
+
|
|
28
|
+
### 2. Public package (planned, gated)
|
|
29
|
+
|
|
30
|
+
The package surface is prepared for agent-friendly execution via:
|
|
31
|
+
|
|
32
|
+
- `npx -y @havesomecode/kibana-mcp-server`
|
|
33
|
+
- MCP clients that invoke the published package binary instead of a repo-local build
|
|
34
|
+
|
|
35
|
+
Public publishing remains gated until the following are complete:
|
|
36
|
+
|
|
37
|
+
- npm package ownership is under maintainer control for the chosen package name
|
|
38
|
+
- Verified `npm pack` contents (runtime entrypoint, plugin metadata, README, LICENSE).
|
|
39
|
+
- CI and release workflows green on the supported Node line.
|
|
40
|
+
- Clear support policy and compatibility matrix published.
|
|
41
|
+
- Trusted publishing enabled (OIDC), no long-lived publish tokens.
|
|
42
|
+
|
|
43
|
+
This path is optional until explicitly enabled.
|
|
44
|
+
|
|
45
|
+
## Artifact Boundary
|
|
46
|
+
|
|
47
|
+
The release artifact must include:
|
|
48
|
+
|
|
49
|
+
- `dist/` runtime build output
|
|
50
|
+
- plugin metadata and MCP config
|
|
51
|
+
- `README.md`, `LICENSE`
|
|
52
|
+
|
|
53
|
+
The release artifact must exclude:
|
|
54
|
+
|
|
55
|
+
- tests and fixtures
|
|
56
|
+
- local operator config (`config/sources.json`, `config/sources.runtime.json`)
|
|
57
|
+
- development-only scripts and tooling that do not affect runtime execution
|
|
58
|
+
|
|
59
|
+
## Versioning
|
|
60
|
+
|
|
61
|
+
Releases use semantic versioning driven by `semantic-release`.
|
|
62
|
+
|
|
63
|
+
The authoritative release record is:
|
|
64
|
+
|
|
65
|
+
- Git tags
|
|
66
|
+
- npm package versions
|
|
67
|
+
- GitHub Releases
|
|
68
|
+
|
|
69
|
+
The repository does not commit generated version bumps or release notes back into git.
|
|
70
|
+
|
|
71
|
+
## Decision Triggers
|
|
72
|
+
|
|
73
|
+
Move from repo-local only to public publishing when:
|
|
74
|
+
|
|
75
|
+
- the repo has an npm package identity maintainers can actually control
|
|
76
|
+
- at least one external adopter confirms successful installation without maintainer intervention
|
|
77
|
+
- artifact verification passes on two consecutive release candidates
|
|
78
|
+
- maintainer agrees to own the support posture defined in `docs/project/support-policy.md`
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: npm Publishing Setup
|
|
3
|
+
status: active
|
|
4
|
+
updated: 2026-04-09
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# npm Publishing Setup
|
|
8
|
+
|
|
9
|
+
This repo is prepared for npm trusted publishing from GitHub Actions, but npm-side setup is still required before the first successful publish.
|
|
10
|
+
|
|
11
|
+
## Package Identity
|
|
12
|
+
|
|
13
|
+
The intended published package is `@havesomecode/kibana-mcp-server`.
|
|
14
|
+
|
|
15
|
+
Before enabling trusted publishing, confirm both of these are true:
|
|
16
|
+
|
|
17
|
+
- the npm scope `@havesomecode` is owned by the maintainer account that will publish this package
|
|
18
|
+
- the package `@havesomecode/kibana-mcp-server` is either unclaimed or already owned by that scope
|
|
19
|
+
|
|
20
|
+
Do not configure trusted publishing against a package or scope you do not control.
|
|
21
|
+
|
|
22
|
+
## Repo-Side Publish Workflow
|
|
23
|
+
|
|
24
|
+
The authoritative workflow file is `.github/workflows/release.yml`.
|
|
25
|
+
|
|
26
|
+
It is responsible for:
|
|
27
|
+
|
|
28
|
+
- running `npm run verify` on `master` pushes before any publish attempt
|
|
29
|
+
- running `semantic-release` on normal `master` pushes
|
|
30
|
+
- publishing to npm automatically when merged history contains a releasable semantic change
|
|
31
|
+
- creating Git tags and GitHub Releases from the same release event
|
|
32
|
+
- using GitHub OIDC instead of npm automation tokens
|
|
33
|
+
|
|
34
|
+
## npm-Side Setup
|
|
35
|
+
|
|
36
|
+
After package ownership is resolved:
|
|
37
|
+
|
|
38
|
+
1. Open the package settings on npmjs.com.
|
|
39
|
+
2. Add a trusted publisher.
|
|
40
|
+
3. Choose GitHub Actions.
|
|
41
|
+
4. Set:
|
|
42
|
+
- Organization or user: `Zacaria`
|
|
43
|
+
- Repository: `kibana-mcp-server`
|
|
44
|
+
- Workflow filename: `release.yml`
|
|
45
|
+
- Environment name: leave blank unless GitHub environments are added later
|
|
46
|
+
5. Save the trusted publisher.
|
|
47
|
+
6. Merge a PR into `master` with a semantic title such as `feat: ...` or `fix: ...`.
|
|
48
|
+
7. After the first successful publish, disable token-based publishing in npm package settings.
|
|
49
|
+
|
|
50
|
+
## Publish Expectations
|
|
51
|
+
|
|
52
|
+
- The workflow requires GitHub-hosted runners.
|
|
53
|
+
- The workflow requires `id-token: write`.
|
|
54
|
+
- The package must remain public for provenance to be generated automatically.
|
|
55
|
+
- The package `repository` field must match this public GitHub repository exactly.
|
|
56
|
+
- The npm package name in `package.json` must stay aligned with the package configured in npm trusted publishers.
|
|
57
|
+
- Squash merge should be enabled so the semantic PR title becomes the final commit title on `master`.
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Release Checklist
|
|
3
|
+
status: active
|
|
4
|
+
updated: 2026-04-09
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Release Checklist
|
|
8
|
+
|
|
9
|
+
## Preconditions
|
|
10
|
+
|
|
11
|
+
- CI is green on `master`.
|
|
12
|
+
- `npm run verify` passes locally.
|
|
13
|
+
- The compatibility matrix is up to date (`docs/project/compatibility-matrix.md`).
|
|
14
|
+
- Any new env or config requirements are documented in `README.md` and `INSTALL.md`.
|
|
15
|
+
- The npm scope and package name (`@havesomecode/kibana-mcp-server`) are owned by the maintainers.
|
|
16
|
+
|
|
17
|
+
## Release Authorization
|
|
18
|
+
|
|
19
|
+
- Releases are created via the `Release` GitHub Actions workflow.
|
|
20
|
+
- Trusted publishing for npm must point at workflow file `release.yml`.
|
|
21
|
+
- Tags and GitHub Releases are created by `semantic-release` after a verified push to `master`.
|
|
22
|
+
- PRs that should affect semver must use a Conventional Commit title because squash merge will carry that title onto `master`.
|
|
23
|
+
- Only maintainers listed in `CODEOWNERS` should have permission to trigger releases.
|
|
24
|
+
|
|
25
|
+
## Security Posture for CI/Release
|
|
26
|
+
|
|
27
|
+
- Workflow permissions must be minimal (`contents: read` for CI, `contents: write` + `id-token: write` for publish).
|
|
28
|
+
- Publishing to npm must use OIDC trusted publishing.
|
|
29
|
+
- Do not store long-lived npm tokens in repo secrets.
|
|
30
|
+
- After trusted publishing is verified, disallow token-based publishing in npm package settings.
|
|
31
|
+
|
|
32
|
+
## Threat Model (Top 3)
|
|
33
|
+
|
|
34
|
+
1. **Compromised GitHub Actions workflow**: mitigate with minimal permissions, required reviews, and avoiding untrusted actions.
|
|
35
|
+
2. **Malicious dependency update**: mitigate with dependency review workflow and human review of automated PRs.
|
|
36
|
+
3. **Accidental secrets leakage in artifacts**: mitigate with `verify-packlist` and explicit exclusion of local config files.
|
|
37
|
+
|
|
38
|
+
## Steps
|
|
39
|
+
|
|
40
|
+
1. Open a PR with a semantic title such as `feat: ...`, `fix: ...`, or `feat!: ...`.
|
|
41
|
+
2. Run `npm run verify`.
|
|
42
|
+
3. Confirm the npm package identity is publishable by this maintainer account.
|
|
43
|
+
4. Configure npm trusted publishing for `release.yml`.
|
|
44
|
+
5. Merge with squash merge into `master`.
|
|
45
|
+
6. Confirm the `Release` workflow either exits with no release or publishes a new version.
|
|
46
|
+
7. Validate the Git tag, GitHub Release notes, published package contents, and provenance.
|
|
47
|
+
|
|
48
|
+
## Post-Release
|
|
49
|
+
|
|
50
|
+
- Confirm install instructions still match the shipped artifact.
|
|
51
|
+
- Update any compatibility claims that changed in the release.
|