@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
@@ -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>;
@@ -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
+ }