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