@havesomecode/kibana-mcp-server 1.1.2 → 1.2.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 +72 -18
- package/dist/src/bootstrap.js +426 -0
- package/dist/src/cli.js +172 -9
- package/dist/src/client_registration.js +197 -0
- package/dist/src/config.js +43 -4
- package/dist/src/kibana_client.js +280 -92
- package/dist/src/profile_store.js +20 -2
- package/dist/src/schema_catalog.js +20 -6
- package/dist/src/secret_store.js +42 -31
- package/dist/src/server.js +86 -14
- package/dist/src/setup_flow.js +48 -36
- package/dist/src/source_catalog.js +3 -0
- package/dist/src/state_lock.js +370 -0
- package/dist/src/tools/configure_index.js +104 -0
- package/dist/src/tools/describe_fields.js +2 -2
- package/dist/src/tools/discover.js +5 -0
- package/dist/src/tools/filter.js +1 -0
- package/dist/src/tools/query.js +5 -2
- package/docs/project/compatibility-matrix.md +2 -1
- package/docs/project/distribution-strategy.md +17 -4
- package/package.json +2 -1
- package/plugins/kibana-log-investigation/.codex-plugin/plugin.json +3 -3
- package/skills/kibana-log-investigation/SKILL.md +99 -0
package/dist/src/cli.js
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import { createInterface } from "node:readline/promises";
|
|
2
2
|
import { Writable } from "node:stream";
|
|
3
|
+
import { parseArgs } from "node:util";
|
|
4
|
+
import { runBootstrap } from "./bootstrap.js";
|
|
5
|
+
import { PROFILE_NAME_ENV } from "./config.js";
|
|
3
6
|
import { startMcpServer } from "./mcp_runtime.js";
|
|
4
7
|
import { runSetupFlow } from "./setup_flow.js";
|
|
5
8
|
function renderHelp() {
|
|
@@ -10,9 +13,35 @@ function renderHelp() {
|
|
|
10
13
|
" kibana-mcp-server [command]",
|
|
11
14
|
"",
|
|
12
15
|
"Commands:",
|
|
13
|
-
" setup
|
|
14
|
-
"
|
|
15
|
-
"
|
|
16
|
+
" setup Run guided machine setup",
|
|
17
|
+
" bootstrap Verify, save, and register a profile without prompts",
|
|
18
|
+
" serve Start the stdio MCP server",
|
|
19
|
+
" help Show this help output",
|
|
20
|
+
].join("\n");
|
|
21
|
+
}
|
|
22
|
+
function renderBootstrapHelp() {
|
|
23
|
+
return [
|
|
24
|
+
"Usage:",
|
|
25
|
+
" kibana-mcp-server bootstrap [options]",
|
|
26
|
+
" kibana-mcp-server setup [options] # compatible non-interactive alias",
|
|
27
|
+
"",
|
|
28
|
+
"Bootstrap verifies and saves only the Kibana connection; it does not inspect or configure indexes.",
|
|
29
|
+
"After installation, ask the agent to configure a specific index or index pattern.",
|
|
30
|
+
"",
|
|
31
|
+
"Connection options use KIBANA_BASE_URL, KIBANA_USERNAME, and KIBANA_PASSWORD as fallbacks.",
|
|
32
|
+
"Passwords are accepted only through --password-stdin, --password-env <NAME>, or KIBANA_PASSWORD.",
|
|
33
|
+
"",
|
|
34
|
+
"Options:",
|
|
35
|
+
" --profile <name> Saved profile name (default: KIBANA_PROFILE or default)",
|
|
36
|
+
" --url <url> Kibana base URL",
|
|
37
|
+
" --username <name> Kibana basic-auth username",
|
|
38
|
+
" --password-stdin Read the password from stdin",
|
|
39
|
+
" --password-env <name> Read the password from a named environment variable",
|
|
40
|
+
" --client <codex|none> Register Codex or skip client registration (default: codex)",
|
|
41
|
+
" --package <specifier> Exact package specifier used by the Codex registration",
|
|
42
|
+
" --mcp-name <name> Base name for the transport-hashed Codex registration",
|
|
43
|
+
" --replace Explicitly replace an existing catalog with an empty managed catalog",
|
|
44
|
+
" --no-default Do not make this profile the default",
|
|
16
45
|
].join("\n");
|
|
17
46
|
}
|
|
18
47
|
export async function runCli(argv = process.argv.slice(2), io = {}, dependencies = {}) {
|
|
@@ -20,13 +49,35 @@ export async function runCli(argv = process.argv.slice(2), io = {}, dependencies
|
|
|
20
49
|
const stderr = io.stderr ?? ((text) => process.stderr.write(`${text}\n`));
|
|
21
50
|
const stdin = io.stdin ?? process.stdin;
|
|
22
51
|
const stdoutStream = io.stdoutStream ?? process.stdout;
|
|
52
|
+
const env = io.env ?? process.env;
|
|
23
53
|
const [command, ...rest] = argv;
|
|
24
54
|
switch (command) {
|
|
25
|
-
case undefined:
|
|
55
|
+
case undefined: {
|
|
56
|
+
try {
|
|
57
|
+
await (dependencies.startMcpServerFn ?? startMcpServer)(env);
|
|
58
|
+
return 0;
|
|
59
|
+
}
|
|
60
|
+
catch (error) {
|
|
61
|
+
stderr(error instanceof Error ? error.message : String(error));
|
|
62
|
+
return 1;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
26
65
|
case "setup": {
|
|
27
66
|
if (rest.length > 0) {
|
|
28
|
-
|
|
29
|
-
|
|
67
|
+
if (rest.includes("--help") || rest.includes("-h")) {
|
|
68
|
+
stdout(renderBootstrapHelp());
|
|
69
|
+
return 0;
|
|
70
|
+
}
|
|
71
|
+
try {
|
|
72
|
+
const options = await parseBootstrapOptions(rest, stdin, env);
|
|
73
|
+
const result = await (dependencies.runBootstrapFn ?? runBootstrap)(options);
|
|
74
|
+
stdout(renderBootstrapResult(result));
|
|
75
|
+
return 0;
|
|
76
|
+
}
|
|
77
|
+
catch (error) {
|
|
78
|
+
stderr(error instanceof Error ? error.message : String(error));
|
|
79
|
+
return 1;
|
|
80
|
+
}
|
|
30
81
|
}
|
|
31
82
|
const promptIo = await createPromptIo(stdout, {
|
|
32
83
|
stdin,
|
|
@@ -48,12 +99,39 @@ export async function runCli(argv = process.argv.slice(2), io = {}, dependencies
|
|
|
48
99
|
stdout(renderHelp());
|
|
49
100
|
return 0;
|
|
50
101
|
}
|
|
102
|
+
case "bootstrap": {
|
|
103
|
+
if (rest.includes("--help") || rest.includes("-h")) {
|
|
104
|
+
stdout(renderBootstrapHelp());
|
|
105
|
+
return 0;
|
|
106
|
+
}
|
|
107
|
+
try {
|
|
108
|
+
const options = await parseBootstrapOptions(rest, stdin, env);
|
|
109
|
+
const result = await (dependencies.runBootstrapFn ?? runBootstrap)(options);
|
|
110
|
+
stdout(renderBootstrapResult(result));
|
|
111
|
+
return 0;
|
|
112
|
+
}
|
|
113
|
+
catch (error) {
|
|
114
|
+
stderr(error instanceof Error ? error.message : String(error));
|
|
115
|
+
return 1;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
51
118
|
case "serve": {
|
|
52
|
-
|
|
53
|
-
|
|
119
|
+
try {
|
|
120
|
+
const { values } = parseArgs({
|
|
121
|
+
args: rest,
|
|
122
|
+
allowPositionals: false,
|
|
123
|
+
strict: true,
|
|
124
|
+
options: {
|
|
125
|
+
profile: { type: "string" },
|
|
126
|
+
},
|
|
127
|
+
});
|
|
128
|
+
const serveEnv = values.profile ? { ...env, [PROFILE_NAME_ENV]: values.profile } : env;
|
|
129
|
+
await (dependencies.startMcpServerFn ?? startMcpServer)(serveEnv);
|
|
130
|
+
}
|
|
131
|
+
catch (error) {
|
|
132
|
+
stderr(error instanceof Error ? error.message : String(error));
|
|
54
133
|
return 1;
|
|
55
134
|
}
|
|
56
|
-
await (dependencies.startMcpServerFn ?? startMcpServer)();
|
|
57
135
|
return 0;
|
|
58
136
|
}
|
|
59
137
|
default: {
|
|
@@ -63,6 +141,91 @@ export async function runCli(argv = process.argv.slice(2), io = {}, dependencies
|
|
|
63
141
|
}
|
|
64
142
|
}
|
|
65
143
|
}
|
|
144
|
+
async function parseBootstrapOptions(args, stdin, env) {
|
|
145
|
+
const { values } = parseArgs({
|
|
146
|
+
args,
|
|
147
|
+
allowPositionals: false,
|
|
148
|
+
strict: true,
|
|
149
|
+
options: {
|
|
150
|
+
profile: { type: "string" },
|
|
151
|
+
url: { type: "string" },
|
|
152
|
+
username: { type: "string" },
|
|
153
|
+
"password-stdin": { type: "boolean" },
|
|
154
|
+
"password-env": { type: "string" },
|
|
155
|
+
timeout: { type: "string" },
|
|
156
|
+
client: { type: "string" },
|
|
157
|
+
package: { type: "string" },
|
|
158
|
+
"mcp-name": { type: "string" },
|
|
159
|
+
replace: { type: "boolean" },
|
|
160
|
+
"no-default": { type: "boolean" },
|
|
161
|
+
},
|
|
162
|
+
});
|
|
163
|
+
const baseUrl = values.url?.trim() || env.KIBANA_BASE_URL?.trim();
|
|
164
|
+
if (!baseUrl) {
|
|
165
|
+
throw new Error("Kibana base URL is required via --url or KIBANA_BASE_URL.");
|
|
166
|
+
}
|
|
167
|
+
const username = values.username?.trim() || env.KIBANA_USERNAME?.trim();
|
|
168
|
+
if (!username) {
|
|
169
|
+
throw new Error("Kibana username is required via --username or KIBANA_USERNAME.");
|
|
170
|
+
}
|
|
171
|
+
if (values["password-stdin"] && values["password-env"]) {
|
|
172
|
+
throw new Error("Use only one of --password-stdin or --password-env.");
|
|
173
|
+
}
|
|
174
|
+
let password;
|
|
175
|
+
if (values["password-stdin"]) {
|
|
176
|
+
password = (await readAllInput(stdin)).replace(/\r?\n$/, "");
|
|
177
|
+
}
|
|
178
|
+
else if (values["password-env"]) {
|
|
179
|
+
password = env[values["password-env"]];
|
|
180
|
+
if (password === undefined) {
|
|
181
|
+
throw new Error(`Password environment variable '${values["password-env"]}' is not set.`);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
else {
|
|
185
|
+
password = env.KIBANA_PASSWORD;
|
|
186
|
+
}
|
|
187
|
+
if (!password) {
|
|
188
|
+
throw new Error("Kibana password is required via --password-stdin, --password-env, or KIBANA_PASSWORD.");
|
|
189
|
+
}
|
|
190
|
+
const client = values.client ?? "codex";
|
|
191
|
+
if (client !== "codex" && client !== "none") {
|
|
192
|
+
throw new Error("--client must be either 'codex' or 'none'.");
|
|
193
|
+
}
|
|
194
|
+
const timeoutMs = values.timeout === undefined ? undefined : Number(values.timeout);
|
|
195
|
+
if (timeoutMs !== undefined && !Number.isFinite(timeoutMs)) {
|
|
196
|
+
throw new Error("--timeout must be a number of milliseconds.");
|
|
197
|
+
}
|
|
198
|
+
return {
|
|
199
|
+
profileName: values.profile?.trim() || env[PROFILE_NAME_ENV]?.trim() || "default",
|
|
200
|
+
baseUrl,
|
|
201
|
+
username,
|
|
202
|
+
password,
|
|
203
|
+
client,
|
|
204
|
+
packageSpecifier: values.package,
|
|
205
|
+
mcpName: values["mcp-name"],
|
|
206
|
+
makeDefault: !values["no-default"],
|
|
207
|
+
replaceExisting: values.replace ?? false,
|
|
208
|
+
timeoutMs,
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
function renderBootstrapResult(result) {
|
|
212
|
+
return [
|
|
213
|
+
`Bootstrap verified for profile '${result.profileName}'.`,
|
|
214
|
+
result.sourceCount === 0
|
|
215
|
+
? "Source catalog is empty. No Kibana indexes were inspected or configured. Ask the user which index or index pattern to configure before querying logs."
|
|
216
|
+
: `Preserved ${result.sourceCount} explicitly configured source${result.sourceCount === 1 ? "" : "s"}; bootstrap did not inspect them.`,
|
|
217
|
+
result.registered
|
|
218
|
+
? `Codex MCP registration is installed and verified for profile '${result.profileName}'.`
|
|
219
|
+
: "Client registration was skipped by request.",
|
|
220
|
+
].join("\n");
|
|
221
|
+
}
|
|
222
|
+
async function readAllInput(stdin) {
|
|
223
|
+
let rawInput = "";
|
|
224
|
+
for await (const chunk of stdin) {
|
|
225
|
+
rawInput += typeof chunk === "string" ? chunk : chunk.toString("utf8");
|
|
226
|
+
}
|
|
227
|
+
return rawInput;
|
|
228
|
+
}
|
|
66
229
|
export async function createPromptIo(stdout, options = {}) {
|
|
67
230
|
const stdin = options.stdin ?? process.stdin;
|
|
68
231
|
const stdoutStream = options.stdoutStream ?? process.stdout;
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
3
|
+
export function deriveCodexRegistrationName(options) {
|
|
4
|
+
const transport = [
|
|
5
|
+
"npx",
|
|
6
|
+
"-y",
|
|
7
|
+
options.packageSpecifier,
|
|
8
|
+
"serve",
|
|
9
|
+
"--profile",
|
|
10
|
+
options.profileName,
|
|
11
|
+
];
|
|
12
|
+
const suffix = createHash("sha256").update(JSON.stringify(transport)).digest("hex").slice(0, 12);
|
|
13
|
+
return `${options.mcpName}-${suffix}`;
|
|
14
|
+
}
|
|
15
|
+
export class CodexClientRegistrar {
|
|
16
|
+
options;
|
|
17
|
+
runner;
|
|
18
|
+
preflightResult;
|
|
19
|
+
rollbackEligible = false;
|
|
20
|
+
constructor(options, runner = runCommand) {
|
|
21
|
+
this.options = options;
|
|
22
|
+
this.runner = runner;
|
|
23
|
+
}
|
|
24
|
+
get registrationName() {
|
|
25
|
+
return deriveCodexRegistrationName(this.options);
|
|
26
|
+
}
|
|
27
|
+
async preflight() {
|
|
28
|
+
const existing = await this.readRegistration();
|
|
29
|
+
if (!existing) {
|
|
30
|
+
this.preflightResult = { alreadyRegistered: false };
|
|
31
|
+
return this.preflightResult;
|
|
32
|
+
}
|
|
33
|
+
if (!this.matchesExpectedTransport(existing)) {
|
|
34
|
+
throw new Error(`Refusing to overwrite existing Codex MCP registration '${this.registrationName}' because its transport differs. Remove or rename it explicitly before retrying.`);
|
|
35
|
+
}
|
|
36
|
+
this.preflightResult = { alreadyRegistered: true };
|
|
37
|
+
return this.preflightResult;
|
|
38
|
+
}
|
|
39
|
+
async register() {
|
|
40
|
+
const preflight = this.preflightResult ?? (await this.preflight());
|
|
41
|
+
if (preflight.alreadyRegistered) {
|
|
42
|
+
return { added: false };
|
|
43
|
+
}
|
|
44
|
+
let addResult;
|
|
45
|
+
try {
|
|
46
|
+
addResult = await this.runner("codex", [
|
|
47
|
+
"mcp",
|
|
48
|
+
"add",
|
|
49
|
+
this.registrationName,
|
|
50
|
+
"--",
|
|
51
|
+
...this.expectedCommand(),
|
|
52
|
+
]);
|
|
53
|
+
}
|
|
54
|
+
catch (error) {
|
|
55
|
+
return await this.reconcileAmbiguousAdd(error);
|
|
56
|
+
}
|
|
57
|
+
if (addResult.exitCode !== 0) {
|
|
58
|
+
return await this.reconcileAmbiguousAdd(new Error(`Codex MCP registration failed: ${addResult.stderr.trim() || addResult.stdout.trim() || `exit code ${addResult.exitCode}`}`));
|
|
59
|
+
}
|
|
60
|
+
this.rollbackEligible = true;
|
|
61
|
+
const registered = await this.readRegistration();
|
|
62
|
+
if (!registered || !this.matchesExpectedTransport(registered)) {
|
|
63
|
+
// A different registration could have won a race. Never remove a transport
|
|
64
|
+
// whose exact ownership cannot be proven.
|
|
65
|
+
this.rollbackEligible = Boolean(registered && this.matchesExpectedTransport(registered));
|
|
66
|
+
throw new Error(`Codex MCP registration readback did not match the requested transport for '${this.registrationName}'. The mismatched registration was left untouched.`);
|
|
67
|
+
}
|
|
68
|
+
return { added: true };
|
|
69
|
+
}
|
|
70
|
+
async reconcileAmbiguousAdd(originalError) {
|
|
71
|
+
const registered = await this.readRegistration();
|
|
72
|
+
if (registered && this.matchesExpectedTransport(registered)) {
|
|
73
|
+
// The requested end-state exists, but another process may own it. Treat the
|
|
74
|
+
// operation as converged and never claim rollback ownership.
|
|
75
|
+
this.rollbackEligible = false;
|
|
76
|
+
return { added: true };
|
|
77
|
+
}
|
|
78
|
+
this.rollbackEligible = false;
|
|
79
|
+
if (registered) {
|
|
80
|
+
throw new Error(`Codex MCP registration '${this.registrationName}' changed concurrently and does not match the requested transport.`, { cause: originalError });
|
|
81
|
+
}
|
|
82
|
+
throw originalError;
|
|
83
|
+
}
|
|
84
|
+
async rollback() {
|
|
85
|
+
if (!this.rollbackEligible) {
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
const current = await this.readRegistration();
|
|
89
|
+
if (!current) {
|
|
90
|
+
this.rollbackEligible = false;
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
if (!this.matchesExpectedTransport(current)) {
|
|
94
|
+
this.rollbackEligible = false;
|
|
95
|
+
throw new Error(`Refusing to roll back Codex MCP registration '${this.registrationName}' because its transport no longer matches this bootstrap.`);
|
|
96
|
+
}
|
|
97
|
+
const removeResult = await this.runner("codex", ["mcp", "remove", this.registrationName]);
|
|
98
|
+
if (removeResult.exitCode !== 0) {
|
|
99
|
+
throw new Error(`Could not roll back Codex MCP registration '${this.registrationName}': ${removeResult.stderr.trim() || removeResult.stdout.trim() || `exit code ${removeResult.exitCode}`}`);
|
|
100
|
+
}
|
|
101
|
+
const remaining = await this.readRegistration();
|
|
102
|
+
if (!remaining || !this.matchesExpectedTransport(remaining)) {
|
|
103
|
+
this.rollbackEligible = false;
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
throw new Error(`Could not confirm removal of Codex MCP registration '${this.registrationName}'.`);
|
|
107
|
+
}
|
|
108
|
+
expectedCommand() {
|
|
109
|
+
return [
|
|
110
|
+
"npx",
|
|
111
|
+
"-y",
|
|
112
|
+
this.options.packageSpecifier,
|
|
113
|
+
"serve",
|
|
114
|
+
"--profile",
|
|
115
|
+
this.options.profileName,
|
|
116
|
+
];
|
|
117
|
+
}
|
|
118
|
+
matchesExpectedTransport(config) {
|
|
119
|
+
const [command, ...args] = this.expectedCommand();
|
|
120
|
+
return (config.enabled === true &&
|
|
121
|
+
config.transport?.type === "stdio" &&
|
|
122
|
+
config.transport.command === command &&
|
|
123
|
+
Array.isArray(config.transport.args) &&
|
|
124
|
+
config.transport.args.every((value) => typeof value === "string") &&
|
|
125
|
+
JSON.stringify(config.transport.args) === JSON.stringify(args) &&
|
|
126
|
+
isEmptyValue(config.transport.cwd) &&
|
|
127
|
+
isEmptyEnvironment(config.transport.env) &&
|
|
128
|
+
isEmptyStringArray(config.transport.env_vars) &&
|
|
129
|
+
hasOnlyKeys(config.transport, ["type", "command", "args", "cwd", "env", "env_vars"]));
|
|
130
|
+
}
|
|
131
|
+
async readRegistration() {
|
|
132
|
+
const result = await this.runner("codex", ["mcp", "get", this.registrationName, "--json"]);
|
|
133
|
+
if (result.exitCode !== 0) {
|
|
134
|
+
const message = `${result.stderr}\n${result.stdout}`.toLowerCase();
|
|
135
|
+
if (message.includes("not found") || message.includes("no mcp server")) {
|
|
136
|
+
return null;
|
|
137
|
+
}
|
|
138
|
+
throw new Error(`Could not inspect Codex MCP registration '${this.registrationName}': ${result.stderr.trim() || result.stdout.trim() || `exit code ${result.exitCode}`}`);
|
|
139
|
+
}
|
|
140
|
+
try {
|
|
141
|
+
return JSON.parse(result.stdout);
|
|
142
|
+
}
|
|
143
|
+
catch {
|
|
144
|
+
throw new Error(`Codex MCP registration '${this.registrationName}' returned invalid JSON during readback.`);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
function isEmptyValue(value) {
|
|
149
|
+
return value === undefined || value === null;
|
|
150
|
+
}
|
|
151
|
+
function isEmptyEnvironment(value) {
|
|
152
|
+
return (isEmptyValue(value) ||
|
|
153
|
+
(typeof value === "object" && value !== null && Object.keys(value).length === 0));
|
|
154
|
+
}
|
|
155
|
+
function isEmptyStringArray(value) {
|
|
156
|
+
return isEmptyValue(value) || (Array.isArray(value) && value.length === 0);
|
|
157
|
+
}
|
|
158
|
+
function hasOnlyKeys(value, allowedKeys) {
|
|
159
|
+
const allowed = new Set(allowedKeys);
|
|
160
|
+
return Object.keys(value).every((key) => allowed.has(key));
|
|
161
|
+
}
|
|
162
|
+
async function runCommand(command, args) {
|
|
163
|
+
return await new Promise((resolve, reject) => {
|
|
164
|
+
const child = spawn(command, args, { stdio: "pipe" });
|
|
165
|
+
let stdout = "";
|
|
166
|
+
let stderr = "";
|
|
167
|
+
let settled = false;
|
|
168
|
+
const finish = (callback) => {
|
|
169
|
+
if (settled)
|
|
170
|
+
return;
|
|
171
|
+
settled = true;
|
|
172
|
+
clearTimeout(timeout);
|
|
173
|
+
callback();
|
|
174
|
+
};
|
|
175
|
+
const timeout = setTimeout(() => {
|
|
176
|
+
child.kill("SIGTERM");
|
|
177
|
+
finish(() => reject(new Error(`Command '${command}' timed out after 30000ms.`)));
|
|
178
|
+
}, 30000);
|
|
179
|
+
child.stdout.setEncoding("utf8");
|
|
180
|
+
child.stderr.setEncoding("utf8");
|
|
181
|
+
child.stdout.on("data", (chunk) => {
|
|
182
|
+
stdout += chunk;
|
|
183
|
+
});
|
|
184
|
+
child.stderr.on("data", (chunk) => {
|
|
185
|
+
stderr += chunk;
|
|
186
|
+
});
|
|
187
|
+
child.on("error", (error) => {
|
|
188
|
+
finish(() => reject(new Error(error && typeof error === "object" && "code" in error && error.code === "ENOENT"
|
|
189
|
+
? `Required command '${command}' is not installed or not available on PATH.`
|
|
190
|
+
: `Could not run '${command}': ${error.message}`)));
|
|
191
|
+
});
|
|
192
|
+
child.on("close", (exitCode) => {
|
|
193
|
+
finish(() => resolve({ stdout, stderr, exitCode: exitCode ?? 1 }));
|
|
194
|
+
});
|
|
195
|
+
child.stdin.end();
|
|
196
|
+
});
|
|
197
|
+
}
|
package/dist/src/config.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
+
import { mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
|
|
2
3
|
import { dirname } from "node:path";
|
|
3
4
|
import { z } from "zod";
|
|
4
5
|
import { ProfileStore } from "./profile_store.js";
|
|
@@ -71,8 +72,36 @@ export const sourceDefinitionSchema = z.object({
|
|
|
71
72
|
defaultTextFields: z.array(z.string().min(1)).default([]),
|
|
72
73
|
evidenceFields: z.array(z.string().min(1)).default([]),
|
|
73
74
|
});
|
|
74
|
-
export const sourceCatalogSchema = z
|
|
75
|
-
|
|
75
|
+
export const sourceCatalogSchema = z
|
|
76
|
+
.object({
|
|
77
|
+
generatedBy: z
|
|
78
|
+
.object({
|
|
79
|
+
tool: z.literal("@havesomecode/kibana-mcp-server"),
|
|
80
|
+
formatVersion: z.literal(1),
|
|
81
|
+
sourceHash: z.string().regex(/^[a-f0-9]{64}$/),
|
|
82
|
+
})
|
|
83
|
+
.optional(),
|
|
84
|
+
sources: z.array(sourceDefinitionSchema),
|
|
85
|
+
})
|
|
86
|
+
.superRefine((catalog, context) => {
|
|
87
|
+
if (catalog.sources.length !== 0)
|
|
88
|
+
return;
|
|
89
|
+
if (!catalog.generatedBy) {
|
|
90
|
+
context.addIssue({
|
|
91
|
+
code: "custom",
|
|
92
|
+
path: ["sources"],
|
|
93
|
+
message: "Empty source catalogs must be managed by @havesomecode/kibana-mcp-server.",
|
|
94
|
+
});
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
const expectedHash = createHash("sha256").update(JSON.stringify(catalog.sources)).digest("hex");
|
|
98
|
+
if (catalog.generatedBy.sourceHash !== expectedHash) {
|
|
99
|
+
context.addIssue({
|
|
100
|
+
code: "custom",
|
|
101
|
+
path: ["generatedBy", "sourceHash"],
|
|
102
|
+
message: "Empty source catalogs require valid managed provenance.",
|
|
103
|
+
});
|
|
104
|
+
}
|
|
76
105
|
});
|
|
77
106
|
function buildKibanaConfig(env) {
|
|
78
107
|
return {
|
|
@@ -157,7 +186,17 @@ export async function loadConfigFromEnvironment(envInput = process.env, dependen
|
|
|
157
186
|
export async function persistSourceCatalog(sources, options = {}) {
|
|
158
187
|
const sourceCatalogPath = options.sourceCatalogPath ?? resolveSourceCatalogPath(options.envInput);
|
|
159
188
|
await mkdir(dirname(sourceCatalogPath), { recursive: true });
|
|
160
|
-
|
|
189
|
+
const temporaryPath = `${sourceCatalogPath}.${process.pid}.${randomUUID()}.tmp`;
|
|
190
|
+
try {
|
|
191
|
+
await writeFile(temporaryPath, `${JSON.stringify(options.catalog ?? { sources }, null, 2)}\n`, {
|
|
192
|
+
encoding: "utf8",
|
|
193
|
+
mode: 0o600,
|
|
194
|
+
});
|
|
195
|
+
await rename(temporaryPath, sourceCatalogPath);
|
|
196
|
+
}
|
|
197
|
+
finally {
|
|
198
|
+
await rm(temporaryPath, { force: true }).catch(() => { });
|
|
199
|
+
}
|
|
161
200
|
return sourceCatalogPath;
|
|
162
201
|
}
|
|
163
202
|
function hasBootstrapConfig(envInput) {
|