@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,261 @@
1
+ import { spawn } from "node:child_process";
2
+ const DEFAULT_SERVICE_NAME = "com.zacaria.kibana-mcp-server";
3
+ export class SecretStoreError extends Error {
4
+ code;
5
+ constructor(message, code) {
6
+ super(message);
7
+ this.code = code;
8
+ this.name = "SecretStoreError";
9
+ }
10
+ }
11
+ export function createSecretStore(platform = process.platform, runner = runCommand, serviceName = DEFAULT_SERVICE_NAME) {
12
+ switch (platform) {
13
+ case "darwin":
14
+ return new MacOsSecretStore(runner, serviceName);
15
+ case "win32":
16
+ return new WindowsSecretStore(runner, serviceName);
17
+ default:
18
+ return new LinuxSecretStore(runner, serviceName);
19
+ }
20
+ }
21
+ class MacOsSecretStore {
22
+ runner;
23
+ serviceName;
24
+ constructor(runner, serviceName) {
25
+ this.runner = runner;
26
+ this.serviceName = serviceName;
27
+ }
28
+ async load(profileId) {
29
+ const result = await this.execute("find-generic-password", [
30
+ "-s",
31
+ this.serviceName,
32
+ "-a",
33
+ profileId,
34
+ "-w",
35
+ ]);
36
+ return parseStoredSecret(result.stdout);
37
+ }
38
+ async save(profileId, secret) {
39
+ await this.execute("add-generic-password", [
40
+ "-U",
41
+ "-s",
42
+ this.serviceName,
43
+ "-a",
44
+ profileId,
45
+ "-w",
46
+ serializeSecret(secret),
47
+ ]);
48
+ }
49
+ async delete(profileId) {
50
+ try {
51
+ await this.execute("delete-generic-password", ["-s", this.serviceName, "-a", profileId]);
52
+ }
53
+ catch (error) {
54
+ if (error instanceof SecretStoreError && error.code === "NOT_FOUND") {
55
+ return;
56
+ }
57
+ throw error;
58
+ }
59
+ }
60
+ async execute(subcommand, args) {
61
+ try {
62
+ const result = await this.runner("security", [subcommand, ...args]);
63
+ if (result.exitCode === 44) {
64
+ throw new SecretStoreError(`No saved credentials found for profile '${args.at(-1)}'.`, "NOT_FOUND");
65
+ }
66
+ if (result.exitCode !== 0) {
67
+ throw new SecretStoreError(result.stderr.trim() || "macOS Keychain command failed.", "OPERATION_FAILED");
68
+ }
69
+ return result;
70
+ }
71
+ catch (error) {
72
+ throw normalizeRunnerError(error, "macOS Keychain is unavailable.");
73
+ }
74
+ }
75
+ }
76
+ class LinuxSecretStore {
77
+ runner;
78
+ serviceName;
79
+ constructor(runner, serviceName) {
80
+ this.runner = runner;
81
+ this.serviceName = serviceName;
82
+ }
83
+ async load(profileId) {
84
+ const result = await this.execute("lookup", this.attributes(profileId));
85
+ const value = result.stdout.trim();
86
+ if (!value) {
87
+ throw new SecretStoreError(`No saved credentials found for profile '${profileId}'.`, "NOT_FOUND");
88
+ }
89
+ return parseStoredSecret(value);
90
+ }
91
+ async save(profileId, secret) {
92
+ await this.execute("store", ["--label", `Kibana MCP (${profileId})`, ...this.attributes(profileId)], serializeSecret(secret));
93
+ }
94
+ async delete(profileId) {
95
+ try {
96
+ await this.execute("clear", this.attributes(profileId));
97
+ }
98
+ catch (error) {
99
+ if (error instanceof SecretStoreError && error.code === "NOT_FOUND") {
100
+ return;
101
+ }
102
+ throw error;
103
+ }
104
+ }
105
+ attributes(profileId) {
106
+ return ["service", this.serviceName, "profile", profileId];
107
+ }
108
+ async execute(command, args, stdin) {
109
+ try {
110
+ const result = await this.runner("secret-tool", [command, ...args], { stdin });
111
+ if (result.exitCode !== 0) {
112
+ const stderr = result.stderr.trim().toLowerCase();
113
+ if (stderr.includes("not found")) {
114
+ throw new SecretStoreError(`No saved credentials found for profile '${args.at(-1) ?? "unknown"}'.`, "NOT_FOUND");
115
+ }
116
+ throw new SecretStoreError(result.stderr.trim() || "Secret Service command failed.", "OPERATION_FAILED");
117
+ }
118
+ return result;
119
+ }
120
+ catch (error) {
121
+ throw normalizeRunnerError(error, "Linux Secret Service is unavailable. Install and unlock a Secret Service-compatible keyring first.");
122
+ }
123
+ }
124
+ }
125
+ class WindowsSecretStore {
126
+ runner;
127
+ serviceName;
128
+ constructor(runner, serviceName) {
129
+ this.runner = runner;
130
+ this.serviceName = serviceName;
131
+ }
132
+ async load(profileId) {
133
+ const result = await this.execute(`
134
+ [void][Windows.Security.Credentials.PasswordVault,Windows.Security.Credentials,ContentType=WindowsRuntime]
135
+ $vault = New-Object Windows.Security.Credentials.PasswordVault
136
+ try {
137
+ $credential = $vault.Retrieve($args[0], $args[1])
138
+ $credential.RetrievePassword()
139
+ [Console]::Out.Write($credential.Password)
140
+ exit 0
141
+ } catch {
142
+ exit 3
143
+ }
144
+ `, [this.serviceName, profileId]);
145
+ return parseStoredSecret(result.stdout);
146
+ }
147
+ async save(profileId, secret) {
148
+ await this.execute(`
149
+ [void][Windows.Security.Credentials.PasswordVault,Windows.Security.Credentials,ContentType=WindowsRuntime]
150
+ $vault = New-Object Windows.Security.Credentials.PasswordVault
151
+ try {
152
+ $existing = $vault.Retrieve($args[0], $args[1])
153
+ $vault.Remove($existing)
154
+ } catch {}
155
+ $credential = New-Object Windows.Security.Credentials.PasswordCredential($args[0], $args[1], $args[2])
156
+ $vault.Add($credential)
157
+ `, [this.serviceName, profileId, serializeSecret(secret)]);
158
+ }
159
+ async delete(profileId) {
160
+ try {
161
+ await this.execute(`
162
+ [void][Windows.Security.Credentials.PasswordVault,Windows.Security.Credentials,ContentType=WindowsRuntime]
163
+ $vault = New-Object Windows.Security.Credentials.PasswordVault
164
+ try {
165
+ $credential = $vault.Retrieve($args[0], $args[1])
166
+ $vault.Remove($credential)
167
+ exit 0
168
+ } catch {
169
+ exit 3
170
+ }
171
+ `, [this.serviceName, profileId]);
172
+ }
173
+ catch (error) {
174
+ if (error instanceof SecretStoreError && error.code === "NOT_FOUND") {
175
+ return;
176
+ }
177
+ throw error;
178
+ }
179
+ }
180
+ async execute(script, args) {
181
+ try {
182
+ const result = await this.runner("powershell.exe", [
183
+ "-NoProfile",
184
+ "-NonInteractive",
185
+ "-Command",
186
+ script.trim(),
187
+ ...args,
188
+ ]);
189
+ if (result.exitCode === 3) {
190
+ throw new SecretStoreError(`No saved credentials found for profile '${args[1]}'.`, "NOT_FOUND");
191
+ }
192
+ if (result.exitCode !== 0) {
193
+ throw new SecretStoreError(result.stderr.trim() || "Windows Credential Manager command failed.", "OPERATION_FAILED");
194
+ }
195
+ return result;
196
+ }
197
+ catch (error) {
198
+ throw normalizeRunnerError(error, "Windows Credential Manager is unavailable.");
199
+ }
200
+ }
201
+ }
202
+ function serializeSecret(secret) {
203
+ return JSON.stringify(secret);
204
+ }
205
+ function parseStoredSecret(serializedSecret) {
206
+ const parsed = JSON.parse(serializedSecret);
207
+ if (!parsed ||
208
+ typeof parsed !== "object" ||
209
+ !("username" in parsed) ||
210
+ !("password" in parsed) ||
211
+ typeof parsed.username !== "string" ||
212
+ typeof parsed.password !== "string") {
213
+ throw new SecretStoreError("Stored credentials are invalid.", "OPERATION_FAILED");
214
+ }
215
+ return {
216
+ username: parsed.username,
217
+ password: parsed.password,
218
+ };
219
+ }
220
+ function normalizeRunnerError(error, unavailableMessage) {
221
+ if (error instanceof SecretStoreError) {
222
+ return error;
223
+ }
224
+ if (error &&
225
+ typeof error === "object" &&
226
+ "code" in error &&
227
+ typeof error.code === "string" &&
228
+ error.code === "ENOENT") {
229
+ return new SecretStoreError(unavailableMessage, "UNAVAILABLE");
230
+ }
231
+ return new SecretStoreError(error instanceof Error ? error.message : "Credential-store command failed.", "OPERATION_FAILED");
232
+ }
233
+ async function runCommand(command, args, options = {}) {
234
+ return await new Promise((resolve, reject) => {
235
+ const child = spawn(command, args, {
236
+ stdio: "pipe",
237
+ });
238
+ let stdout = "";
239
+ let stderr = "";
240
+ child.stdout.setEncoding("utf8");
241
+ child.stderr.setEncoding("utf8");
242
+ child.stdout.on("data", (chunk) => {
243
+ stdout += chunk;
244
+ });
245
+ child.stderr.on("data", (chunk) => {
246
+ stderr += chunk;
247
+ });
248
+ child.on("error", reject);
249
+ child.on("close", (exitCode) => {
250
+ resolve({
251
+ stdout,
252
+ stderr,
253
+ exitCode: exitCode ?? 1,
254
+ });
255
+ });
256
+ if (options.stdin) {
257
+ child.stdin.write(options.stdin);
258
+ }
259
+ child.stdin.end();
260
+ });
261
+ }
@@ -0,0 +1,87 @@
1
+ import { McpServer } from "@modelcontextprotocol/server";
2
+ import { KibanaClient } from "./kibana_client.js";
3
+ import { SchemaCatalog } from "./schema_catalog.js";
4
+ import { SourceCatalog } from "./source_catalog.js";
5
+ import { configureInputSchema, configureOutputSchema, createConfigureCallToolResult, executeConfigure, } from "./tools/configure.js";
6
+ import { createDescribeFieldsCallToolResult, describeFieldsInputSchema, describeFieldsOutputSchema, executeDescribeFields, } from "./tools/describe_fields.js";
7
+ import { createDiscoverCallToolResult, discoverInputSchema, discoverOutputSchema, executeDiscover, } from "./tools/discover.js";
8
+ import { createFilterCallToolResult, executeFilter, filterInputSchema, filterOutputSchema, } from "./tools/filter.js";
9
+ import { createQueryCallToolResult, executeQuery, queryInputSchema, queryOutputSchema, } from "./tools/query.js";
10
+ export function createApplication(initialConfig, dependencies) {
11
+ const server = new McpServer({
12
+ name: "kibana-log-investigation",
13
+ version: "0.1.0",
14
+ });
15
+ const kibanaClientFactory = dependencies?.kibanaClientFactory ??
16
+ ((config) => new KibanaClient(config));
17
+ let activeConfig = initialConfig;
18
+ let sourceCatalog = activeConfig ? new SourceCatalog(activeConfig.sources) : null;
19
+ let kibanaClient = activeConfig
20
+ ? (dependencies?.kibanaClient ?? kibanaClientFactory(activeConfig.kibana))
21
+ : null;
22
+ let schemaCatalog = kibanaClient ? new SchemaCatalog(kibanaClient) : null;
23
+ function requireConfigured() {
24
+ if (!sourceCatalog || !kibanaClient || !schemaCatalog) {
25
+ throw new Error("Server is not configured. Call the 'configure' tool first.");
26
+ }
27
+ return { sourceCatalog, kibanaClient, schemaCatalog };
28
+ }
29
+ const configureHandler = async (input) => {
30
+ const { nextConfig, result } = await executeConfigure(configureInputSchema.parse(input), {
31
+ sourceCatalogPath: activeConfig?.sourceCatalogPath,
32
+ });
33
+ activeConfig = {
34
+ ...nextConfig,
35
+ sourceCatalogPath: result.source_catalog_path,
36
+ profileName: activeConfig?.profileName,
37
+ sourceCatalogOrigin: activeConfig?.sourceCatalogOrigin,
38
+ };
39
+ sourceCatalog = new SourceCatalog(nextConfig.sources);
40
+ kibanaClient = kibanaClientFactory(nextConfig.kibana);
41
+ schemaCatalog = new SchemaCatalog(kibanaClient);
42
+ return result;
43
+ };
44
+ const describeFieldsHandler = (input) => executeDescribeFields(describeFieldsInputSchema.parse(input), requireConfigured().sourceCatalog, requireConfigured().schemaCatalog);
45
+ const discoverHandler = (input) => executeDiscover(discoverInputSchema.parse(input), requireConfigured().sourceCatalog);
46
+ const filterHandler = (input) => executeFilter(filterInputSchema.parse(input), requireConfigured().sourceCatalog, requireConfigured().kibanaClient, {
47
+ schemaCatalog: requireConfigured().schemaCatalog,
48
+ });
49
+ const queryHandler = (input) => executeQuery(queryInputSchema.parse(input), requireConfigured().sourceCatalog, requireConfigured().kibanaClient, {
50
+ schemaCatalog: requireConfigured().schemaCatalog,
51
+ });
52
+ server.registerTool("configure", {
53
+ description: "Configure the Kibana connection and logical source catalog for this server session.",
54
+ inputSchema: configureInputSchema,
55
+ outputSchema: configureOutputSchema,
56
+ }, async (input) => createConfigureCallToolResult(await configureHandler(input)));
57
+ server.registerTool("describe_fields", {
58
+ description: "Describe the effective field capabilities for a configured logical source.",
59
+ inputSchema: describeFieldsInputSchema,
60
+ outputSchema: describeFieldsOutputSchema,
61
+ }, async (input) => createDescribeFieldsCallToolResult(await describeFieldsHandler(input)));
62
+ server.registerTool("discover", {
63
+ description: "List configured logical log sources and field hints for investigation work.",
64
+ inputSchema: discoverInputSchema,
65
+ outputSchema: discoverOutputSchema,
66
+ }, async (input) => createDiscoverCallToolResult(discoverHandler(input)));
67
+ server.registerTool("filter", {
68
+ description: "Run an exact-field filter when the field name is already known, bypassing alias resolution.",
69
+ inputSchema: filterInputSchema,
70
+ outputSchema: filterOutputSchema,
71
+ }, async (input) => createFilterCallToolResult(await filterHandler(input)));
72
+ server.registerTool("query", {
73
+ description: "Query one or more logical log sources over an absolute time window with text, filters, and aggregate modes.",
74
+ inputSchema: queryInputSchema,
75
+ outputSchema: queryOutputSchema,
76
+ }, async (input) => createQueryCallToolResult(await queryHandler(input)));
77
+ return {
78
+ server,
79
+ handlers: {
80
+ configure: configureHandler,
81
+ describe_fields: describeFieldsHandler,
82
+ discover: discoverHandler,
83
+ filter: filterHandler,
84
+ query: queryHandler,
85
+ },
86
+ };
87
+ }
@@ -0,0 +1,160 @@
1
+ import { access, mkdir, readFile, writeFile } from "node:fs/promises";
2
+ import { dirname, join, resolve } from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ import { sourceCatalogSchema } from "./config.js";
5
+ import { resolveProfilePaths } from "./profile_paths.js";
6
+ import { ProfileStore, deriveProfileId } from "./profile_store.js";
7
+ import { SecretStoreError, createSecretStore } from "./secret_store.js";
8
+ const DEFAULT_TIMEOUT_MS = 10000;
9
+ const KNOWN_ENDPOINT_PATHS = [
10
+ "/internal/search/es",
11
+ "/api/data_views/fields_for_wildcard",
12
+ "/api/index_patterns/_fields_for_wildcard",
13
+ ];
14
+ export async function runSetupFlow(prompter, dependencies = {}) {
15
+ const paths = dependencies.paths ?? resolveProfilePaths();
16
+ const profileStore = dependencies.profileStore ?? new ProfileStore(paths);
17
+ const secretStore = dependencies.secretStore ?? createSecretStore();
18
+ const readFileImpl = dependencies.readFile ?? readFile;
19
+ const writeFileImpl = dependencies.writeFile ?? writeFile;
20
+ const mkdirImpl = dependencies.mkdir ?? mkdir;
21
+ const cwd = dependencies.cwd ?? process.cwd();
22
+ const savedProfileNames = [];
23
+ const savedCatalogPaths = [];
24
+ await prompter.info("Guided setup will save one machine-level Kibana profile and reuse it in later threads.");
25
+ let continueAddingProfiles = true;
26
+ while (continueAddingProfiles) {
27
+ const existingProfiles = await profileStore.listProfiles();
28
+ const profileName = await promptForProfileName(prompter, existingProfiles.map((profile) => profile.name));
29
+ const existingProfile = existingProfiles.find((profile) => profile.name.toLowerCase() === profileName.toLowerCase());
30
+ const profileId = existingProfile?.id ??
31
+ deriveProfileId(profileName, existingProfiles.map((profile) => profile.id));
32
+ const baseUrl = await promptForBaseUrl(prompter);
33
+ const username = await promptForRequiredValue(prompter, "Kibana username");
34
+ const password = await promptForRequiredValue(prompter, "Kibana password", { secret: true });
35
+ const importPath = await promptForSourceCatalogPath(prompter, cwd);
36
+ const importedCatalogPath = join(paths.sourceCatalogsDir, `${profileId}.json`);
37
+ await copyValidatedSourceCatalog(importPath, importedCatalogPath, {
38
+ readFile: readFileImpl,
39
+ writeFile: writeFileImpl,
40
+ mkdir: mkdirImpl,
41
+ });
42
+ try {
43
+ await secretStore.save(profileId, { username, password });
44
+ }
45
+ catch (error) {
46
+ if (error instanceof SecretStoreError) {
47
+ throw new Error(`Could not save Kibana credentials securely: ${error.message}`);
48
+ }
49
+ throw error;
50
+ }
51
+ const profile = await profileStore.upsertProfile({
52
+ id: profileId,
53
+ name: profileName,
54
+ baseUrl,
55
+ timeoutMs: DEFAULT_TIMEOUT_MS,
56
+ sourceCatalogPath: importedCatalogPath,
57
+ }, {
58
+ makeDefault: existingProfiles.length === 0,
59
+ });
60
+ if (existingProfiles.length > 0) {
61
+ const makeDefault = await prompter.confirm(`Make '${profile.name}' the default environment for new threads?`, false);
62
+ if (makeDefault) {
63
+ await profileStore.setDefaultProfile(profile.id);
64
+ }
65
+ }
66
+ savedProfileNames.push(profile.name);
67
+ savedCatalogPaths.push(importedCatalogPath);
68
+ continueAddingProfiles = await prompter.confirm("Add another environment now?", false);
69
+ }
70
+ const defaultProfile = await profileStore.getDefaultProfile();
71
+ if (!defaultProfile) {
72
+ throw new Error("Setup completed without a saved default profile.");
73
+ }
74
+ return {
75
+ defaultProfileName: defaultProfile.name,
76
+ profiles: savedProfileNames,
77
+ sourceCatalogPaths: savedCatalogPaths,
78
+ };
79
+ }
80
+ async function promptForProfileName(prompter, existingNames) {
81
+ while (true) {
82
+ const value = (await prompter.prompt("Environment name", { defaultValue: "default" })).trim();
83
+ if (!value) {
84
+ continue;
85
+ }
86
+ if (!/^[A-Za-z0-9][A-Za-z0-9 _-]*$/.test(value)) {
87
+ await prompter.info("Use letters, numbers, spaces, dashes, or underscores for the environment name.");
88
+ continue;
89
+ }
90
+ if (existingNames.some((candidate) => candidate.toLowerCase() === value.toLowerCase())) {
91
+ const overwrite = await prompter.confirm(`An environment named '${value}' already exists. Update it with new credentials and sources?`, true);
92
+ if (!overwrite) {
93
+ continue;
94
+ }
95
+ }
96
+ return value;
97
+ }
98
+ }
99
+ async function promptForBaseUrl(prompter) {
100
+ while (true) {
101
+ const rawValue = (await prompter.prompt("Kibana base URL")).trim();
102
+ try {
103
+ const parsed = new URL(rawValue);
104
+ const normalizedBaseUrl = parsed.toString().replace(/\/+$/, "");
105
+ if (KNOWN_ENDPOINT_PATHS.some((path) => parsed.pathname.includes(path))) {
106
+ await prompter.info("Enter the Kibana base URL only. Do not include endpoint paths such as /internal/search/es.");
107
+ continue;
108
+ }
109
+ return normalizedBaseUrl;
110
+ }
111
+ catch {
112
+ await prompter.info("Enter a valid URL such as https://kibana.example.com or https://gateway.example.com/logs.");
113
+ }
114
+ }
115
+ }
116
+ async function promptForRequiredValue(prompter, label, options = {}) {
117
+ while (true) {
118
+ const value = (await prompter.prompt(label, options)).trim();
119
+ if (value) {
120
+ return value;
121
+ }
122
+ await prompter.info(`${label} is required.`);
123
+ }
124
+ }
125
+ async function promptForSourceCatalogPath(prompter, cwd) {
126
+ const bundledCatalogPath = await resolveBundledSourceCatalogPath();
127
+ while (true) {
128
+ const rawValue = (await prompter.prompt("Source catalog path to import", {
129
+ defaultValue: bundledCatalogPath,
130
+ })).trim();
131
+ const importPath = rawValue ? resolve(cwd, rawValue) : bundledCatalogPath;
132
+ try {
133
+ await access(importPath);
134
+ return importPath;
135
+ }
136
+ catch {
137
+ await prompter.info(`Source catalog not found at ${importPath}. Use a JSON file or accept the bundled example.`);
138
+ }
139
+ }
140
+ }
141
+ async function copyValidatedSourceCatalog(importPath, destinationPath, io) {
142
+ const rawCatalog = await io.readFile(importPath, "utf8");
143
+ const parsedCatalog = sourceCatalogSchema.parse(JSON.parse(rawCatalog));
144
+ await io.mkdir(dirname(destinationPath), { recursive: true });
145
+ await io.writeFile(destinationPath, `${JSON.stringify(parsedCatalog, null, 2)}\n`, "utf8");
146
+ }
147
+ async function resolveBundledSourceCatalogPath() {
148
+ const candidates = [
149
+ new URL("../config/sources.example.json", import.meta.url),
150
+ new URL("../../config/sources.example.json", import.meta.url),
151
+ ].map((candidate) => fileURLToPath(candidate));
152
+ for (const candidate of candidates) {
153
+ try {
154
+ await access(candidate);
155
+ return candidate;
156
+ }
157
+ catch { }
158
+ }
159
+ throw new Error("Bundled source catalog config/sources.example.json could not be found.");
160
+ }
@@ -0,0 +1,47 @@
1
+ export class SourceCatalog {
2
+ sources;
3
+ sourceMap;
4
+ constructor(sources) {
5
+ this.sources = sources;
6
+ this.sourceMap = new Map(sources.map((source) => [source.id, source]));
7
+ }
8
+ list(query, limit = 20) {
9
+ const normalizedQuery = query?.trim().toLowerCase();
10
+ const filtered = normalizedQuery
11
+ ? this.sources.filter((source) => this.matchesQuery(source, normalizedQuery))
12
+ : this.sources;
13
+ return filtered.slice(0, limit).map((source) => ({
14
+ id: source.id,
15
+ name: source.name,
16
+ description: source.description,
17
+ tags: source.tags,
18
+ time_field: source.timeField,
19
+ field_hints: source.fieldHints.map((fieldHint) => ({
20
+ ...fieldHint,
21
+ aliases: fieldHint.aliases ?? [],
22
+ })),
23
+ }));
24
+ }
25
+ getRequiredSources(sourceIds) {
26
+ const resolved = sourceIds.map((sourceId) => this.sourceMap.get(sourceId));
27
+ const missing = sourceIds.filter((sourceId, index) => !resolved[index]);
28
+ if (missing.length > 0) {
29
+ throw new Error(`Unknown source ids: ${missing.join(", ")}`);
30
+ }
31
+ return resolved;
32
+ }
33
+ matchesQuery(source, query) {
34
+ const haystacks = [
35
+ source.id,
36
+ source.name,
37
+ source.description ?? "",
38
+ ...source.tags,
39
+ ...source.fieldHints.flatMap((fieldHint) => [
40
+ fieldHint.name,
41
+ fieldHint.description ?? "",
42
+ ...(fieldHint.aliases ?? []),
43
+ ]),
44
+ ];
45
+ return haystacks.some((value) => value.toLowerCase().includes(query));
46
+ }
47
+ }
@@ -0,0 +1,53 @@
1
+ import { z } from "zod";
2
+ import { persistSourceCatalog, sourceDefinitionSchema } from "../config.js";
3
+ export const configureInputSchema = z.object({
4
+ kibana: z.object({
5
+ baseUrl: z.url(),
6
+ username: z.string().min(1),
7
+ password: z.string().min(1),
8
+ timeoutMs: z.number().int().positive().max(120000).default(10000),
9
+ }),
10
+ sources: z.array(sourceDefinitionSchema).min(1),
11
+ });
12
+ export const configureOutputSchema = z.object({
13
+ configured: z.literal(true),
14
+ persisted: z.literal(true),
15
+ source_catalog_path: z.string(),
16
+ source_count: z.number().int().positive(),
17
+ source_ids: z.array(z.string()),
18
+ base_url: z.string(),
19
+ });
20
+ export async function executeConfigure(input, options = {}) {
21
+ const nextConfig = {
22
+ kibana: {
23
+ baseUrl: input.kibana.baseUrl.replace(/\/+$/, ""),
24
+ username: input.kibana.username,
25
+ password: input.kibana.password,
26
+ timeoutMs: input.kibana.timeoutMs,
27
+ },
28
+ sources: input.sources,
29
+ };
30
+ const sourceCatalogPath = await persistSourceCatalog(input.sources, options);
31
+ return {
32
+ nextConfig,
33
+ result: {
34
+ configured: true,
35
+ persisted: true,
36
+ source_catalog_path: sourceCatalogPath,
37
+ source_count: input.sources.length,
38
+ source_ids: input.sources.map((source) => source.id),
39
+ base_url: nextConfig.kibana.baseUrl,
40
+ },
41
+ };
42
+ }
43
+ export function createConfigureCallToolResult(result) {
44
+ return {
45
+ content: [
46
+ {
47
+ type: "text",
48
+ text: `Configured Kibana connection for ${result.base_url} with ${result.source_count} sources. Source catalog persisted to ${result.source_catalog_path}.`,
49
+ },
50
+ ],
51
+ structuredContent: result,
52
+ };
53
+ }
@@ -0,0 +1,55 @@
1
+ import { z } from "zod";
2
+ export const describeFieldsInputSchema = z.object({
3
+ source_id: z.string().min(1),
4
+ query: z.string().trim().min(1).optional(),
5
+ limit: z.number().int().positive().max(500).default(100),
6
+ });
7
+ export const describeFieldsOutputSchema = z.object({
8
+ source_id: z.string(),
9
+ total: z.number().int().nonnegative(),
10
+ fields: z.array(z.object({
11
+ name: z.string(),
12
+ type: z.string().optional(),
13
+ description: z.string().optional(),
14
+ aliases: z.array(z.string()).optional(),
15
+ searchable: z.boolean().optional(),
16
+ aggregatable: z.boolean().optional(),
17
+ subfields: z.array(z.string()),
18
+ nested_path: z.string().optional(),
19
+ object_array_path: z.string().optional(),
20
+ multi_field_parent: z.string().optional(),
21
+ preferred_exact_field: z.string().optional(),
22
+ })),
23
+ });
24
+ export async function executeDescribeFields(input, sourceCatalog, schemaCatalog) {
25
+ const [source] = sourceCatalog.getRequiredSources([input.source_id]);
26
+ const fields = await schemaCatalog.getFields(source);
27
+ const filteredFields = schemaCatalog.filterFields(fields, input.query, input.limit);
28
+ return {
29
+ source_id: source.id,
30
+ total: filteredFields.length,
31
+ fields: filteredFields,
32
+ };
33
+ }
34
+ export function formatDescribeFieldsResult(result) {
35
+ const preview = result.fields
36
+ .slice(0, 10)
37
+ .map((field) => {
38
+ const traits = [
39
+ field.type,
40
+ field.searchable === true ? "searchable" : undefined,
41
+ field.aggregatable === true ? "aggregatable" : undefined,
42
+ field.object_array_path ? `object_array=${field.object_array_path}` : undefined,
43
+ field.preferred_exact_field ? `exact=${field.preferred_exact_field}` : undefined,
44
+ ].filter(Boolean);
45
+ return `${field.name}${traits.length > 0 ? ` (${traits.join(", ")})` : ""}`;
46
+ })
47
+ .join("\n");
48
+ return `Described ${result.total} fields for ${result.source_id}.\n${preview}`;
49
+ }
50
+ export function createDescribeFieldsCallToolResult(result) {
51
+ return {
52
+ content: [{ type: "text", text: formatDescribeFieldsResult(result) }],
53
+ structuredContent: result,
54
+ };
55
+ }