@havesomecode/kibana-mcp-server 1.3.1 → 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 (85) 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/run_cli.d.ts +4 -0
  4. package/dist/src/bin/run_cli.js +1 -1
  5. package/dist/src/commands/context.d.ts +18 -0
  6. package/dist/src/commands/serve.d.ts +4 -0
  7. package/dist/src/commands/serve.js +30 -6
  8. package/dist/src/commands/setup.d.ts +3 -0
  9. package/dist/src/commands/setup.js +2 -2
  10. package/dist/src/config/config_file.d.ts +59 -0
  11. package/dist/src/config/config_file.js +76 -0
  12. package/dist/src/config/environment.d.ts +100 -0
  13. package/dist/src/{config.js → config/environment.js} +26 -24
  14. package/dist/src/config/startup.d.ts +31 -0
  15. package/dist/src/config/startup.js +86 -0
  16. package/dist/src/kibana/client.d.ts +28 -0
  17. package/dist/src/kibana/schema_catalog.d.ts +11 -0
  18. package/dist/src/{schema_catalog.js → kibana/schema_catalog.js} +33 -11
  19. package/dist/src/kibana/source_catalog.d.ts +10 -0
  20. package/dist/src/mcp_entry.d.ts +2 -0
  21. package/dist/src/mcp_entry.js +1 -1
  22. package/dist/src/profiles/context.d.ts +4 -0
  23. package/dist/src/profiles/context.js +15 -0
  24. package/dist/src/profiles/names.d.ts +10 -0
  25. package/dist/src/profiles/names.js +35 -0
  26. package/dist/src/profiles/paths.d.ts +9 -0
  27. package/dist/src/profiles/runtime.d.ts +43 -0
  28. package/dist/src/profiles/runtime.js +150 -0
  29. package/dist/src/profiles/secret_store.d.ts +20 -0
  30. package/dist/src/profiles/state_lock.d.ts +13 -0
  31. package/dist/src/{state_lock.js → profiles/state_lock.js} +14 -5
  32. package/dist/src/profiles/store.d.ts +34 -0
  33. package/dist/src/{profile_store.js → profiles/store.js} +7 -5
  34. package/dist/src/query/compiler.d.ts +9 -0
  35. package/dist/src/query/cursor.d.ts +8 -0
  36. package/dist/src/query/normalize.d.ts +2 -0
  37. package/dist/src/server.d.ts +29 -0
  38. package/dist/src/server.js +40 -50
  39. package/dist/src/setup/bootstrap.d.ts +50 -0
  40. package/dist/src/{bootstrap.js → setup/bootstrap.js} +10 -36
  41. package/dist/src/setup/client_registration.d.ts +38 -0
  42. package/dist/src/setup/setup_flow.d.ts +26 -0
  43. package/dist/src/{setup_flow.js → setup/setup_flow.js} +6 -6
  44. package/dist/src/stores/catalog_store.d.ts +38 -0
  45. package/dist/src/stores/catalog_store.js +22 -0
  46. package/dist/src/stores/file.d.ts +5 -0
  47. package/dist/src/stores/file.js +48 -0
  48. package/dist/src/stores/ioredis.d.ts +21 -0
  49. package/dist/src/stores/ioredis.js +33 -0
  50. package/dist/src/stores/mongodb.d.ts +42 -0
  51. package/dist/src/stores/mongodb.js +57 -0
  52. package/dist/src/stores/postgres.d.ts +24 -0
  53. package/dist/src/stores/postgres.js +65 -0
  54. package/dist/src/stores/redis.d.ts +24 -0
  55. package/dist/src/stores/redis.js +31 -0
  56. package/dist/src/stores/shared.d.ts +25 -0
  57. package/dist/src/stores/shared.js +88 -0
  58. package/dist/src/stores/sqlite.d.ts +26 -0
  59. package/dist/src/stores/sqlite.js +63 -0
  60. package/dist/src/tools/configure.d.ts +59 -0
  61. package/dist/src/tools/configure.js +1 -1
  62. package/dist/src/tools/configure_index.d.ts +35 -0
  63. package/dist/src/tools/configure_index.js +75 -66
  64. package/dist/src/tools/describe_fields.d.ts +29 -0
  65. package/dist/src/tools/discover.d.ts +27 -0
  66. package/dist/src/tools/filter.d.ts +180 -0
  67. package/dist/src/tools/query.d.ts +181 -0
  68. package/dist/src/transports/http.d.ts +15 -0
  69. package/dist/src/transports/http.js +291 -0
  70. package/dist/src/transports/stdio.d.ts +1 -0
  71. package/dist/src/transports/stdio.js +46 -0
  72. package/dist/src/types.d.ts +254 -0
  73. package/dist/src/utils/cli_io.d.ts +11 -0
  74. package/dist/src/utils/cli_prompts.d.ts +9 -0
  75. package/dist/src/utils/cli_ui.d.ts +6 -0
  76. package/dist/src/utils/fs.d.ts +3 -0
  77. package/dist/src/utils/fs.js +30 -0
  78. package/package.json +58 -1
  79. package/skills/kibana-log-investigation/SKILL.md +36 -5
  80. package/dist/src/mcp_runtime.js +0 -23
  81. /package/dist/src/{kibana_client.js → kibana/client.js} +0 -0
  82. /package/dist/src/{source_catalog.js → kibana/source_catalog.js} +0 -0
  83. /package/dist/src/{profile_paths.js → profiles/paths.js} +0 -0
  84. /package/dist/src/{secret_store.js → profiles/secret_store.js} +0 -0
  85. /package/dist/src/{client_registration.js → setup/client_registration.js} +0 -0
@@ -1,15 +1,39 @@
1
- import { PROFILE_NAME_ENV } from "../config.js";
2
- import { startMcpServer } from "../mcp_runtime.js";
1
+ import { CONFIG_FILE_ENV } from "../config/config_file.js";
2
+ import { PROFILE_NAME_ENV } from "../config/environment.js";
3
+ import { parsePort, startHttpMcpServer } from "../transports/http.js";
4
+ import { startMcpServer } from "../transports/stdio.js";
3
5
  export default (program, context) => {
4
6
  program
5
7
  .command("serve")
6
- .description("Start the stdio MCP server")
8
+ .description("Start the MCP server over stdio, or over Streamable HTTP with --http")
7
9
  .option("--profile <name>", "Saved profile name")
10
+ .option("--config <path>", "Configuration file, e.g. kibana.mcp.config.ts")
11
+ .option("--http", "Serve over Streamable HTTP instead of stdio")
12
+ .option("--host <host>", "HTTP listen host (default 127.0.0.1)")
13
+ .option("--port <port>", "HTTP listen port (default 3000)")
14
+ .option("--path <path>", "HTTP endpoint path (default /mcp)")
15
+ .option("--profile-header <name>", "HTTP header carrying the caller's profile")
8
16
  .action(async (options) => {
9
17
  try {
10
- await (context.dependencies.startMcpServerFn ?? startMcpServer)(options.profile
11
- ? { ...context.io.env, [PROFILE_NAME_ENV]: options.profile }
12
- : context.io.env);
18
+ const env = {
19
+ ...context.io.env,
20
+ ...(options.profile ? { [PROFILE_NAME_ENV]: options.profile } : {}),
21
+ ...(options.config ? { [CONFIG_FILE_ENV]: options.config } : {}),
22
+ };
23
+ if (!options.http) {
24
+ if (options.host || options.port || options.path || options.profileHeader) {
25
+ throw new Error("--host, --port, --path and --profile-header require --http.");
26
+ }
27
+ await (context.dependencies.startMcpServerFn ?? startMcpServer)(env);
28
+ return;
29
+ }
30
+ const { url } = await (context.dependencies.startHttpMcpServerFn ?? startHttpMcpServer)(env, {
31
+ host: options.host,
32
+ port: parsePort(options.port),
33
+ path: options.path,
34
+ profileHeader: options.profileHeader,
35
+ });
36
+ context.ui.success(`MCP server listening on ${url}`);
13
37
  }
14
38
  catch (error) {
15
39
  context.ui.error(error instanceof Error ? error.message : String(error));
@@ -0,0 +1,3 @@
1
+ import { type Command } from "commander";
2
+ import type { CliCommandContext } from "./context.js";
3
+ export default function registerSetup(program: Command, context: CliCommandContext): void;
@@ -1,6 +1,6 @@
1
1
  import { InvalidArgumentError, Option } from "commander";
2
- import { runBootstrap as runBootstrapFlow } from "../bootstrap.js";
3
- import { runSetupFlow } from "../setup_flow.js";
2
+ import { runBootstrap as runBootstrapFlow } from "../setup/bootstrap.js";
3
+ import { runSetupFlow } from "../setup/setup_flow.js";
4
4
  import { PromptCancelledError, createPromptIo } from "../utils/cli_prompts.js";
5
5
  export default function registerSetup(program, context) {
6
6
  const setupCommand = addOptions(program
@@ -0,0 +1,59 @@
1
+ import type { CatalogStore } from "../stores/catalog_store.js";
2
+ export { CatalogConflictError } from "../stores/catalog_store.js";
3
+ export type { CatalogStore } from "../stores/catalog_store.js";
4
+ export declare const CONFIG_FILE_ENV = "KIBANA_MCP_CONFIG";
5
+ export declare const HTTP_HOST_ENV = "KIBANA_HTTP_HOST";
6
+ export declare const HTTP_PORT_ENV = "KIBANA_HTTP_PORT";
7
+ export declare const HTTP_PATH_ENV = "KIBANA_HTTP_PATH";
8
+ export declare const HTTP_PROFILE_HEADER_ENV = "KIBANA_HTTP_PROFILE_HEADER";
9
+ export declare const HTTP_AUTH_TOKEN_ENV = "KIBANA_HTTP_AUTH_TOKEN";
10
+ export type CatalogStoreFactory<TStoreOptions> = (options?: TStoreOptions) => CatalogStore | Promise<CatalogStore>;
11
+ export interface KibanaMcpConfig<TStoreOptions = Record<string, unknown>> {
12
+ /** Kibana base URL. Overridden by `KIBANA_BASE_URL`. */
13
+ url?: string;
14
+ /** Kibana user. Overridden by `KIBANA_USERNAME`. */
15
+ user?: string;
16
+ /** Kibana password. Overridden by `KIBANA_PASSWORD`. */
17
+ password?: string;
18
+ /** Backend deadline in milliseconds. Overridden by `KIBANA_TIMEOUT_MS`. */
19
+ timeoutMs?: number;
20
+ /** Profile served when the transport names none. Overridden by `KIBANA_PROFILE`. */
21
+ profile?: string;
22
+ /**
23
+ * Where the source catalogs are kept, one per profile: a store, or a store factory such as the
24
+ * default export of `stores/redis`, called with `storeOptions`. Without it, catalogs are files.
25
+ */
26
+ store?: CatalogStore | CatalogStoreFactory<TStoreOptions>;
27
+ /** Options handed to the `store` factory: connection, key prefix, anything the store defines. */
28
+ storeOptions?: TStoreOptions;
29
+ /** Streamable HTTP transport, used by `serve --http`. Flags and environment variables win. */
30
+ http?: {
31
+ host?: string;
32
+ port?: number;
33
+ path?: string;
34
+ /** Header carrying the profile of the caller. */
35
+ profileHeader?: string;
36
+ /** Shared secret expected as `Authorization: Bearer <token>`. */
37
+ authToken?: string;
38
+ };
39
+ }
40
+ type KibanaMcpConfigExport<TStoreOptions> = KibanaMcpConfig<TStoreOptions> | (() => KibanaMcpConfig<TStoreOptions> | Promise<KibanaMcpConfig<TStoreOptions>>);
41
+ /**
42
+ * Types the default export of a `kibana.mcp.config.ts` file.
43
+ */
44
+ export declare function defineKibanaMcpConfig<TStoreOptions = Record<string, unknown>>(config: KibanaMcpConfigExport<TStoreOptions>): KibanaMcpConfigExport<TStoreOptions>;
45
+ export interface LoadedConfigFile {
46
+ path: string;
47
+ config: KibanaMcpConfig;
48
+ store?: CatalogStore;
49
+ }
50
+ /**
51
+ * Loads the configuration file named explicitly by the operator. The file is executed as code,
52
+ * so it is never discovered implicitly from the working directory.
53
+ */
54
+ export declare function loadConfigFile(configPath: string | undefined, cwd?: string): Promise<LoadedConfigFile | undefined>;
55
+ /**
56
+ * Returns the environment completed with the values of the configuration file. Variables that are
57
+ * already set win, so a deployment can override the file without editing it.
58
+ */
59
+ export declare function applyConfigFile(envInput: NodeJS.ProcessEnv, { config }: LoadedConfigFile): NodeJS.ProcessEnv;
@@ -0,0 +1,76 @@
1
+ import { access } from "node:fs/promises";
2
+ import { resolve } from "node:path";
3
+ import { pathToFileURL } from "node:url";
4
+ import { PROFILE_NAME_ENV } from "./environment.js";
5
+ export { CatalogConflictError } from "../stores/catalog_store.js";
6
+ export const CONFIG_FILE_ENV = "KIBANA_MCP_CONFIG";
7
+ export const HTTP_HOST_ENV = "KIBANA_HTTP_HOST";
8
+ export const HTTP_PORT_ENV = "KIBANA_HTTP_PORT";
9
+ export const HTTP_PATH_ENV = "KIBANA_HTTP_PATH";
10
+ export const HTTP_PROFILE_HEADER_ENV = "KIBANA_HTTP_PROFILE_HEADER";
11
+ export const HTTP_AUTH_TOKEN_ENV = "KIBANA_HTTP_AUTH_TOKEN";
12
+ /**
13
+ * Types the default export of a `kibana.mcp.config.ts` file.
14
+ */
15
+ export function defineKibanaMcpConfig(config) {
16
+ return config;
17
+ }
18
+ /**
19
+ * Loads the configuration file named explicitly by the operator. The file is executed as code,
20
+ * so it is never discovered implicitly from the working directory.
21
+ */
22
+ export async function loadConfigFile(configPath, cwd = process.cwd()) {
23
+ const requested = configPath?.trim();
24
+ if (!requested) {
25
+ return undefined;
26
+ }
27
+ const path = resolve(cwd, requested);
28
+ await access(path).catch(() => {
29
+ throw new Error(`Configuration file '${path}' was not found.`);
30
+ });
31
+ let exported;
32
+ try {
33
+ exported = (await import(pathToFileURL(path).href)).default;
34
+ }
35
+ catch (error) {
36
+ const hint = /\.[cm]?ts$/.test(path)
37
+ ? " A TypeScript configuration needs a runtime that strips types (Node.js 22.18 or later); otherwise use a .mjs file."
38
+ : "";
39
+ throw new Error(`Configuration file '${path}' could not be loaded: ${error instanceof Error ? error.message : String(error)}.${hint}`, { cause: error });
40
+ }
41
+ const config = (typeof exported === "function" ? await exported() : exported);
42
+ if (!config || typeof config !== "object" || Array.isArray(config)) {
43
+ throw new Error(`Configuration file '${path}' must default-export a configuration object.`);
44
+ }
45
+ const store = typeof config.store === "function" ? await config.store(config.storeOptions) : config.store;
46
+ if (store !== undefined &&
47
+ (typeof store?.read !== "function" || typeof store?.replace !== "function")) {
48
+ throw new Error(`Configuration file '${path}': store must implement read() and replace().`);
49
+ }
50
+ return { path, config, store };
51
+ }
52
+ /**
53
+ * Returns the environment completed with the values of the configuration file. Variables that are
54
+ * already set win, so a deployment can override the file without editing it.
55
+ */
56
+ export function applyConfigFile(envInput, { config }) {
57
+ const defaults = {
58
+ KIBANA_BASE_URL: config.url,
59
+ KIBANA_USERNAME: config.user,
60
+ KIBANA_PASSWORD: config.password,
61
+ KIBANA_TIMEOUT_MS: config.timeoutMs,
62
+ [PROFILE_NAME_ENV]: config.profile,
63
+ [HTTP_HOST_ENV]: config.http?.host,
64
+ [HTTP_PORT_ENV]: config.http?.port,
65
+ [HTTP_PATH_ENV]: config.http?.path,
66
+ [HTTP_PROFILE_HEADER_ENV]: config.http?.profileHeader,
67
+ [HTTP_AUTH_TOKEN_ENV]: config.http?.authToken,
68
+ };
69
+ const env = { ...envInput };
70
+ for (const [name, value] of Object.entries(defaults)) {
71
+ if (value !== undefined && value !== "" && !env[name]?.trim()) {
72
+ env[name] = String(value);
73
+ }
74
+ }
75
+ return env;
76
+ }
@@ -0,0 +1,100 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import { z } from "zod";
3
+ import { type SecretStore } from "../profiles/secret_store.js";
4
+ import { ProfileStore } from "../profiles/store.js";
5
+ import type { AppConfig, ResolvedAppConfig } from "../types.js";
6
+ export declare const DEFAULT_RUNTIME_SOURCE_CATALOG_PATH = "config/sources.runtime.json";
7
+ export declare const DEFAULT_STATIC_SOURCE_CATALOG_PATH = "config/sources.json";
8
+ export declare const PROFILE_NAME_ENV = "KIBANA_PROFILE";
9
+ export declare const PROFILE_PLACEHOLDER = "{profile}";
10
+ export declare class NoSavedProfileError extends Error {
11
+ constructor(message: string);
12
+ }
13
+ export declare const sourceDefinitionSchema: z.ZodObject<{
14
+ id: z.ZodString;
15
+ name: z.ZodString;
16
+ description: z.ZodOptional<z.ZodString>;
17
+ tags: z.ZodDefault<z.ZodArray<z.ZodString>>;
18
+ timeField: z.ZodString;
19
+ backend: z.ZodObject<{
20
+ kind: z.ZodEnum<{
21
+ elasticsearch_search: "elasticsearch_search";
22
+ kibana_internal_search_es: "kibana_internal_search_es";
23
+ }>;
24
+ path: z.ZodString;
25
+ index: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodArray<z.ZodString>]>>;
26
+ }, z.core.$strip>;
27
+ schema: z.ZodOptional<z.ZodObject<{
28
+ kind: z.ZodEnum<{
29
+ elasticsearch_field_caps: "elasticsearch_field_caps";
30
+ kibana_data_views_fields: "kibana_data_views_fields";
31
+ kibana_index_patterns_fields: "kibana_index_patterns_fields";
32
+ }>;
33
+ path: z.ZodOptional<z.ZodString>;
34
+ index: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodArray<z.ZodString>]>>;
35
+ }, z.core.$strip>>;
36
+ fieldHints: z.ZodDefault<z.ZodArray<z.ZodObject<{
37
+ name: z.ZodString;
38
+ type: z.ZodOptional<z.ZodString>;
39
+ description: z.ZodOptional<z.ZodString>;
40
+ aliases: z.ZodDefault<z.ZodArray<z.ZodString>>;
41
+ }, z.core.$strip>>>;
42
+ defaultTextFields: z.ZodDefault<z.ZodArray<z.ZodString>>;
43
+ evidenceFields: z.ZodDefault<z.ZodArray<z.ZodString>>;
44
+ }, z.core.$strip>;
45
+ export declare const sourceCatalogSchema: z.ZodObject<{
46
+ generatedBy: z.ZodOptional<z.ZodObject<{
47
+ tool: z.ZodLiteral<"@havesomecode/kibana-mcp-server">;
48
+ formatVersion: z.ZodLiteral<1>;
49
+ sourceHash: z.ZodString;
50
+ }, z.core.$strip>>;
51
+ sources: z.ZodArray<z.ZodObject<{
52
+ id: z.ZodString;
53
+ name: z.ZodString;
54
+ description: z.ZodOptional<z.ZodString>;
55
+ tags: z.ZodDefault<z.ZodArray<z.ZodString>>;
56
+ timeField: z.ZodString;
57
+ backend: z.ZodObject<{
58
+ kind: z.ZodEnum<{
59
+ elasticsearch_search: "elasticsearch_search";
60
+ kibana_internal_search_es: "kibana_internal_search_es";
61
+ }>;
62
+ path: z.ZodString;
63
+ index: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodArray<z.ZodString>]>>;
64
+ }, z.core.$strip>;
65
+ schema: z.ZodOptional<z.ZodObject<{
66
+ kind: z.ZodEnum<{
67
+ elasticsearch_field_caps: "elasticsearch_field_caps";
68
+ kibana_data_views_fields: "kibana_data_views_fields";
69
+ kibana_index_patterns_fields: "kibana_index_patterns_fields";
70
+ }>;
71
+ path: z.ZodOptional<z.ZodString>;
72
+ index: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodArray<z.ZodString>]>>;
73
+ }, z.core.$strip>>;
74
+ fieldHints: z.ZodDefault<z.ZodArray<z.ZodObject<{
75
+ name: z.ZodString;
76
+ type: z.ZodOptional<z.ZodString>;
77
+ description: z.ZodOptional<z.ZodString>;
78
+ aliases: z.ZodDefault<z.ZodArray<z.ZodString>>;
79
+ }, z.core.$strip>>>;
80
+ defaultTextFields: z.ZodDefault<z.ZodArray<z.ZodString>>;
81
+ evidenceFields: z.ZodDefault<z.ZodArray<z.ZodString>>;
82
+ }, z.core.$strip>>;
83
+ }, z.core.$strip>;
84
+ export declare function resolveSourceCatalogPath(envInput?: NodeJS.ProcessEnv): string;
85
+ export declare function parseAppConfig(envInput: unknown, sourceCatalogInput: unknown): AppConfig;
86
+ export declare function loadConfigFromEnvironment(envInput?: NodeJS.ProcessEnv, dependencies?: {
87
+ profileStore?: Pick<ProfileStore, "findProfile" | "getDefaultProfile">;
88
+ exactProfileName?: boolean;
89
+ secretStore?: Pick<SecretStore, "load">;
90
+ readFile?: typeof readFile;
91
+ }): Promise<ResolvedAppConfig>;
92
+ /**
93
+ * Reads the connection supplied through the bootstrap environment variables, if any.
94
+ */
95
+ export declare function readBootstrapConnection(envInput?: NodeJS.ProcessEnv): AppConfig["kibana"] | undefined;
96
+ export declare function persistSourceCatalog(sources: AppConfig["sources"], options?: {
97
+ envInput?: NodeJS.ProcessEnv;
98
+ sourceCatalogPath?: string;
99
+ catalog?: unknown;
100
+ }): Promise<string>;
@@ -1,12 +1,13 @@
1
- import { createHash, randomUUID } from "node:crypto";
2
- import { mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
3
- import { dirname } from "node:path";
1
+ import { createHash } from "node:crypto";
2
+ import { readFile } from "node:fs/promises";
4
3
  import { z } from "zod";
5
- import { ProfileStore } from "./profile_store.js";
6
- import { SecretStoreError, createSecretStore } from "./secret_store.js";
4
+ import { SecretStoreError, createSecretStore } from "../profiles/secret_store.js";
5
+ import { ProfileStore } from "../profiles/store.js";
6
+ import { writeTextAtomically } from "../utils/fs.js";
7
7
  export const DEFAULT_RUNTIME_SOURCE_CATALOG_PATH = "config/sources.runtime.json";
8
8
  export const DEFAULT_STATIC_SOURCE_CATALOG_PATH = "config/sources.json";
9
9
  export const PROFILE_NAME_ENV = "KIBANA_PROFILE";
10
+ export const PROFILE_PLACEHOLDER = "{profile}";
10
11
  export class NoSavedProfileError extends Error {
11
12
  constructor(message) {
12
13
  super(message);
@@ -129,17 +130,15 @@ export function parseAppConfig(envInput, sourceCatalogInput) {
129
130
  export async function loadConfigFromEnvironment(envInput = process.env, dependencies = {}) {
130
131
  const overrides = startupOverrideSchema.parse(envInput);
131
132
  const readFileImpl = dependencies.readFile ?? readFile;
132
- if (hasPartialBootstrapConfig(overrides)) {
133
- throw new Error("Incomplete Kibana bootstrap environment. Provide KIBANA_BASE_URL, KIBANA_USERNAME, and KIBANA_PASSWORD together.");
134
- }
135
- if (hasBootstrapConfig(overrides)) {
133
+ const kibana = readBootstrapConnection(envInput);
134
+ if (kibana) {
136
135
  const preferredPath = resolveSourceCatalogPath(envInput);
137
136
  const sourceCatalog = await loadSourceCatalog(preferredPath === DEFAULT_RUNTIME_SOURCE_CATALOG_PATH
138
137
  ? [preferredPath, DEFAULT_STATIC_SOURCE_CATALOG_PATH]
139
138
  : [preferredPath], readFileImpl);
140
- const config = parseAppConfig(overrides, sourceCatalog.value);
141
139
  return {
142
- ...config,
140
+ kibana,
141
+ sources: sourceCatalogSchema.parse(sourceCatalog.value).sources,
143
142
  sourceCatalogPath: preferredPath,
144
143
  sourceCatalogOrigin: sourceCatalog.loadedFrom === DEFAULT_STATIC_SOURCE_CATALOG_PATH
145
144
  ? "static_default"
@@ -152,7 +151,9 @@ export async function loadConfigFromEnvironment(envInput = process.env, dependen
152
151
  const secretStore = dependencies.secretStore ?? createSecretStore();
153
152
  const requestedProfileName = overrides[PROFILE_NAME_ENV]?.trim();
154
153
  const profile = requestedProfileName
155
- ? await profileStore.findProfile(requestedProfileName)
154
+ ? await profileStore.findProfile(requestedProfileName, {
155
+ exactName: dependencies.exactProfileName,
156
+ })
156
157
  : await profileStore.getDefaultProfile();
157
158
  if (!profile) {
158
159
  if (!requestedProfileName) {
@@ -183,20 +184,21 @@ export async function loadConfigFromEnvironment(envInput = process.env, dependen
183
184
  sourceCatalogOrigin: overrides.KIBANA_SOURCE_CATALOG_PATH?.trim() ? "environment" : "profile",
184
185
  };
185
186
  }
187
+ /**
188
+ * Reads the connection supplied through the bootstrap environment variables, if any.
189
+ */
190
+ export function readBootstrapConnection(envInput = process.env) {
191
+ const overrides = startupOverrideSchema.parse(envInput);
192
+ if (hasPartialBootstrapConfig(overrides)) {
193
+ throw new Error("Incomplete Kibana bootstrap environment. Provide KIBANA_BASE_URL, KIBANA_USERNAME, and KIBANA_PASSWORD together.");
194
+ }
195
+ return hasBootstrapConfig(overrides)
196
+ ? buildKibanaConfig(bootstrapEnvSchema.parse(overrides))
197
+ : undefined;
198
+ }
186
199
  export async function persistSourceCatalog(sources, options = {}) {
187
200
  const sourceCatalogPath = options.sourceCatalogPath ?? resolveSourceCatalogPath(options.envInput);
188
- await mkdir(dirname(sourceCatalogPath), { recursive: true });
189
- const temporaryPath = `${sourceCatalogPath}.${process.pid}.${randomUUID()}.tmp`;
190
- try {
191
- await writeFile(temporaryPath, `${JSON.stringify(options.catalog ?? { sources }, null, 2)}\n`, {
192
- encoding: "utf8",
193
- mode: 0o600,
194
- });
195
- await rename(temporaryPath, sourceCatalogPath);
196
- }
197
- finally {
198
- await rm(temporaryPath, { force: true }).catch(() => { });
199
- }
201
+ await writeTextAtomically(sourceCatalogPath, `${JSON.stringify(options.catalog ?? { sources }, null, 2)}\n`);
200
202
  return sourceCatalogPath;
201
203
  }
202
204
  function hasBootstrapConfig(envInput) {
@@ -0,0 +1,31 @@
1
+ import type { ProfileConfigLoader } from "../profiles/runtime.js";
2
+ import type { ResolvedAppConfig } from "../types.js";
3
+ import { type LoadedConfigFile } from "./config_file.js";
4
+ import { loadConfigFromEnvironment } from "./environment.js";
5
+ /** Profile served over stdio when per-profile catalogs are configured and none is named. */
6
+ export declare const DEFAULT_PROFILE_NAME = "default";
7
+ export interface StartupConfiguration {
8
+ defaultConfig?: ResolvedAppConfig;
9
+ loadProfileConfig?: ProfileConfigLoader;
10
+ /** Releases what the configuration opened, such as the catalog store. */
11
+ close: () => Promise<void>;
12
+ }
13
+ /**
14
+ * Loads the configuration file named by `KIBANA_MCP_CONFIG`, if any, and resolves the startup
15
+ * configuration from the environment completed with its values.
16
+ */
17
+ export declare function loadStartupConfiguration(envInput?: NodeJS.ProcessEnv): Promise<{
18
+ env: NodeJS.ProcessEnv;
19
+ startupConfiguration: StartupConfiguration;
20
+ }>;
21
+ /**
22
+ * Resolves what the server serves by default and, when the configuration allows it, how to load
23
+ * the configuration of a named profile on demand.
24
+ *
25
+ * Named profiles come from either the saved profile store, or from one connection shared by every
26
+ * profile whose catalogs live in a catalog store: the `store` of the configuration file, or files
27
+ * when `KIBANA_SOURCE_CATALOG_PATH` carries the `{profile}` placeholder.
28
+ */
29
+ export declare function resolveStartupConfiguration(envInput?: NodeJS.ProcessEnv, dependencies?: Parameters<typeof loadConfigFromEnvironment>[1] & {
30
+ configFile?: LoadedConfigFile;
31
+ }): Promise<StartupConfiguration>;
@@ -0,0 +1,86 @@
1
+ import { bindCatalogStore } from "../stores/catalog_store.js";
2
+ import { createFileCatalogStore } from "../stores/file.js";
3
+ import { CONFIG_FILE_ENV, applyConfigFile, loadConfigFile, } from "./config_file.js";
4
+ import { NoSavedProfileError, PROFILE_NAME_ENV, PROFILE_PLACEHOLDER, loadConfigFromEnvironment, readBootstrapConnection, } from "./environment.js";
5
+ /** Profile served over stdio when per-profile catalogs are configured and none is named. */
6
+ export const DEFAULT_PROFILE_NAME = "default";
7
+ /**
8
+ * Loads the configuration file named by `KIBANA_MCP_CONFIG`, if any, and resolves the startup
9
+ * configuration from the environment completed with its values.
10
+ */
11
+ export async function loadStartupConfiguration(envInput = process.env) {
12
+ const configFile = await loadConfigFile(envInput[CONFIG_FILE_ENV]);
13
+ const env = configFile ? applyConfigFile(envInput, configFile) : envInput;
14
+ return { env, startupConfiguration: await resolveStartupConfiguration(env, { configFile }) };
15
+ }
16
+ /**
17
+ * Resolves what the server serves by default and, when the configuration allows it, how to load
18
+ * the configuration of a named profile on demand.
19
+ *
20
+ * Named profiles come from either the saved profile store, or from one connection shared by every
21
+ * profile whose catalogs live in a catalog store: the `store` of the configuration file, or files
22
+ * when `KIBANA_SOURCE_CATALOG_PATH` carries the `{profile}` placeholder.
23
+ */
24
+ export async function resolveStartupConfiguration(envInput = process.env, dependencies = {}) {
25
+ const customStore = dependencies.configFile?.store;
26
+ const close = async () => {
27
+ await customStore?.close?.();
28
+ };
29
+ try {
30
+ return { ...(await resolveConfiguration(envInput, dependencies, customStore)), close };
31
+ }
32
+ catch (error) {
33
+ await close().catch(() => { });
34
+ throw error;
35
+ }
36
+ }
37
+ async function resolveConfiguration(envInput, dependencies, customStore) {
38
+ const kibana = readBootstrapConnection(envInput);
39
+ const requestedProfileName = envInput[PROFILE_NAME_ENV]?.trim();
40
+ const catalogPath = envInput.KIBANA_SOURCE_CATALOG_PATH?.trim();
41
+ const catalogPathTemplate = catalogPath?.includes(PROFILE_PLACEHOLDER) ? catalogPath : undefined;
42
+ if (customStore && catalogPath) {
43
+ throw new Error("KIBANA_SOURCE_CATALOG_PATH cannot be combined with the store of the configuration file.");
44
+ }
45
+ if (kibana) {
46
+ const store = customStore ??
47
+ (catalogPathTemplate
48
+ ? createFileCatalogStore((profileKey) => catalogPathTemplate.replaceAll(PROFILE_PLACEHOLDER, profileKey))
49
+ : undefined);
50
+ if (!store) {
51
+ return { defaultConfig: await loadConfigFromEnvironment(envInput, dependencies) };
52
+ }
53
+ // Sources are read from the store when the profile is resolved, not here.
54
+ const loadProfileConfig = async (profileName) => ({
55
+ kibana,
56
+ sources: [],
57
+ profileName,
58
+ catalog: bindCatalogStore(store, profileName),
59
+ });
60
+ // No profile is assumed here: a transport serving several callers must not fall back to a
61
+ // catalog shared by all of them unless the operator named it.
62
+ return {
63
+ defaultConfig: requestedProfileName
64
+ ? await loadProfileConfig(requestedProfileName)
65
+ : undefined,
66
+ loadProfileConfig,
67
+ };
68
+ }
69
+ if (customStore || catalogPathTemplate) {
70
+ throw new Error("Per-profile catalogs (a store, or {profile} in KIBANA_SOURCE_CATALOG_PATH) require a Kibana url, user and password.");
71
+ }
72
+ // Named saved profiles carry their own catalog: the global catalog override is not applied.
73
+ const loadProfileConfig = (profileName) => loadConfigFromEnvironment({ KIBANA_TIMEOUT_MS: envInput.KIBANA_TIMEOUT_MS, [PROFILE_NAME_ENV]: profileName }, { ...dependencies, exactProfileName: true });
74
+ try {
75
+ return {
76
+ defaultConfig: await loadConfigFromEnvironment(envInput, dependencies),
77
+ loadProfileConfig,
78
+ };
79
+ }
80
+ catch (error) {
81
+ if (error instanceof NoSavedProfileError) {
82
+ return {};
83
+ }
84
+ throw error;
85
+ }
86
+ }
@@ -0,0 +1,28 @@
1
+ import type { AppConfig, CompiledSourceQuery, KibanaSearchExecutionResult, SourceDefinition, SourceFieldDescriptor } from "../types.js";
2
+ export type KibanaRequestErrorCode = "KIBANA_AUTHENTICATION" | "KIBANA_CANCELLED" | "KIBANA_CONNECTION" | "KIBANA_DNS" | "KIBANA_HTTP" | "KIBANA_NETWORK" | "KIBANA_OVERLOADED" | "KIBANA_RESPONSE" | "KIBANA_TIMEOUT" | "KIBANA_TLS";
3
+ export type KibanaRequestPhase = "queue" | "request" | "response";
4
+ export declare class KibanaRequestError extends Error {
5
+ readonly code: KibanaRequestErrorCode;
6
+ readonly phase: KibanaRequestPhase;
7
+ readonly sourceId: string | undefined;
8
+ readonly timeoutMs: number;
9
+ readonly status?: number | undefined;
10
+ readonly causeCode?: string | undefined;
11
+ constructor(code: KibanaRequestErrorCode, phase: KibanaRequestPhase, sourceId: string | undefined, timeoutMs: number, message: string, cause?: unknown, status?: number | undefined, causeCode?: string | undefined);
12
+ }
13
+ export declare class KibanaClient {
14
+ private readonly config;
15
+ private readonly requestGate;
16
+ private readonly queueTimeoutMs;
17
+ constructor(config: AppConfig["kibana"], options?: {
18
+ maxConcurrency?: number;
19
+ maxQueueDepth?: number;
20
+ queueTimeoutMs?: number;
21
+ });
22
+ private requestJson;
23
+ verifyConnection(): Promise<void>;
24
+ execute(compiledQuery: CompiledSourceQuery, callerSignal?: AbortSignal): Promise<KibanaSearchExecutionResult>;
25
+ executeMany(sourceQueries: CompiledSourceQuery[], callerSignal?: AbortSignal): Promise<KibanaSearchExecutionResult[]>;
26
+ private describeFieldsViaSearchBackend;
27
+ describeFields(source: SourceDefinition, callerSignal?: AbortSignal): Promise<SourceFieldDescriptor[]>;
28
+ }
@@ -0,0 +1,11 @@
1
+ import type { SourceDefinition, SourceFieldDescriptor } from "../types.js";
2
+ export declare class SchemaCatalog {
3
+ private readonly client;
4
+ private readonly cache;
5
+ private readonly inFlight;
6
+ constructor(client: {
7
+ describeFields: (source: SourceDefinition, callerSignal?: AbortSignal) => Promise<SourceFieldDescriptor[]>;
8
+ });
9
+ getFields(source: SourceDefinition, callerSignal?: AbortSignal): Promise<SourceFieldDescriptor[]>;
10
+ filterFields(fields: SourceFieldDescriptor[], query?: string, limit?: number): SourceFieldDescriptor[];
11
+ }
@@ -1,3 +1,4 @@
1
+ import { KibanaRequestError } from "./client.js";
1
2
  function normalizeFieldName(value) {
2
3
  return value.trim().toLowerCase();
3
4
  }
@@ -59,26 +60,47 @@ export class SchemaCatalog {
59
60
  this.client = client;
60
61
  }
61
62
  async getFields(source, callerSignal) {
63
+ const cancelled = () => new KibanaRequestError("KIBANA_CANCELLED", "request", source.id, 0, `[KIBANA_CANCELLED] Schema discovery for source '${source.id}' was cancelled.`, callerSignal?.reason);
64
+ if (callerSignal?.aborted) {
65
+ throw cancelled();
66
+ }
62
67
  const existing = this.cache.get(source.id);
63
68
  if (existing) {
64
69
  return existing;
65
70
  }
66
- const pending = this.inFlight.get(source.id);
67
- if (pending) {
68
- return pending;
71
+ let pending = this.inFlight.get(source.id);
72
+ if (!pending) {
73
+ const controller = new AbortController();
74
+ const promise = this.client.describeFields(source, controller.signal).then((fields) => {
75
+ const resolved = linkFieldRelationships(mergeFieldHints(source, fields));
76
+ if (!controller.signal.aborted) {
77
+ this.cache.set(source.id, resolved);
78
+ }
79
+ return resolved;
80
+ });
81
+ pending = { promise, controller, callers: 0 };
82
+ this.inFlight.set(source.id, pending);
69
83
  }
70
- const discovery = this.client.describeFields(source, callerSignal).then((fields) => {
71
- const resolved = linkFieldRelationships(mergeFieldHints(source, fields));
72
- this.cache.set(source.id, resolved);
73
- return resolved;
74
- });
75
- this.inFlight.set(source.id, discovery);
84
+ const discovery = pending;
85
+ discovery.callers += 1;
86
+ let onAbort;
76
87
  try {
77
- return await discovery;
88
+ return await new Promise((resolve, reject) => {
89
+ onAbort = () => reject(cancelled());
90
+ callerSignal?.addEventListener("abort", onAbort, { once: true });
91
+ discovery.promise.then(resolve, reject);
92
+ if (callerSignal?.aborted)
93
+ onAbort();
94
+ });
78
95
  }
79
96
  finally {
80
- if (this.inFlight.get(source.id) === discovery) {
97
+ if (onAbort)
98
+ callerSignal?.removeEventListener("abort", onAbort);
99
+ discovery.callers -= 1;
100
+ if (discovery.callers === 0 && this.inFlight.get(source.id) === discovery) {
81
101
  this.inFlight.delete(source.id);
102
+ if (callerSignal?.aborted)
103
+ discovery.controller.abort();
82
104
  }
83
105
  }
84
106
  }
@@ -0,0 +1,10 @@
1
+ import type { DiscoverSourceResult, SourceDefinition } from "../types.js";
2
+ export declare class SourceCatalog {
3
+ private readonly sources;
4
+ private readonly sourceMap;
5
+ constructor(sources: SourceDefinition[]);
6
+ count(): number;
7
+ list(query?: string, limit?: number): DiscoverSourceResult[];
8
+ getRequiredSources(sourceIds: string[]): SourceDefinition[];
9
+ private matchesQuery;
10
+ }
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { startMcpServer } from "./mcp_runtime.js";
2
+ import { startMcpServer } from "./transports/stdio.js";
3
3
  startMcpServer().catch((error) => {
4
4
  console.error(error);
5
5
  process.exitCode = 1;
@@ -0,0 +1,4 @@
1
+ import type { ProfileRuntime } from "./runtime.js";
2
+ export declare function runWithProfileRuntime<T>(runtime: ProfileRuntime, operation: () => T): T;
3
+ export declare function getProfileRuntime(): ProfileRuntime | undefined;
4
+ export declare function requireProfileRuntime(): ProfileRuntime;
@@ -0,0 +1,15 @@
1
+ import { AsyncLocalStorage } from "node:async_hooks";
2
+ const profileRuntimeStorage = new AsyncLocalStorage();
3
+ export function runWithProfileRuntime(runtime, operation) {
4
+ return profileRuntimeStorage.run(runtime, operation);
5
+ }
6
+ export function getProfileRuntime() {
7
+ return profileRuntimeStorage.getStore();
8
+ }
9
+ export function requireProfileRuntime() {
10
+ const runtime = profileRuntimeStorage.getStore();
11
+ if (!runtime) {
12
+ throw new Error("No profile is bound to the current tool call.");
13
+ }
14
+ return runtime;
15
+ }