@havesomecode/kibana-mcp-server 1.3.0 → 1.4.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 (86) hide show
  1. package/README.md +54 -490
  2. package/dist/src/bin/kibana-mcp-server.d.ts +2 -0
  3. package/dist/src/bin/kibana-mcp-server.js +2 -53
  4. package/dist/src/bin/run_cli.d.ts +4 -0
  5. package/dist/src/bin/run_cli.js +49 -0
  6. package/dist/src/commands/context.d.ts +18 -0
  7. package/dist/src/commands/serve.d.ts +4 -0
  8. package/dist/src/commands/serve.js +30 -6
  9. package/dist/src/commands/setup.d.ts +3 -0
  10. package/dist/src/commands/setup.js +2 -2
  11. package/dist/src/config/config_file.d.ts +59 -0
  12. package/dist/src/config/config_file.js +76 -0
  13. package/dist/src/config/environment.d.ts +100 -0
  14. package/dist/src/{config.js → config/environment.js} +26 -24
  15. package/dist/src/config/startup.d.ts +31 -0
  16. package/dist/src/config/startup.js +86 -0
  17. package/dist/src/kibana/client.d.ts +28 -0
  18. package/dist/src/kibana/schema_catalog.d.ts +11 -0
  19. package/dist/src/{schema_catalog.js → kibana/schema_catalog.js} +33 -11
  20. package/dist/src/kibana/source_catalog.d.ts +10 -0
  21. package/dist/src/mcp_entry.d.ts +2 -0
  22. package/dist/src/mcp_entry.js +1 -1
  23. package/dist/src/profiles/context.d.ts +4 -0
  24. package/dist/src/profiles/context.js +15 -0
  25. package/dist/src/profiles/names.d.ts +10 -0
  26. package/dist/src/profiles/names.js +35 -0
  27. package/dist/src/profiles/paths.d.ts +9 -0
  28. package/dist/src/profiles/runtime.d.ts +43 -0
  29. package/dist/src/profiles/runtime.js +150 -0
  30. package/dist/src/profiles/secret_store.d.ts +20 -0
  31. package/dist/src/profiles/state_lock.d.ts +13 -0
  32. package/dist/src/{state_lock.js → profiles/state_lock.js} +14 -5
  33. package/dist/src/profiles/store.d.ts +34 -0
  34. package/dist/src/{profile_store.js → profiles/store.js} +7 -5
  35. package/dist/src/query/compiler.d.ts +9 -0
  36. package/dist/src/query/cursor.d.ts +8 -0
  37. package/dist/src/query/normalize.d.ts +2 -0
  38. package/dist/src/server.d.ts +29 -0
  39. package/dist/src/server.js +40 -50
  40. package/dist/src/setup/bootstrap.d.ts +50 -0
  41. package/dist/src/{bootstrap.js → setup/bootstrap.js} +10 -36
  42. package/dist/src/setup/client_registration.d.ts +38 -0
  43. package/dist/src/setup/setup_flow.d.ts +26 -0
  44. package/dist/src/{setup_flow.js → setup/setup_flow.js} +6 -6
  45. package/dist/src/stores/catalog_store.d.ts +38 -0
  46. package/dist/src/stores/catalog_store.js +22 -0
  47. package/dist/src/stores/file.d.ts +5 -0
  48. package/dist/src/stores/file.js +48 -0
  49. package/dist/src/stores/ioredis.d.ts +21 -0
  50. package/dist/src/stores/ioredis.js +33 -0
  51. package/dist/src/stores/mongodb.d.ts +42 -0
  52. package/dist/src/stores/mongodb.js +57 -0
  53. package/dist/src/stores/postgres.d.ts +24 -0
  54. package/dist/src/stores/postgres.js +65 -0
  55. package/dist/src/stores/redis.d.ts +24 -0
  56. package/dist/src/stores/redis.js +31 -0
  57. package/dist/src/stores/shared.d.ts +25 -0
  58. package/dist/src/stores/shared.js +88 -0
  59. package/dist/src/stores/sqlite.d.ts +26 -0
  60. package/dist/src/stores/sqlite.js +63 -0
  61. package/dist/src/tools/configure.d.ts +59 -0
  62. package/dist/src/tools/configure.js +1 -1
  63. package/dist/src/tools/configure_index.d.ts +35 -0
  64. package/dist/src/tools/configure_index.js +75 -66
  65. package/dist/src/tools/describe_fields.d.ts +29 -0
  66. package/dist/src/tools/discover.d.ts +27 -0
  67. package/dist/src/tools/filter.d.ts +180 -0
  68. package/dist/src/tools/query.d.ts +181 -0
  69. package/dist/src/transports/http.d.ts +15 -0
  70. package/dist/src/transports/http.js +291 -0
  71. package/dist/src/transports/stdio.d.ts +1 -0
  72. package/dist/src/transports/stdio.js +46 -0
  73. package/dist/src/types.d.ts +254 -0
  74. package/dist/src/utils/cli_io.d.ts +11 -0
  75. package/dist/src/utils/cli_prompts.d.ts +9 -0
  76. package/dist/src/utils/cli_ui.d.ts +6 -0
  77. package/dist/src/utils/fs.d.ts +3 -0
  78. package/dist/src/utils/fs.js +30 -0
  79. package/package.json +58 -1
  80. package/skills/kibana-log-investigation/SKILL.md +36 -5
  81. package/dist/src/mcp_runtime.js +0 -23
  82. /package/dist/src/{kibana_client.js → kibana/client.js} +0 -0
  83. /package/dist/src/{source_catalog.js → kibana/source_catalog.js} +0 -0
  84. /package/dist/src/{profile_paths.js → profiles/paths.js} +0 -0
  85. /package/dist/src/{secret_store.js → profiles/secret_store.js} +0 -0
  86. /package/dist/src/{client_registration.js → setup/client_registration.js} +0 -0
@@ -0,0 +1,65 @@
1
+ import { CatalogConflictError } from "./catalog_store.js";
2
+ import { DEFAULT_TABLE_NAME, describeStoreUrl, importDriver, quoteSqlIdentifier, resolveStoreUrl, } from "./shared.js";
3
+ /**
4
+ * Catalog store backed by PostgreSQL through the `pg` package: one row per profile.
5
+ *
6
+ * Usable directly as `store` in the configuration file; its options come from `storeOptions`.
7
+ */
8
+ export default async function postgresCatalogStore(options = {}) {
9
+ const tableName = options.table ?? DEFAULT_TABLE_NAME;
10
+ const table = quoteSqlIdentifier(tableName);
11
+ let client = options.client;
12
+ let label = "postgres:";
13
+ let close;
14
+ if (!client) {
15
+ const url = resolveStoreUrl("PostgreSQL", options.url, ["DATABASE_URL"], ["postgres:", "postgresql:"]);
16
+ const pg = await importDriver("PostgreSQL", "pg");
17
+ const Pool = pg.Pool ?? pg.default?.Pool;
18
+ if (!Pool) {
19
+ throw new Error("The installed 'pg' package does not expose Pool.");
20
+ }
21
+ const pool = new Pool({ connectionString: url.href });
22
+ // Without a listener, an error on an idle connection would crash the process.
23
+ pool.on("error", () => { });
24
+ client = pool;
25
+ label = describeStoreUrl(url);
26
+ close = () => pool.end();
27
+ }
28
+ const database = client;
29
+ try {
30
+ if (options.createTable ?? true) {
31
+ await database.query(`CREATE TABLE IF NOT EXISTS ${table} (
32
+ profile_key TEXT PRIMARY KEY,
33
+ content TEXT NOT NULL,
34
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
35
+ )`);
36
+ }
37
+ }
38
+ catch (error) {
39
+ await close?.().catch(() => { });
40
+ throw error;
41
+ }
42
+ const describe = (profileKey) => `${label}/${tableName}/${profileKey}`;
43
+ return {
44
+ describe,
45
+ async read(profileKey) {
46
+ const { rows } = await database.query(`SELECT content FROM ${table} WHERE profile_key = $1`, [
47
+ profileKey,
48
+ ]);
49
+ const content = rows[0]?.content;
50
+ return typeof content === "string" ? content : undefined;
51
+ },
52
+ async replace(profileKey, expected, next, signal) {
53
+ signal?.throwIfAborted();
54
+ const { rowCount } = expected === undefined
55
+ ? await database.query(`INSERT INTO ${table} (profile_key, content) VALUES ($1, $2)
56
+ ON CONFLICT (profile_key) DO NOTHING`, [profileKey, next])
57
+ : await database.query(`UPDATE ${table} SET content = $3, updated_at = now()
58
+ WHERE profile_key = $1 AND content = $2`, [profileKey, expected, next]);
59
+ if (rowCount !== 1) {
60
+ throw new CatalogConflictError(describe(profileKey));
61
+ }
62
+ },
63
+ close,
64
+ };
65
+ }
@@ -0,0 +1,24 @@
1
+ import type { CatalogStore } from "./catalog_store.js";
2
+ export interface RedisCatalogStoreOptions {
3
+ /** `redis://` or `rediss://` URL. Defaults to `KIBANA_STORE_URL`, then `REDIS_URL`. */
4
+ url?: string;
5
+ /** Prefix of the catalog keys. Defaults to `kibana-mcp:catalog:`. */
6
+ keyPrefix?: string;
7
+ /** A connected `redis` client to use instead of `url`. The store never closes it. */
8
+ client?: RedisCatalogClient;
9
+ }
10
+ /** The subset of the `redis` client this store relies on. */
11
+ export interface RedisCatalogClient {
12
+ get(key: string): Promise<string | null>;
13
+ eval(script: string, options: {
14
+ keys: string[];
15
+ arguments: string[];
16
+ }): Promise<unknown>;
17
+ }
18
+ /**
19
+ * Catalog store backed by Redis through the `redis` package: one key per profile, without expiry.
20
+ * Use a Redis with persistence and a `noeviction` policy, catalogs are not a cache.
21
+ *
22
+ * Usable directly as `store` in the configuration file; its options come from `storeOptions`.
23
+ */
24
+ export default function redisCatalogStore(options?: RedisCatalogStoreOptions): Promise<CatalogStore>;
@@ -0,0 +1,31 @@
1
+ import { DEFAULT_KEY_PREFIX, REDIS_COMPARE_AND_SET_SCRIPT, createKeyValueCatalogStore, describeStoreUrl, importDriver, resolveStoreUrl, } from "./shared.js";
2
+ /**
3
+ * Catalog store backed by Redis through the `redis` package: one key per profile, without expiry.
4
+ * Use a Redis with persistence and a `noeviction` policy, catalogs are not a cache.
5
+ *
6
+ * Usable directly as `store` in the configuration file; its options come from `storeOptions`.
7
+ */
8
+ export default async function redisCatalogStore(options = {}) {
9
+ let client = options.client;
10
+ let label = "redis:";
11
+ let close;
12
+ if (!client) {
13
+ const url = resolveStoreUrl("Redis", options.url, ["REDIS_URL"], ["redis:", "rediss:"]);
14
+ const redis = await importDriver("Redis", "redis");
15
+ const owned = redis.createClient({ url: url.href });
16
+ // Without a listener, a connection error would crash the process instead of failing the call.
17
+ owned.on("error", () => { });
18
+ await owned.connect();
19
+ client = owned;
20
+ label = describeStoreUrl(url);
21
+ close = () => owned.close();
22
+ }
23
+ const connected = client;
24
+ return createKeyValueCatalogStore({
25
+ label,
26
+ keyPrefix: options.keyPrefix ?? DEFAULT_KEY_PREFIX,
27
+ get: (key) => connected.get(key),
28
+ compareAndSet: (key, values) => connected.eval(REDIS_COMPARE_AND_SET_SCRIPT, { keys: [key], arguments: values }),
29
+ close,
30
+ });
31
+ }
@@ -0,0 +1,25 @@
1
+ import { type CatalogStore } from "./catalog_store.js";
2
+ export declare const STORE_URL_ENV = "KIBANA_STORE_URL";
3
+ export declare const DEFAULT_KEY_PREFIX = "kibana-mcp:catalog:";
4
+ export declare const DEFAULT_TABLE_NAME = "kibana_mcp_catalogs";
5
+ /**
6
+ * Resolves the connection URL of a store: the explicit option, then `KIBANA_STORE_URL`, then the
7
+ * conventional variables of the backend.
8
+ */
9
+ export declare function resolveStoreUrl(storeName: string, explicitUrl: string | undefined, fallbackEnvNames: string[], protocols: string[]): URL;
10
+ /** Like resolveStoreUrl, but leaves backend-specific URI parsing to its driver. */
11
+ export declare function resolveStoreConnectionString(storeName: string, explicitUrl: string | undefined, fallbackEnvNames: string[], protocols: string[]): string;
12
+ /** Location of a store without its credentials, for tool output. */
13
+ export declare function describeStoreUrl(url: URL): string;
14
+ export declare function importDriver<T>(storeName: string, packageName: string): Promise<T>;
15
+ /** Quotes a possibly schema-qualified SQL identifier, refusing anything but plain names. */
16
+ export declare function quoteSqlIdentifier(name: string): string;
17
+ export declare const REDIS_COMPARE_AND_SET_SCRIPT = "\nlocal current = redis.call('GET', KEYS[1])\nif ARGV[1] == '1' then\n if current ~= ARGV[2] then return 0 end\nelseif current then\n return 0\nend\nredis.call('SET', KEYS[1], ARGV[3])\nreturn 1\n";
18
+ export declare function createKeyValueCatalogStore(backend: {
19
+ label: string;
20
+ keyPrefix: string;
21
+ get(key: string): Promise<string | null | undefined>;
22
+ /** Runs {@link REDIS_COMPARE_AND_SET_SCRIPT} with `[hasExpected, expected, next]`. */
23
+ compareAndSet(key: string, values: [string, string, string]): Promise<unknown>;
24
+ close?(): Promise<void>;
25
+ }): CatalogStore;
@@ -0,0 +1,88 @@
1
+ import { CatalogConflictError } from "./catalog_store.js";
2
+ export const STORE_URL_ENV = "KIBANA_STORE_URL";
3
+ export const DEFAULT_KEY_PREFIX = "kibana-mcp:catalog:";
4
+ export const DEFAULT_TABLE_NAME = "kibana_mcp_catalogs";
5
+ /**
6
+ * Resolves the connection URL of a store: the explicit option, then `KIBANA_STORE_URL`, then the
7
+ * conventional variables of the backend.
8
+ */
9
+ export function resolveStoreUrl(storeName, explicitUrl, fallbackEnvNames, protocols) {
10
+ const url = resolveStoreConnectionString(storeName, explicitUrl, fallbackEnvNames, protocols);
11
+ try {
12
+ return new URL(url);
13
+ }
14
+ catch {
15
+ throw new Error(`The ${storeName} catalog store URL is not a valid URL.`);
16
+ }
17
+ }
18
+ /** Like resolveStoreUrl, but leaves backend-specific URI parsing to its driver. */
19
+ export function resolveStoreConnectionString(storeName, explicitUrl, fallbackEnvNames, protocols) {
20
+ const envNames = [STORE_URL_ENV, ...fallbackEnvNames];
21
+ const url = explicitUrl?.trim() ||
22
+ envNames.map((name) => process.env[name]?.trim()).find((value) => Boolean(value));
23
+ if (!url) {
24
+ throw new Error(`The ${storeName} catalog store needs a URL: pass 'url', or set ${envNames.join(" or ")}.`);
25
+ }
26
+ const protocol = /^[a-z][a-z0-9+.-]*:/i.exec(url)?.[0].toLowerCase();
27
+ if (!protocol) {
28
+ throw new Error(`The ${storeName} catalog store URL is not a valid URL.`);
29
+ }
30
+ if (!protocols.includes(protocol)) {
31
+ throw new Error(`The ${storeName} catalog store URL must use ${protocols.map((protocol) => `${protocol}//`).join(" or ")}.`);
32
+ }
33
+ return url;
34
+ }
35
+ /** Location of a store without its credentials, for tool output. */
36
+ export function describeStoreUrl(url) {
37
+ return `${url.protocol}//${url.host}${url.pathname === "/" ? "" : url.pathname}`;
38
+ }
39
+ export async function importDriver(storeName, packageName) {
40
+ try {
41
+ return (await import(packageName));
42
+ }
43
+ catch (error) {
44
+ throw new Error(`The ${storeName} catalog store requires the '${packageName}' package. Install it next to this server.`, { cause: error });
45
+ }
46
+ }
47
+ /** Quotes a possibly schema-qualified SQL identifier, refusing anything but plain names. */
48
+ export function quoteSqlIdentifier(name) {
49
+ const parts = name.split(".");
50
+ if (parts.length > 2 || parts.some((part) => !/^[A-Za-z_][A-Za-z0-9_]*$/.test(part))) {
51
+ throw new Error(`Invalid table name '${name}': use letters, digits and underscores, optionally prefixed by a schema.`);
52
+ }
53
+ return parts.map((part) => `"${part}"`).join(".");
54
+ }
55
+ // Writes only when the key still holds the content the caller read, so that several server
56
+ // processes sharing one Redis never overwrite each other's catalog changes.
57
+ export const REDIS_COMPARE_AND_SET_SCRIPT = `
58
+ local current = redis.call('GET', KEYS[1])
59
+ if ARGV[1] == '1' then
60
+ if current ~= ARGV[2] then return 0 end
61
+ elseif current then
62
+ return 0
63
+ end
64
+ redis.call('SET', KEYS[1], ARGV[3])
65
+ return 1
66
+ `;
67
+ export function createKeyValueCatalogStore(backend) {
68
+ const keyOf = (profileKey) => `${backend.keyPrefix}${profileKey}`;
69
+ const describe = (profileKey) => `${backend.label}/${keyOf(profileKey)}`;
70
+ return {
71
+ describe,
72
+ async read(profileKey) {
73
+ return (await backend.get(keyOf(profileKey))) ?? undefined;
74
+ },
75
+ async replace(profileKey, expected, next, signal) {
76
+ signal?.throwIfAborted();
77
+ const replaced = await backend.compareAndSet(keyOf(profileKey), [
78
+ expected === undefined ? "0" : "1",
79
+ expected ?? "",
80
+ next,
81
+ ]);
82
+ if (Number(replaced) !== 1) {
83
+ throw new CatalogConflictError(describe(profileKey));
84
+ }
85
+ },
86
+ close: backend.close,
87
+ };
88
+ }
@@ -0,0 +1,26 @@
1
+ import { type CatalogStore } from "./catalog_store.js";
2
+ export interface SqliteCatalogStoreOptions {
3
+ /** Database file, or `:memory:`. Defaults to `KIBANA_STORE_URL`, with or without a `sqlite:` prefix. */
4
+ path?: string;
5
+ /** Catalog table. Defaults to `kibana_mcp_catalogs`. */
6
+ table?: string;
7
+ /** An open `node:sqlite` database to use instead of `path`. The store never closes it. */
8
+ database?: SqliteCatalogDatabase;
9
+ }
10
+ /** The subset of a `node:sqlite` `DatabaseSync` this store relies on. */
11
+ export interface SqliteCatalogDatabase {
12
+ exec(sql: string): void;
13
+ prepare(sql: string): {
14
+ get(...parameters: string[]): unknown;
15
+ run(...parameters: string[]): {
16
+ changes: number | bigint;
17
+ };
18
+ };
19
+ }
20
+ /**
21
+ * Catalog store backed by a SQLite file through the built-in `node:sqlite` module: one row per
22
+ * profile. Suited to a single host; several processes may share the file.
23
+ *
24
+ * Usable directly as `store` in the configuration file; its options come from `storeOptions`.
25
+ */
26
+ export default function sqliteCatalogStore(options?: SqliteCatalogStoreOptions): Promise<CatalogStore>;
@@ -0,0 +1,63 @@
1
+ import { mkdir } from "node:fs/promises";
2
+ import { dirname, resolve } from "node:path";
3
+ import { CatalogConflictError } from "./catalog_store.js";
4
+ import { DEFAULT_TABLE_NAME, STORE_URL_ENV, importDriver, quoteSqlIdentifier } from "./shared.js";
5
+ /**
6
+ * Catalog store backed by a SQLite file through the built-in `node:sqlite` module: one row per
7
+ * profile. Suited to a single host; several processes may share the file.
8
+ *
9
+ * Usable directly as `store` in the configuration file; its options come from `storeOptions`.
10
+ */
11
+ export default async function sqliteCatalogStore(options = {}) {
12
+ const tableName = options.table ?? DEFAULT_TABLE_NAME;
13
+ const table = quoteSqlIdentifier(tableName);
14
+ let database = options.database;
15
+ let label = "sqlite:";
16
+ let close;
17
+ if (!database) {
18
+ const configuredPath = (options.path?.trim() || process.env[STORE_URL_ENV]?.trim())?.replace(/^sqlite:(\/\/)?/, "");
19
+ if (!configuredPath) {
20
+ throw new Error(`The SQLite catalog store needs a database file: pass 'path', or set ${STORE_URL_ENV}.`);
21
+ }
22
+ const inMemory = configuredPath === ":memory:";
23
+ const path = inMemory ? configuredPath : resolve(configuredPath);
24
+ if (!inMemory) {
25
+ await mkdir(dirname(path), { recursive: true });
26
+ }
27
+ const sqlite = await importDriver("SQLite", "node:sqlite");
28
+ const opened = new sqlite.DatabaseSync(path);
29
+ // Let several server processes share the file: readers do not block, writers wait their turn.
30
+ opened.exec("PRAGMA journal_mode = WAL; PRAGMA busy_timeout = 5000;");
31
+ database = opened;
32
+ label = `sqlite:${path}`;
33
+ close = async () => opened.close();
34
+ }
35
+ database.exec(`CREATE TABLE IF NOT EXISTS ${table} (
36
+ profile_key TEXT PRIMARY KEY,
37
+ content TEXT NOT NULL,
38
+ updated_at TEXT NOT NULL
39
+ )`);
40
+ const select = database.prepare(`SELECT content FROM ${table} WHERE profile_key = ?`);
41
+ const insert = database.prepare(`INSERT INTO ${table} (profile_key, content, updated_at) VALUES (?, ?, ?)
42
+ ON CONFLICT (profile_key) DO NOTHING`);
43
+ const update = database.prepare(`UPDATE ${table} SET content = ?, updated_at = ? WHERE profile_key = ? AND content = ?`);
44
+ const describe = (profileKey) => `${label}/${tableName}/${profileKey}`;
45
+ return {
46
+ describe,
47
+ async read(profileKey) {
48
+ const row = select.get(profileKey);
49
+ return typeof row?.content === "string" ? row.content : undefined;
50
+ },
51
+ async replace(profileKey, expected, next, signal) {
52
+ signal?.throwIfAborted();
53
+ const now = new Date().toISOString();
54
+ const { changes } = expected === undefined
55
+ ? insert.run(profileKey, next, now)
56
+ : update.run(next, now, profileKey, expected);
57
+ if (Number(changes) !== 1) {
58
+ throw new CatalogConflictError(describe(profileKey));
59
+ }
60
+ },
61
+ close,
62
+ };
63
+ }
@@ -0,0 +1,59 @@
1
+ import type { CallToolResult } from "@modelcontextprotocol/server";
2
+ import { z } from "zod";
3
+ import type { AppConfig } from "../types.js";
4
+ export declare const configureInputSchema: z.ZodObject<{
5
+ kibana: z.ZodObject<{
6
+ baseUrl: z.ZodURL;
7
+ username: z.ZodString;
8
+ password: z.ZodString;
9
+ timeoutMs: z.ZodDefault<z.ZodNumber>;
10
+ }, z.core.$strip>;
11
+ sources: z.ZodArray<z.ZodObject<{
12
+ id: z.ZodString;
13
+ name: z.ZodString;
14
+ description: z.ZodOptional<z.ZodString>;
15
+ tags: z.ZodDefault<z.ZodArray<z.ZodString>>;
16
+ timeField: z.ZodString;
17
+ backend: z.ZodObject<{
18
+ kind: z.ZodEnum<{
19
+ elasticsearch_search: "elasticsearch_search";
20
+ kibana_internal_search_es: "kibana_internal_search_es";
21
+ }>;
22
+ path: z.ZodString;
23
+ index: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodArray<z.ZodString>]>>;
24
+ }, z.core.$strip>;
25
+ schema: z.ZodOptional<z.ZodObject<{
26
+ kind: z.ZodEnum<{
27
+ elasticsearch_field_caps: "elasticsearch_field_caps";
28
+ kibana_data_views_fields: "kibana_data_views_fields";
29
+ kibana_index_patterns_fields: "kibana_index_patterns_fields";
30
+ }>;
31
+ path: z.ZodOptional<z.ZodString>;
32
+ index: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodArray<z.ZodString>]>>;
33
+ }, z.core.$strip>>;
34
+ fieldHints: z.ZodDefault<z.ZodArray<z.ZodObject<{
35
+ name: z.ZodString;
36
+ type: z.ZodOptional<z.ZodString>;
37
+ description: z.ZodOptional<z.ZodString>;
38
+ aliases: z.ZodDefault<z.ZodArray<z.ZodString>>;
39
+ }, z.core.$strip>>>;
40
+ defaultTextFields: z.ZodDefault<z.ZodArray<z.ZodString>>;
41
+ evidenceFields: z.ZodDefault<z.ZodArray<z.ZodString>>;
42
+ }, z.core.$strip>>;
43
+ }, z.core.$strip>;
44
+ export declare const configureOutputSchema: z.ZodObject<{
45
+ configured: z.ZodLiteral<true>;
46
+ persisted: z.ZodLiteral<true>;
47
+ source_catalog_path: z.ZodString;
48
+ source_count: z.ZodNumber;
49
+ source_ids: z.ZodArray<z.ZodString>;
50
+ base_url: z.ZodString;
51
+ }, z.core.$strip>;
52
+ export declare function executeConfigure(input: z.infer<typeof configureInputSchema>, options?: {
53
+ envInput?: NodeJS.ProcessEnv;
54
+ sourceCatalogPath?: string;
55
+ }): Promise<{
56
+ nextConfig: AppConfig;
57
+ result: z.infer<typeof configureOutputSchema>;
58
+ }>;
59
+ export declare function createConfigureCallToolResult(result: z.infer<typeof configureOutputSchema>): CallToolResult;
@@ -1,5 +1,5 @@
1
1
  import { z } from "zod";
2
- import { persistSourceCatalog, sourceDefinitionSchema } from "../config.js";
2
+ import { persistSourceCatalog, sourceDefinitionSchema } from "../config/environment.js";
3
3
  export const configureInputSchema = z.object({
4
4
  kibana: z.object({
5
5
  baseUrl: z.url(),
@@ -0,0 +1,35 @@
1
+ import type { CallToolResult } from "@modelcontextprotocol/server";
2
+ import { z } from "zod";
3
+ import type { KibanaClient } from "../kibana/client.js";
4
+ import type { ResolvedAppConfig, SourceDefinition } from "../types.js";
5
+ export declare const configureIndexInputSchema: z.ZodObject<{
6
+ index: z.ZodString;
7
+ source_name: z.ZodOptional<z.ZodString>;
8
+ source_id: z.ZodOptional<z.ZodString>;
9
+ time_field: z.ZodOptional<z.ZodString>;
10
+ replace: z.ZodDefault<z.ZodBoolean>;
11
+ }, z.core.$strict>;
12
+ export declare const configureIndexOutputSchema: z.ZodObject<{
13
+ configured: z.ZodLiteral<true>;
14
+ persisted: z.ZodLiteral<true>;
15
+ source_catalog_path: z.ZodString;
16
+ source_count: z.ZodNumber;
17
+ source_id: z.ZodString;
18
+ index: z.ZodString;
19
+ time_field: z.ZodString;
20
+ }, z.core.$strip>;
21
+ type ConfigureIndexInput = z.input<typeof configureIndexInputSchema>;
22
+ type ConfigureIndexClient = Pick<KibanaClient, "describeFields" | "execute">;
23
+ export declare function executeConfigureIndex(inputValue: ConfigureIndexInput, options: {
24
+ config: ResolvedAppConfig;
25
+ kibanaClient: ConfigureIndexClient;
26
+ stateRoot?: string;
27
+ callerSignal?: AbortSignal;
28
+ }): Promise<{
29
+ sources: SourceDefinition[];
30
+ result: z.infer<typeof configureIndexOutputSchema>;
31
+ /** The serialized catalog that was written. */
32
+ catalogContent?: string;
33
+ }>;
34
+ export declare function createConfigureIndexCallToolResult(result: z.infer<typeof configureIndexOutputSchema>): CallToolResult;
35
+ export {};
@@ -1,9 +1,7 @@
1
- import { readFile } from "node:fs/promises";
2
1
  import { z } from "zod";
3
- import { buildGeneratedSource, chooseGeneratedSourceFields, createGeneratedCatalog, parseGeneratedCatalog, } from "../bootstrap.js";
4
- import { persistSourceCatalog } from "../config.js";
5
- import { resolveProfilePaths } from "../profile_paths.js";
6
- import { acquireStateLock } from "../state_lock.js";
2
+ import { buildGeneratedSource, chooseGeneratedSourceFields, createGeneratedCatalog, parseGeneratedCatalog, } from "../setup/bootstrap.js";
3
+ import { bindCatalogStore } from "../stores/catalog_store.js";
4
+ import { createFileCatalogStore } from "../stores/file.js";
7
5
  export const configureIndexInputSchema = z
8
6
  .object({
9
7
  index: z.string().trim().min(1),
@@ -24,72 +22,83 @@ export const configureIndexOutputSchema = z.object({
24
22
  });
25
23
  export async function executeConfigureIndex(inputValue, options) {
26
24
  const input = configureIndexInputSchema.parse(inputValue);
27
- const sourceCatalogPath = options.config.sourceCatalogPath;
25
+ options.callerSignal?.throwIfAborted();
26
+ // A catalog kept at a plain path goes through the same store contract as any other: the write
27
+ // only happens if the catalog is still the one that was read, so Kibana is never called under a lock.
28
+ const catalog = options.config.catalog ?? fileCatalogOf(options.config, options.stateRoot);
29
+ const existingContent = await catalog.read();
30
+ // Sources without a catalog at the persisted location were loaded from somewhere else, such as
31
+ // the static fallback catalog: starting from an empty catalog would silently drop them.
32
+ if (existingContent === undefined && options.config.sources.length > 0) {
33
+ throw new Error(`Source catalog '${catalog.location}' does not exist although sources are configured. Refusing to replace them with a new managed catalog.`);
34
+ }
35
+ const next = await buildNextCatalog(input, existingContent, options);
36
+ const catalogContent = `${JSON.stringify(next.catalog, null, 2)}\n`;
37
+ options.callerSignal?.throwIfAborted();
38
+ await catalog.replace(existingContent, catalogContent, options.callerSignal);
39
+ return {
40
+ sources: next.sources,
41
+ catalogContent,
42
+ result: {
43
+ configured: true,
44
+ persisted: true,
45
+ source_catalog_path: catalog.location,
46
+ source_count: next.sources.length,
47
+ source_id: next.source.id,
48
+ index: input.index,
49
+ time_field: next.source.timeField,
50
+ },
51
+ };
52
+ }
53
+ function fileCatalogOf(config, stateRoot) {
54
+ const sourceCatalogPath = config.sourceCatalogPath;
28
55
  if (!sourceCatalogPath) {
29
56
  throw new Error("configure_index requires a persisted source catalog. Run connection bootstrap first.");
30
57
  }
31
- const stateRoot = options.stateRoot ?? resolveProfilePaths().stateRoot;
32
- const releaseLock = await acquireStateLock(stateRoot);
33
- try {
34
- const existingContent = await readFile(sourceCatalogPath, "utf8");
35
- const existingCatalog = parseGeneratedCatalog(existingContent);
36
- if (!existingCatalog) {
37
- throw new Error("configure_index can update only a managed generated catalog. Refusing to overwrite a hand-authored or modified catalog.");
38
- }
39
- const proposedSource = buildGeneratedSource([input.index], input.source_name, input.source_id);
40
- const existingSource = existingCatalog.sources.find((source) => source.id === proposedSource.id);
41
- if (existingSource &&
42
- !input.replace &&
43
- JSON.stringify(existingSource.backend.index) !== JSON.stringify(proposedSource.backend.index)) {
44
- throw new Error(`Source id '${proposedSource.id}' already refers to a different index. Re-run with replace=true only after the user explicitly approves replacement.`);
45
- }
46
- const discoveredFields = await options.kibanaClient.describeFields(proposedSource, options.callerSignal);
47
- if (discoveredFields.length === 0) {
48
- throw new Error(`Explicit index pattern '${input.index}' resolved to no fields. Check the index and Kibana permissions.`);
49
- }
50
- const generatedFields = chooseGeneratedSourceFields(discoveredFields, input.time_field);
51
- const verifiedSource = {
52
- ...proposedSource,
53
- ...generatedFields,
54
- };
55
- await options.kibanaClient.execute({
56
- source: verifiedSource,
57
- request: {
58
- body: {
59
- size: 1,
60
- track_total_hits: false,
61
- query: { match_all: {} },
62
- },
63
- },
64
- resolvedFilters: [],
65
- resolvedNestedFilters: [],
66
- resolvedSortBy: verifiedSource.timeField,
67
- advisories: [],
68
- }, options.callerSignal);
69
- const sources = existingSource
70
- ? existingCatalog.sources.map((source) => source.id === verifiedSource.id ? verifiedSource : source)
71
- : [...existingCatalog.sources, verifiedSource];
72
- const generatedCatalog = createGeneratedCatalog(sources);
73
- await persistSourceCatalog(sources, {
74
- sourceCatalogPath,
75
- catalog: generatedCatalog,
76
- });
77
- return {
78
- sources,
79
- result: {
80
- configured: true,
81
- persisted: true,
82
- source_catalog_path: sourceCatalogPath,
83
- source_count: sources.length,
84
- source_id: verifiedSource.id,
85
- index: input.index,
86
- time_field: verifiedSource.timeField,
87
- },
88
- };
58
+ return bindCatalogStore(createFileCatalogStore(() => sourceCatalogPath, stateRoot), config.profileName ?? "default");
59
+ }
60
+ async function buildNextCatalog(input, existingContent, options) {
61
+ // A profile that has no catalog yet starts from an empty managed one.
62
+ const existingCatalog = existingContent === undefined
63
+ ? createGeneratedCatalog([])
64
+ : parseGeneratedCatalog(existingContent);
65
+ if (!existingCatalog) {
66
+ throw new Error("configure_index can update only a managed generated catalog. Refusing to overwrite a hand-authored or modified catalog.");
67
+ }
68
+ const proposedSource = buildGeneratedSource([input.index], input.source_name, input.source_id);
69
+ const existingSource = existingCatalog.sources.find((source) => source.id === proposedSource.id);
70
+ if (existingSource &&
71
+ !input.replace &&
72
+ JSON.stringify(existingSource.backend.index) !== JSON.stringify(proposedSource.backend.index)) {
73
+ throw new Error(`Source id '${proposedSource.id}' already refers to a different index. Re-run with replace=true only after the user explicitly approves replacement.`);
89
74
  }
90
- finally {
91
- await releaseLock();
75
+ const discoveredFields = await options.kibanaClient.describeFields(proposedSource, options.callerSignal);
76
+ if (discoveredFields.length === 0) {
77
+ throw new Error(`Explicit index pattern '${input.index}' resolved to no fields. Check the index and Kibana permissions.`);
92
78
  }
79
+ const generatedFields = chooseGeneratedSourceFields(discoveredFields, input.time_field);
80
+ const verifiedSource = {
81
+ ...proposedSource,
82
+ ...generatedFields,
83
+ };
84
+ await options.kibanaClient.execute({
85
+ source: verifiedSource,
86
+ request: {
87
+ body: {
88
+ size: 1,
89
+ track_total_hits: false,
90
+ query: { match_all: {} },
91
+ },
92
+ },
93
+ resolvedFilters: [],
94
+ resolvedNestedFilters: [],
95
+ resolvedSortBy: verifiedSource.timeField,
96
+ advisories: [],
97
+ }, options.callerSignal);
98
+ const sources = existingSource
99
+ ? existingCatalog.sources.map((source) => source.id === verifiedSource.id ? verifiedSource : source)
100
+ : [...existingCatalog.sources, verifiedSource];
101
+ return { sources, source: verifiedSource, catalog: createGeneratedCatalog(sources) };
93
102
  }
94
103
  export function createConfigureIndexCallToolResult(result) {
95
104
  return {
@@ -0,0 +1,29 @@
1
+ import type { CallToolResult } from "@modelcontextprotocol/server";
2
+ import { z } from "zod";
3
+ import type { SchemaCatalog } from "../kibana/schema_catalog.js";
4
+ import type { SourceCatalog } from "../kibana/source_catalog.js";
5
+ export declare const describeFieldsInputSchema: z.ZodObject<{
6
+ source_id: z.ZodString;
7
+ query: z.ZodOptional<z.ZodString>;
8
+ limit: z.ZodDefault<z.ZodNumber>;
9
+ }, z.core.$strip>;
10
+ export declare const describeFieldsOutputSchema: z.ZodObject<{
11
+ source_id: z.ZodString;
12
+ total: z.ZodNumber;
13
+ fields: z.ZodArray<z.ZodObject<{
14
+ name: z.ZodString;
15
+ type: z.ZodOptional<z.ZodString>;
16
+ description: z.ZodOptional<z.ZodString>;
17
+ aliases: z.ZodOptional<z.ZodArray<z.ZodString>>;
18
+ searchable: z.ZodOptional<z.ZodBoolean>;
19
+ aggregatable: z.ZodOptional<z.ZodBoolean>;
20
+ subfields: z.ZodArray<z.ZodString>;
21
+ nested_path: z.ZodOptional<z.ZodString>;
22
+ object_array_path: z.ZodOptional<z.ZodString>;
23
+ multi_field_parent: z.ZodOptional<z.ZodString>;
24
+ preferred_exact_field: z.ZodOptional<z.ZodString>;
25
+ }, z.core.$strip>>;
26
+ }, z.core.$strip>;
27
+ export declare function executeDescribeFields(input: z.infer<typeof describeFieldsInputSchema>, sourceCatalog: Pick<SourceCatalog, "getRequiredSources">, schemaCatalog: SchemaCatalog, callerSignal?: AbortSignal): Promise<z.infer<typeof describeFieldsOutputSchema>>;
28
+ export declare function formatDescribeFieldsResult(result: z.infer<typeof describeFieldsOutputSchema>): string;
29
+ export declare function createDescribeFieldsCallToolResult(result: z.infer<typeof describeFieldsOutputSchema>): CallToolResult;
@@ -0,0 +1,27 @@
1
+ import type { CallToolResult } from "@modelcontextprotocol/server";
2
+ import { z } from "zod";
3
+ import type { SourceCatalog } from "../kibana/source_catalog.js";
4
+ export declare const discoverInputSchema: z.ZodObject<{
5
+ query: z.ZodOptional<z.ZodString>;
6
+ limit: z.ZodDefault<z.ZodNumber>;
7
+ }, z.core.$strip>;
8
+ export declare const discoverOutputSchema: z.ZodObject<{
9
+ configured_total: z.ZodNumber;
10
+ total: z.ZodNumber;
11
+ sources: z.ZodArray<z.ZodObject<{
12
+ id: z.ZodString;
13
+ name: z.ZodString;
14
+ description: z.ZodOptional<z.ZodString>;
15
+ tags: z.ZodArray<z.ZodString>;
16
+ time_field: z.ZodString;
17
+ field_hints: z.ZodArray<z.ZodObject<{
18
+ name: z.ZodString;
19
+ type: z.ZodOptional<z.ZodString>;
20
+ description: z.ZodOptional<z.ZodString>;
21
+ aliases: z.ZodArray<z.ZodString>;
22
+ }, z.core.$strip>>;
23
+ }, z.core.$strip>>;
24
+ }, z.core.$strip>;
25
+ export declare function executeDiscover(input: z.infer<typeof discoverInputSchema>, sourceCatalog: SourceCatalog): z.infer<typeof discoverOutputSchema>;
26
+ export declare function formatDiscoverResult(result: z.infer<typeof discoverOutputSchema>): string;
27
+ export declare function createDiscoverCallToolResult(result: z.infer<typeof discoverOutputSchema>): CallToolResult;