@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
@@ -1,7 +1,7 @@
1
1
  import { McpServer } from "@modelcontextprotocol/server";
2
- import { KibanaClient, KibanaRequestError } from "./kibana_client.js";
3
- import { SchemaCatalog } from "./schema_catalog.js";
4
- import { SourceCatalog } from "./source_catalog.js";
2
+ import { KibanaRequestError } from "./kibana/client.js";
3
+ import { requireProfileRuntime, runWithProfileRuntime } from "./profiles/context.js";
4
+ import { ProfileRuntimeRegistry } from "./profiles/runtime.js";
5
5
  import { configureInputSchema, configureOutputSchema, createConfigureCallToolResult, executeConfigure, } from "./tools/configure.js";
6
6
  import { configureIndexInputSchema, configureIndexOutputSchema, createConfigureIndexCallToolResult, executeConfigureIndex, } from "./tools/configure_index.js";
7
7
  import { createDescribeFieldsCallToolResult, describeFieldsInputSchema, describeFieldsOutputSchema, executeDescribeFields, } from "./tools/describe_fields.js";
@@ -37,19 +37,19 @@ export function createApplication(initialConfig, dependencies) {
37
37
  name: "kibana-log-investigation",
38
38
  version: "0.1.0",
39
39
  });
40
- const kibanaClientFactory = dependencies?.kibanaClientFactory ??
41
- ((config) => new KibanaClient(config));
42
- let activeConfig = initialConfig;
43
- let sourceCatalog = activeConfig ? new SourceCatalog(activeConfig.sources) : null;
44
- let kibanaClient = activeConfig
45
- ? (dependencies?.kibanaClient ?? kibanaClientFactory(activeConfig.kibana))
46
- : null;
47
- let schemaCatalog = kibanaClient ? new SchemaCatalog(kibanaClient) : null;
40
+ const registry = dependencies?.profileRegistry ??
41
+ new ProfileRuntimeRegistry({
42
+ defaultConfig: initialConfig,
43
+ defaultKibanaClient: dependencies?.kibanaClient,
44
+ kibanaClientFactory: dependencies?.kibanaClientFactory,
45
+ });
46
+ // The profile is resolved once per tool call, then read back through the async context.
47
+ async function withProfile(operation) {
48
+ const runtime = await registry.resolve(dependencies?.requestProfile);
49
+ return runWithProfileRuntime(runtime, () => operation(runtime));
50
+ }
48
51
  async function withKibanaToolDeadline(callerSignal, operation) {
49
- const timeoutMs = activeConfig?.kibana.timeoutMs;
50
- if (timeoutMs === undefined) {
51
- throw new Error("Server is not configured. Call the 'configure' tool first.");
52
- }
52
+ const timeoutMs = requireProfileRuntime().config.kibana.timeoutMs;
53
53
  const timeoutSignal = AbortSignal.timeout(timeoutMs);
54
54
  const signal = callerSignal ? AbortSignal.any([callerSignal, timeoutSignal]) : timeoutSignal;
55
55
  try {
@@ -63,55 +63,45 @@ export function createApplication(initialConfig, dependencies) {
63
63
  throw new KibanaRequestError("KIBANA_TIMEOUT", requestError?.phase ?? "request", requestError?.sourceId, timeoutMs, `Kibana tool call exceeded its ${timeoutMs} ms backend deadline.`, error, requestError?.status, requestError?.causeCode);
64
64
  }
65
65
  }
66
- function requireConfigured() {
67
- if (!sourceCatalog || !kibanaClient || !schemaCatalog) {
68
- throw new Error("Server is not configured. Call the 'configure' tool first.");
69
- }
70
- return { sourceCatalog, kibanaClient, schemaCatalog };
71
- }
72
66
  const configureHandler = async (input) => {
67
+ const current = registry.getDefault()?.config;
73
68
  const { nextConfig, result } = await executeConfigure(configureInputSchema.parse(input), {
74
- sourceCatalogPath: activeConfig?.sourceCatalogPath,
69
+ sourceCatalogPath: current?.sourceCatalogPath,
75
70
  });
76
- activeConfig = {
71
+ registry.setDefault({
77
72
  ...nextConfig,
78
73
  sourceCatalogPath: result.source_catalog_path,
79
- profileName: activeConfig?.profileName,
80
- sourceCatalogOrigin: activeConfig?.sourceCatalogOrigin,
81
- };
82
- sourceCatalog = new SourceCatalog(nextConfig.sources);
83
- kibanaClient = kibanaClientFactory(nextConfig.kibana);
84
- schemaCatalog = new SchemaCatalog(kibanaClient);
74
+ profileName: current?.profileName,
75
+ sourceCatalogOrigin: current?.sourceCatalogOrigin,
76
+ });
85
77
  return result;
86
78
  };
87
79
  const configureIndexHandler = async (input, callerSignal) => {
88
- if (!activeConfig || !kibanaClient) {
80
+ if (!registry.isConfigured) {
89
81
  throw new Error("No saved Kibana connection is available. Run connection bootstrap first.");
90
82
  }
91
- const configured = activeConfig;
92
- const client = kibanaClient;
93
83
  const configureIndexFn = dependencies?.configureIndexFn ?? executeConfigureIndex;
94
- const { sources, result } = await withKibanaToolDeadline(callerSignal, (signal) => configureIndexFn(configureIndexInputSchema.parse(input), {
95
- config: configured,
96
- kibanaClient: client,
97
- callerSignal: signal,
84
+ return withProfile((runtime) => withKibanaToolDeadline(callerSignal, async (signal) => {
85
+ const { sources, result, catalogContent } = await configureIndexFn(configureIndexInputSchema.parse(input), {
86
+ config: runtime.config,
87
+ kibanaClient: runtime.kibanaClient,
88
+ callerSignal: signal,
89
+ });
90
+ registry.replaceSources(runtime, sources, catalogContent);
91
+ return result;
98
92
  }));
99
- activeConfig = { ...activeConfig, sources };
100
- sourceCatalog = new SourceCatalog(sources);
101
- schemaCatalog = new SchemaCatalog(client);
102
- return result;
103
93
  };
104
- const describeFieldsHandler = (input, callerSignal) => withKibanaToolDeadline(callerSignal, (signal) => executeDescribeFields(describeFieldsInputSchema.parse(input), requireConfigured().sourceCatalog, requireConfigured().schemaCatalog, signal));
105
- const discoverHandler = (input) => executeDiscover(discoverInputSchema.parse(input), requireConfigured().sourceCatalog);
106
- const filterHandler = (input, callerSignal) => withKibanaToolDeadline(callerSignal, (signal) => executeFilter(filterInputSchema.parse(input), requireConfigured().sourceCatalog, requireConfigured().kibanaClient, {
107
- schemaCatalog: requireConfigured().schemaCatalog,
94
+ const describeFieldsHandler = (input, callerSignal) => withProfile((runtime) => withKibanaToolDeadline(callerSignal, (signal) => executeDescribeFields(describeFieldsInputSchema.parse(input), runtime.sourceCatalog, runtime.schemaCatalog, signal)));
95
+ const discoverHandler = (input) => withProfile((runtime) => executeDiscover(discoverInputSchema.parse(input), runtime.sourceCatalog));
96
+ const filterHandler = (input, callerSignal) => withProfile((runtime) => withKibanaToolDeadline(callerSignal, (signal) => executeFilter(filterInputSchema.parse(input), runtime.sourceCatalog, runtime.kibanaClient, {
97
+ schemaCatalog: runtime.schemaCatalog,
108
98
  callerSignal: signal,
109
- }));
110
- const queryHandler = (input, callerSignal) => withKibanaToolDeadline(callerSignal, (signal) => executeQuery(queryInputSchema.parse(input), requireConfigured().sourceCatalog, requireConfigured().kibanaClient, {
111
- schemaCatalog: requireConfigured().schemaCatalog,
99
+ })));
100
+ const queryHandler = (input, callerSignal) => withProfile((runtime) => withKibanaToolDeadline(callerSignal, (signal) => executeQuery(queryInputSchema.parse(input), runtime.sourceCatalog, runtime.kibanaClient, {
101
+ schemaCatalog: runtime.schemaCatalog,
112
102
  callerSignal: signal,
113
- }));
114
- if (!initialConfig) {
103
+ })));
104
+ if (!registry.isConfigured) {
115
105
  server.registerTool("configure", {
116
106
  description: "Advanced legacy setup for a server started without a saved profile. Replaces the Kibana connection and full source catalog.",
117
107
  inputSchema: configureInputSchema,
@@ -134,7 +124,7 @@ export function createApplication(initialConfig, dependencies) {
134
124
  description: "List configured logical log sources and field hints for investigation work.",
135
125
  inputSchema: discoverInputSchema,
136
126
  outputSchema: discoverOutputSchema,
137
- }, async (input) => createDiscoverCallToolResult(discoverHandler(input)));
127
+ }, async (input) => createDiscoverCallToolResult(await discoverHandler(input)));
138
128
  server.registerTool("filter", {
139
129
  description: "Run an exact-field filter when the field name is already known, bypassing alias resolution.",
140
130
  inputSchema: filterInputSchema,
@@ -0,0 +1,50 @@
1
+ import { sourceCatalogSchema } from "../config/environment.js";
2
+ import { type ProfilePaths } from "../profiles/paths.js";
3
+ import { type SecretStore } from "../profiles/secret_store.js";
4
+ import { ProfileStore } from "../profiles/store.js";
5
+ import type { KibanaConnectionConfig, SourceDefinition, SourceFieldDescriptor } from "../types.js";
6
+ import { type ClientRegistrar } from "./client_registration.js";
7
+ export type BootstrapClient = "codex" | "none";
8
+ export interface BootstrapOptions {
9
+ profileName: string;
10
+ baseUrl: string;
11
+ username: string;
12
+ password: string;
13
+ client: BootstrapClient;
14
+ makeDefault: boolean;
15
+ timeoutMs?: number;
16
+ mcpName?: string;
17
+ packageSpecifier?: string;
18
+ replaceExisting?: boolean;
19
+ }
20
+ export interface BootstrapResult {
21
+ profileName: string;
22
+ profileId: string;
23
+ sourceCount: number;
24
+ sourceCatalogPath: string;
25
+ client: BootstrapClient;
26
+ registered: boolean;
27
+ verified: boolean;
28
+ }
29
+ export interface BootstrapVerifier {
30
+ verify(connection: KibanaConnectionConfig): Promise<void>;
31
+ }
32
+ interface BootstrapDependencies {
33
+ paths?: ProfilePaths;
34
+ profileStore?: ProfileStore;
35
+ secretStore?: SecretStore;
36
+ verifier?: BootstrapVerifier;
37
+ clientRegistrar?: ClientRegistrar;
38
+ }
39
+ interface GeneratedSourceFields {
40
+ timeField: string;
41
+ defaultTextFields: string[];
42
+ evidenceFields: string[];
43
+ }
44
+ export declare function buildGeneratedSource(rawIndexes: string[], sourceName?: string, sourceId?: string): SourceDefinition;
45
+ export declare function chooseGeneratedSourceFields(fields: SourceFieldDescriptor[], explicitTimeField?: string): GeneratedSourceFields;
46
+ export declare function runBootstrap(rawOptions: BootstrapOptions, dependencies?: BootstrapDependencies): Promise<BootstrapResult>;
47
+ export declare function resolveCurrentPackageSpecifier(): Promise<string>;
48
+ export declare function createGeneratedCatalog(sourceOrSources: SourceDefinition | SourceDefinition[]): ReturnType<typeof sourceCatalogSchema.parse>;
49
+ export declare function parseGeneratedCatalog(content: string): ReturnType<typeof sourceCatalogSchema.parse> | undefined;
50
+ export {};
@@ -1,14 +1,15 @@
1
- import { createHash, randomUUID } from "node:crypto";
2
- import { mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
1
+ import { createHash } from "node:crypto";
2
+ import { rm } from "node:fs/promises";
3
3
  import { dirname, join } from "node:path";
4
4
  import { fileURLToPath } from "node:url";
5
+ import { sourceCatalogSchema, sourceDefinitionSchema } from "../config/environment.js";
6
+ import { KibanaClient } from "../kibana/client.js";
7
+ import { resolveProfilePaths } from "../profiles/paths.js";
8
+ import { SecretStoreError, createSecretStore } from "../profiles/secret_store.js";
9
+ import { acquireStateLock } from "../profiles/state_lock.js";
10
+ import { ProfileStore, deriveProfileId } from "../profiles/store.js";
11
+ import { readOptionalFile, writeTextAtomically } from "../utils/fs.js";
5
12
  import { CodexClientRegistrar } from "./client_registration.js";
6
- import { sourceCatalogSchema, sourceDefinitionSchema } from "./config.js";
7
- import { KibanaClient } from "./kibana_client.js";
8
- import { resolveProfilePaths } from "./profile_paths.js";
9
- import { ProfileStore, deriveProfileId } from "./profile_store.js";
10
- import { SecretStoreError, createSecretStore } from "./secret_store.js";
11
- import { acquireStateLock } from "./state_lock.js";
12
13
  const DEFAULT_TIMEOUT_MS = 10000;
13
14
  const DEFAULT_MCP_NAME = "kibana-log-investigation";
14
15
  const KNOWN_ENDPOINT_PATHS = [
@@ -244,8 +245,8 @@ function normalizeOptions(options) {
244
245
  export async function resolveCurrentPackageSpecifier() {
245
246
  const moduleDirectory = dirname(fileURLToPath(import.meta.url));
246
247
  const candidates = [
247
- join(moduleDirectory, "..", "package.json"),
248
248
  join(moduleDirectory, "..", "..", "package.json"),
249
+ join(moduleDirectory, "..", "..", "..", "package.json"),
249
250
  ];
250
251
  for (const candidate of candidates) {
251
252
  const content = await readOptionalFile(candidate);
@@ -359,33 +360,9 @@ function normalizeBaseUrl(rawValue) {
359
360
  }
360
361
  return parsed.toString().replace(/\/+$/, "");
361
362
  }
362
- async function readOptionalFile(path) {
363
- try {
364
- return await readFile(path, "utf8");
365
- }
366
- catch (error) {
367
- if (isMissingFileError(error))
368
- return undefined;
369
- throw error;
370
- }
371
- }
372
363
  async function writeJsonAtomically(path, value) {
373
364
  await writeTextAtomically(path, `${JSON.stringify(value, null, 2)}\n`);
374
365
  }
375
- async function writeTextAtomically(path, content) {
376
- const temporaryPath = `${path}.${process.pid}.${randomUUID()}.tmp`;
377
- await mkdir(dirname(path), { recursive: true });
378
- try {
379
- await writeFile(temporaryPath, content, {
380
- encoding: "utf8",
381
- mode: 0o600,
382
- });
383
- await rename(temporaryPath, path);
384
- }
385
- finally {
386
- await rm(temporaryPath, { force: true }).catch(() => { });
387
- }
388
- }
389
366
  async function restoreFile(path, previousContent) {
390
367
  if (previousContent === undefined) {
391
368
  await rm(path, { force: true });
@@ -400,9 +377,6 @@ async function restoreSecret(secretStore, profileId, previousSecret) {
400
377
  }
401
378
  await secretStore.delete(profileId);
402
379
  }
403
- function isMissingFileError(error) {
404
- return Boolean(error && typeof error === "object" && "code" in error && error.code === "ENOENT");
405
- }
406
380
  function formatIndex(index) {
407
381
  if (!index)
408
382
  return "unknown";
@@ -0,0 +1,38 @@
1
+ export interface CommandResult {
2
+ stdout: string;
3
+ stderr: string;
4
+ exitCode: number;
5
+ }
6
+ export type CommandRunner = (command: string, args: string[]) => Promise<CommandResult>;
7
+ export interface ClientRegistrationPreflight {
8
+ alreadyRegistered: boolean;
9
+ }
10
+ export interface ClientRegistrationResult {
11
+ added: boolean;
12
+ }
13
+ export interface ClientRegistrar {
14
+ preflight(): Promise<ClientRegistrationPreflight>;
15
+ register(): Promise<ClientRegistrationResult>;
16
+ rollback(): Promise<void>;
17
+ }
18
+ export interface CodexRegistrationOptions {
19
+ mcpName: string;
20
+ profileName: string;
21
+ packageSpecifier: string;
22
+ }
23
+ export declare function deriveCodexRegistrationName(options: CodexRegistrationOptions): string;
24
+ export declare class CodexClientRegistrar implements ClientRegistrar {
25
+ private readonly options;
26
+ private readonly runner;
27
+ private preflightResult;
28
+ private rollbackEligible;
29
+ constructor(options: CodexRegistrationOptions, runner?: CommandRunner);
30
+ private get registrationName();
31
+ preflight(): Promise<ClientRegistrationPreflight>;
32
+ register(): Promise<ClientRegistrationResult>;
33
+ private reconcileAmbiguousAdd;
34
+ rollback(): Promise<void>;
35
+ private expectedCommand;
36
+ private matchesExpectedTransport;
37
+ private readRegistration;
38
+ }
@@ -0,0 +1,26 @@
1
+ import { mkdir, readFile, writeFile } from "node:fs/promises";
2
+ import { type ProfilePaths } from "../profiles/paths.js";
3
+ import { type SecretStore } from "../profiles/secret_store.js";
4
+ import { ProfileStore } from "../profiles/store.js";
5
+ export interface SetupPrompter {
6
+ info(message: string): void | Promise<void>;
7
+ prompt(message: string, options?: {
8
+ defaultValue?: string;
9
+ secret?: boolean;
10
+ }): Promise<string>;
11
+ confirm(message: string, defaultValue?: boolean): Promise<boolean>;
12
+ }
13
+ export interface SetupFlowResult {
14
+ defaultProfileName: string;
15
+ profiles: string[];
16
+ sourceCatalogPaths: string[];
17
+ }
18
+ export declare function runSetupFlow(prompter: SetupPrompter, dependencies?: {
19
+ profileStore?: ProfileStore;
20
+ secretStore?: SecretStore;
21
+ cwd?: string;
22
+ paths?: ProfilePaths;
23
+ readFile?: typeof readFile;
24
+ writeFile?: typeof writeFile;
25
+ mkdir?: typeof mkdir;
26
+ }): Promise<SetupFlowResult>;
@@ -1,11 +1,11 @@
1
1
  import { access, mkdir, readFile, writeFile } from "node:fs/promises";
2
2
  import { dirname, join, resolve } from "node:path";
3
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
- import { acquireStateLock } from "./state_lock.js";
4
+ import { sourceCatalogSchema } from "../config/environment.js";
5
+ import { resolveProfilePaths } from "../profiles/paths.js";
6
+ import { SecretStoreError, createSecretStore } from "../profiles/secret_store.js";
7
+ import { acquireStateLock } from "../profiles/state_lock.js";
8
+ import { ProfileStore, deriveProfileId } from "../profiles/store.js";
9
9
  const DEFAULT_TIMEOUT_MS = 10000;
10
10
  const KNOWN_ENDPOINT_PATHS = [
11
11
  "/internal/search/es",
@@ -158,8 +158,8 @@ async function copyValidatedSourceCatalog(importPath, destinationPath, io) {
158
158
  }
159
159
  async function resolveBundledSourceCatalogPath() {
160
160
  const candidates = [
161
- new URL("../config/sources.example.json", import.meta.url),
162
161
  new URL("../../config/sources.example.json", import.meta.url),
162
+ new URL("../../../config/sources.example.json", import.meta.url),
163
163
  ].map((candidate) => fileURLToPath(candidate));
164
164
  for (const candidate of candidates) {
165
165
  try {
@@ -0,0 +1,38 @@
1
+ export declare class CatalogConflictError extends Error {
2
+ constructor(location: string);
3
+ }
4
+ /**
5
+ * Storage of the serialized source catalogs, one per profile. Implement it to keep catalogs
6
+ * anywhere (a database, a key-value store, an object bucket) and register it in the configuration
7
+ * file.
8
+ *
9
+ * `profileKey` is a filesystem-safe identifier derived from the profile name: lowercase letters,
10
+ * digits and dashes only.
11
+ */
12
+ export interface CatalogStore {
13
+ /** Returns the serialized catalog of the profile, or `undefined` when it has none yet. */
14
+ read(profileKey: string): Promise<string | undefined>;
15
+ /**
16
+ * Replaces the catalog of the profile. `expected` is the content the caller read before
17
+ * computing `next` (`undefined` when there was none): a store shared by several processes should
18
+ * write only if it still holds `expected`, and throw {@link CatalogConflictError} otherwise.
19
+ * When supplied, `signal` must be checked after any wait and before issuing the write.
20
+ * Cancellation after the write starts does not roll back a committed catalog.
21
+ */
22
+ replace(profileKey: string, expected: string | undefined, next: string, signal?: AbortSignal): Promise<void>;
23
+ /** Human-readable location of a catalog, reported by tools. Must not expose credentials. */
24
+ describe?(profileKey: string): string;
25
+ /** Releases the resources held by the store when the server stops. */
26
+ close?(): Promise<void>;
27
+ }
28
+ /** A {@link CatalogStore} bound to one profile. */
29
+ export interface CatalogHandle {
30
+ location: string;
31
+ read(): Promise<string | undefined>;
32
+ replace(expected: string | undefined, next: string, signal?: AbortSignal): Promise<void>;
33
+ }
34
+ /**
35
+ * Binds a store to one profile. The key handed to the store is derived here, so that no caller
36
+ * can reach a store with a name that is unsafe as a path or key.
37
+ */
38
+ export declare function bindCatalogStore(store: CatalogStore, profileName: string): CatalogHandle;
@@ -0,0 +1,22 @@
1
+ import { toProfileKey } from "../profiles/names.js";
2
+ export class CatalogConflictError extends Error {
3
+ constructor(location) {
4
+ super(`Source catalog '${location}' changed concurrently. Retry the operation.`);
5
+ this.name = "CatalogConflictError";
6
+ }
7
+ }
8
+ /**
9
+ * Binds a store to one profile. The key handed to the store is derived here, so that no caller
10
+ * can reach a store with a name that is unsafe as a path or key.
11
+ */
12
+ export function bindCatalogStore(store, profileName) {
13
+ const profileKey = toProfileKey(profileName);
14
+ return {
15
+ location: store.describe?.(profileKey) ?? profileKey,
16
+ read: () => store.read(profileKey),
17
+ replace: async (expected, next, signal) => {
18
+ signal?.throwIfAborted();
19
+ await store.replace(profileKey, expected, next, signal);
20
+ },
21
+ };
22
+ }
@@ -0,0 +1,5 @@
1
+ import { type CatalogStore } from "./catalog_store.js";
2
+ /**
3
+ * Catalog store keeping one file per profile, at the path `resolvePath` gives for its key.
4
+ */
5
+ export declare function createFileCatalogStore(resolvePath: (profileKey: string) => string, stateRoot?: string): CatalogStore;
@@ -0,0 +1,48 @@
1
+ import { createHash } from "node:crypto";
2
+ import { mkdir, realpath } from "node:fs/promises";
3
+ import { basename, dirname, join } from "node:path";
4
+ import { resolveProfilePaths } from "../profiles/paths.js";
5
+ import { acquireStateLock } from "../profiles/state_lock.js";
6
+ import { isMissingFileError, readOptionalFile, writeTextAtomically } from "../utils/fs.js";
7
+ import { CatalogConflictError } from "./catalog_store.js";
8
+ /**
9
+ * Catalog store keeping one file per profile, at the path `resolvePath` gives for its key.
10
+ */
11
+ export function createFileCatalogStore(resolvePath, stateRoot = resolveProfilePaths().stateRoot) {
12
+ return {
13
+ read: (profileKey) => readOptionalFile(resolvePath(profileKey)),
14
+ describe: resolvePath,
15
+ async replace(profileKey, expected, next, signal) {
16
+ const path = resolvePath(profileKey);
17
+ // Keep bootstrap's machine-state coordination, then lock the actual catalog independently
18
+ // of that root. All file-store writers acquire these locks in the same order.
19
+ const releaseLock = await acquireStateLock(stateRoot, { signal });
20
+ try {
21
+ await mkdir(dirname(path), { recursive: true });
22
+ const canonicalPath = await realpath(path).catch(async (error) => {
23
+ if (!isMissingFileError(error))
24
+ throw error;
25
+ return join(await realpath(dirname(path)), basename(path));
26
+ });
27
+ const digest = createHash("sha256").update(canonicalPath).digest("hex");
28
+ const releaseCatalogLock = await acquireStateLock(dirname(canonicalPath), {
29
+ lockName: `.catalog-${digest}.lock`,
30
+ signal,
31
+ });
32
+ try {
33
+ if ((await readOptionalFile(canonicalPath)) !== expected) {
34
+ throw new CatalogConflictError(path);
35
+ }
36
+ signal?.throwIfAborted();
37
+ await writeTextAtomically(canonicalPath, next);
38
+ }
39
+ finally {
40
+ await releaseCatalogLock();
41
+ }
42
+ }
43
+ finally {
44
+ await releaseLock();
45
+ }
46
+ },
47
+ };
48
+ }
@@ -0,0 +1,21 @@
1
+ import type { CatalogStore } from "./catalog_store.js";
2
+ export interface IoredisCatalogStoreOptions {
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
+ /** An `ioredis` client (standalone, Sentinel or Cluster) to use instead of `url`. The store never closes it. */
8
+ client?: IoredisCatalogClient;
9
+ }
10
+ /** The subset of the `ioredis` client this store relies on. */
11
+ export interface IoredisCatalogClient {
12
+ get(key: string): Promise<string | null>;
13
+ eval(script: string, numberOfKeys: number, ...keysAndArguments: string[]): Promise<unknown>;
14
+ }
15
+ /**
16
+ * Catalog store backed by Redis through the `ioredis` package: one key per profile, without
17
+ * expiry. Use a Redis with persistence and a `noeviction` policy, catalogs are not a cache.
18
+ *
19
+ * Usable directly as `store` in the configuration file; its options come from `storeOptions`.
20
+ */
21
+ export default function ioredisCatalogStore(options?: IoredisCatalogStoreOptions): Promise<CatalogStore>;
@@ -0,0 +1,33 @@
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 `ioredis` package: one key per profile, without
4
+ * expiry. 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 ioredisCatalogStore(options = {}) {
9
+ let client = options.client;
10
+ let label = "redis:";
11
+ let close;
12
+ if (!client) {
13
+ const url = resolveStoreUrl("ioredis", options.url, ["REDIS_URL"], ["redis:", "rediss:"]);
14
+ const { Redis } = await importDriver("ioredis", "ioredis");
15
+ const owned = new Redis(url.href, { lazyConnect: true });
16
+ // Without a listener, a connection error would be reported as an unhandled error event.
17
+ owned.on("error", () => { });
18
+ await owned.connect();
19
+ client = owned;
20
+ label = describeStoreUrl(url);
21
+ close = async () => {
22
+ await owned.quit();
23
+ };
24
+ }
25
+ const connected = client;
26
+ return createKeyValueCatalogStore({
27
+ label,
28
+ keyPrefix: options.keyPrefix ?? DEFAULT_KEY_PREFIX,
29
+ get: (key) => connected.get(key),
30
+ compareAndSet: (key, values) => connected.eval(REDIS_COMPARE_AND_SET_SCRIPT, 1, key, ...values),
31
+ close,
32
+ });
33
+ }
@@ -0,0 +1,42 @@
1
+ import { type CatalogStore } from "./catalog_store.js";
2
+ export interface MongodbCatalogStoreOptions {
3
+ /** `mongodb://` or `mongodb+srv://` URL. Defaults to `KIBANA_STORE_URL`, then `MONGODB_URI`, then `MONGO_URL`. */
4
+ url?: string;
5
+ /** Database name. Defaults to the database of the URL. */
6
+ database?: string;
7
+ /** Collection name. Defaults to `kibana_mcp_catalogs`. */
8
+ collectionName?: string;
9
+ /** A `mongodb` collection to use instead of `url`. The store never closes its client. */
10
+ collection?: MongodbCatalogCollection;
11
+ }
12
+ export interface MongodbCatalogDocument {
13
+ _id: string;
14
+ content: string;
15
+ updatedAt: Date;
16
+ }
17
+ /** The subset of a `mongodb` collection this store relies on. */
18
+ export interface MongodbCatalogCollection {
19
+ findOne(filter: {
20
+ _id: string;
21
+ }): Promise<{
22
+ content?: unknown;
23
+ } | null>;
24
+ insertOne(document: MongodbCatalogDocument): Promise<unknown>;
25
+ updateOne(filter: {
26
+ _id: string;
27
+ content: string;
28
+ }, update: {
29
+ $set: {
30
+ content: string;
31
+ updatedAt: Date;
32
+ };
33
+ }): Promise<{
34
+ matchedCount: number;
35
+ }>;
36
+ }
37
+ /**
38
+ * Catalog store backed by MongoDB through the `mongodb` package: one document per profile.
39
+ *
40
+ * Usable directly as `store` in the configuration file; its options come from `storeOptions`.
41
+ */
42
+ export default function mongodbCatalogStore(options?: MongodbCatalogStoreOptions): Promise<CatalogStore>;
@@ -0,0 +1,57 @@
1
+ import { CatalogConflictError } from "./catalog_store.js";
2
+ import { DEFAULT_TABLE_NAME, importDriver, resolveStoreConnectionString } from "./shared.js";
3
+ const DUPLICATE_KEY_ERROR_CODE = 11000;
4
+ /**
5
+ * Catalog store backed by MongoDB through the `mongodb` package: one document per profile.
6
+ *
7
+ * Usable directly as `store` in the configuration file; its options come from `storeOptions`.
8
+ */
9
+ export default async function mongodbCatalogStore(options = {}) {
10
+ const collectionName = options.collectionName ?? DEFAULT_TABLE_NAME;
11
+ let collection = options.collection;
12
+ let label = "mongodb:";
13
+ let close;
14
+ if (!collection) {
15
+ const url = resolveStoreConnectionString("MongoDB", options.url, ["MONGODB_URI", "MONGO_URL"], ["mongodb:", "mongodb+srv:"]);
16
+ const { MongoClient } = await importDriver("MongoDB", "mongodb");
17
+ // MongoDB's driver accepts seed lists and SRV URIs that WHATWG URL cannot parse.
18
+ const client = new MongoClient(url);
19
+ await client.connect();
20
+ collection = client.db(options.database).collection(collectionName);
21
+ label = url
22
+ .split(/[?#]/, 1)[0]
23
+ .replace(/(\/\/)[^/]*@/, "$1")
24
+ .replace(/\/$/, "");
25
+ close = () => client.close();
26
+ }
27
+ const catalogs = collection;
28
+ const describe = (profileKey) => `${label}/${collectionName}/${profileKey}`;
29
+ return {
30
+ describe,
31
+ async read(profileKey) {
32
+ const document = await catalogs.findOne({ _id: profileKey });
33
+ return typeof document?.content === "string" ? document.content : undefined;
34
+ },
35
+ async replace(profileKey, expected, next, signal) {
36
+ signal?.throwIfAborted();
37
+ const updatedAt = new Date();
38
+ if (expected === undefined) {
39
+ try {
40
+ await catalogs.insertOne({ _id: profileKey, content: next, updatedAt });
41
+ }
42
+ catch (error) {
43
+ if (error?.code === DUPLICATE_KEY_ERROR_CODE) {
44
+ throw new CatalogConflictError(describe(profileKey));
45
+ }
46
+ throw error;
47
+ }
48
+ return;
49
+ }
50
+ const { matchedCount } = await catalogs.updateOne({ _id: profileKey, content: expected }, { $set: { content: next, updatedAt } });
51
+ if (matchedCount !== 1) {
52
+ throw new CatalogConflictError(describe(profileKey));
53
+ }
54
+ },
55
+ close,
56
+ };
57
+ }
@@ -0,0 +1,24 @@
1
+ import { type CatalogStore } from "./catalog_store.js";
2
+ export interface PostgresCatalogStoreOptions {
3
+ /** `postgres://` or `postgresql://` URL. Defaults to `KIBANA_STORE_URL`, then `DATABASE_URL`. */
4
+ url?: string;
5
+ /** Catalog table, optionally schema-qualified. Defaults to `kibana_mcp_catalogs`. */
6
+ table?: string;
7
+ /** Set to `false` when the table is created by your own migrations. */
8
+ createTable?: boolean;
9
+ /** A `pg` Pool or Client to use instead of `url`. The store never closes it. */
10
+ client?: PostgresCatalogClient;
11
+ }
12
+ /** The subset of a `pg` Pool or Client this store relies on. */
13
+ export interface PostgresCatalogClient {
14
+ query(text: string, values?: unknown[]): Promise<{
15
+ rows: Array<Record<string, unknown>>;
16
+ rowCount?: number | null;
17
+ }>;
18
+ }
19
+ /**
20
+ * Catalog store backed by PostgreSQL through the `pg` package: one row per profile.
21
+ *
22
+ * Usable directly as `store` in the configuration file; its options come from `storeOptions`.
23
+ */
24
+ export default function postgresCatalogStore(options?: PostgresCatalogStoreOptions): Promise<CatalogStore>;