feat: expand troubleshooting tools — server_stats, resource_stats, updates, build logs, 8 new execute ops

- New komodo_server_stats: system_information, system_stats, historical_server_stats, server_state, periphery_information, containers_summary, version, core_info
- New komodo_resource_stats: deployment_stats, deployment_container, build_monthly_stats, action_state (9 resource types), concurrency_limit_usage, resource_matching_container
- New komodo_updates: get (full execution logs) and list (recent operations history)
- Expanded komodo_logs: add build resource type (built_contents, remote_contents, remote_error, hashes)
- Expanded komodo_list_detail: terminals, alerts, containers_summary
- Expanded komodo_execute: refresh_server, refresh_server_containers, refresh_stack_cache, refresh_stack_content, remove_orphan_containers, check_stack_for_update, check_deployment_for_update, get_mcp_token
This commit is contained in:
2026-09-08 20:28:53 +00:00
parent 9df8ab5af8
commit fe00ed430b
8 changed files with 397 additions and 4 deletions
+21
View File
@@ -16,6 +16,9 @@ import { searchLogsInputSchema, handleSearchLogs } from "./tools/search-logs.js"
import { listDetailInputSchema, handleListDetail } from "./tools/list-detail.js"; import { listDetailInputSchema, handleListDetail } from "./tools/list-detail.js";
import { copyInputSchema, handleCopy } from "./tools/copy.js"; import { copyInputSchema, handleCopy } from "./tools/copy.js";
import { renameInputSchema, handleRename } from "./tools/rename.js"; import { renameInputSchema, handleRename } from "./tools/rename.js";
import { serverStatsInputSchema, handleServerStats } from "./tools/server-stats.js";
import { resourceStatsInputSchema, handleResourceStats } from "./tools/resource-stats.js";
import { updatesInputSchema, handleUpdates } from "./tools/updates.js";
const PORT = parseInt(process.env.PORT || "9800", 10); const PORT = parseInt(process.env.PORT || "9800", 10);
@@ -120,6 +123,24 @@ function createServerInstance(client: KomodoClient): McpServer {
inputSchema: renameInputSchema, inputSchema: renameInputSchema,
}, async (args) => handleRename(args, client)); }, async (args) => handleRename(args, client));
server.registerTool("komodo_server_stats", {
description:
"Get server diagnostics. stat_type: system_information (OS/CPU/hardware), system_stats (live CPU/mem/disk), historical_server_stats (timeseries), server_state (connectivity), periphery_information (agent info), containers_summary (aggregate counts), version (Core API version), core_info (Core configuration).",
inputSchema: serverStatsInputSchema,
}, async (args) => handleServerStats(args, client));
server.registerTool("komodo_resource_stats", {
description:
"Get resource diagnostics. stat_type: deployment_stats (CPU/mem/net usage), deployment_container (container details), build_monthly_stats (build frequency), action_state (is resource currently running — requires resource_type param), concurrency_limit_usage (active limits), resource_matching_container (reverse-lookup container → resource).",
inputSchema: resourceStatsInputSchema,
}, async (args) => handleResourceStats(args, client));
server.registerTool("komodo_updates", {
description:
"Get execution history and logs. mode: get (retrieve specific update by ID with full structured logs including stage/command/stdout/stderr), list (list recent operations with status/success/target). Use list to find recent executions, then get with the update ID to see full logs.",
inputSchema: updatesInputSchema,
}, async (args) => handleUpdates(args, client));
return server; return server;
} }
+9
View File
@@ -78,6 +78,15 @@ const OPERATION_PARAM_KEY: Record<string, string> = {
global_auto_update: "_none", global_auto_update: "_none",
send_alert: "alert", send_alert: "alert",
test_alerter: "alerter", test_alerter: "alerter",
// Troubleshooting / refresh
refresh_server: "server",
refresh_server_containers: "server",
refresh_stack_cache: "stack",
refresh_stack_content: "stack",
remove_orphan_containers: "stack",
check_stack_for_update: "stack",
check_deployment_for_update: "deployment",
get_mcp_token: "_none",
}; };
// Operations that need {server: <value>} alongside the entity param // Operations that need {server: <value>} alongside the entity param
+2
View File
@@ -20,6 +20,8 @@ const listDetailType = z.enum([
"common_stack_build_extra_args", "common_stack_extra_args", "common_stack_build_extra_args", "common_stack_extra_args",
// Other // Other
"image_history", "all_stack_services", "image_history", "all_stack_services",
// Troubleshooting
"terminals", "alerts", "containers_summary",
]); ]);
export const listDetailInputSchema = { export const listDetailInputSchema = {
+20 -1
View File
@@ -1,7 +1,7 @@
import { z } from "zod"; import { z } from "zod";
import { KomodoClient } from "../komodo-client.js"; import { KomodoClient } from "../komodo-client.js";
const LogResourceType = z.enum(["deployment", "stack", "container", "swarm_service"]); const LogResourceType = z.enum(["deployment", "stack", "container", "swarm_service", "build"]);
export const logsInputSchema = { export const logsInputSchema = {
resource_type: LogResourceType.describe( resource_type: LogResourceType.describe(
@@ -61,6 +61,25 @@ export async function handleLogs(
result = await client.rpc("read", "GetSwarmServiceLog", params); result = await client.rpc("read", "GetSwarmServiceLog", params);
break; break;
} }
case "build": {
const build = (await client.rpc("read", "GetBuild", { id })) as Record<string, unknown>;
const info = build?.info as Record<string, unknown> | undefined;
const config = build?.config as Record<string, unknown> | undefined;
const lines: string[] = [];
lines.push(`Build: ${build?.name ?? id}`);
if (config?.repo) lines.push(`Repo: ${config.repo}`);
if (config?.branch) lines.push(`Branch: ${config.branch}`);
if (info?.last_built_at) lines.push(`Last built: ${new Date(info.last_built_at as number).toISOString()}`);
if (info?.built_hash) lines.push(`Built hash: ${info.built_hash}`);
if (info?.latest_hash) lines.push(`Latest hash: ${info.latest_hash}`);
if (info?.built_message) lines.push(`Built message: ${info.built_message}`);
if (info?.latest_message) lines.push(`Latest message: ${info.latest_message}`);
if (info?.remote_error) lines.push(`Remote error: ${info.remote_error}`);
if (info?.built_contents) lines.push(`--- Built Dockerfile ---\n${info.built_contents}`);
if (info?.remote_contents) lines.push(`--- Remote Dockerfile ---\n${info.remote_contents}`);
result = lines.join("\n");
break;
}
} }
return { return {
+99
View File
@@ -0,0 +1,99 @@
import { z } from "zod";
import { KomodoClient } from "../komodo-client.js";
import { RESOURCE_STATS_REQUEST_MAP, ACTION_STATE_REQUEST_MAP } from "../types.js";
const ResourceStatType = z.enum([
"deployment_stats",
"deployment_container",
"build_monthly_stats",
"action_state",
"concurrency_limit_usage",
"resource_matching_container",
]);
const ActionStateResourceType = z.enum([
"action", "build", "deployment", "procedure",
"repo", "server", "stack", "swarm", "sync_resource",
]);
export const resourceStatsInputSchema = {
stat_type: ResourceStatType.describe(
"Resource stat to retrieve: deployment_stats (CPU/mem/net usage), deployment_container (container details), build_monthly_stats (build frequency), action_state (is resource currently running), concurrency_limit_usage (active limits), resource_matching_container (reverse-lookup container → resource)."
),
id: z.string().optional().describe("Resource ID or name (required for deployment_stats, deployment_container, build_monthly_stats, action_state)"),
resource_type: ActionStateResourceType.optional().describe("Resource type for action_state (e.g. 'stack', 'deployment', 'build'). Required when stat_type is 'action_state'."),
server: z.string().optional().describe("Server name (required for resource_matching_container)"),
container: z.string().optional().describe("Container name (required for resource_matching_container)"),
};
export async function handleResourceStats(
args: {
stat_type: z.infer<typeof ResourceStatType>;
id?: string;
resource_type?: z.infer<typeof ActionStateResourceType>;
server?: string;
container?: string;
},
client: KomodoClient,
): Promise<{ content: { type: "text"; text: string }[] }> {
const { stat_type, id, resource_type, server, container } = args;
// Handle action_state separately — it dispatches to different endpoints per resource type
if (stat_type === "action_state") {
if (!resource_type) throw new Error(`action_state requires a resource_type (e.g. 'stack', 'deployment', 'build')`);
if (!id) throw new Error(`action_state requires an id`);
const requestName = ACTION_STATE_REQUEST_MAP[resource_type];
if (!requestName) {
throw new Error(`No action state endpoint for resource type: ${resource_type}`);
}
// The param key matches the resource type for most, but sync_resource uses "resource_sync"
const paramKey = resource_type === "sync_resource" ? "resource_sync" : resource_type;
const params: Record<string, unknown> = { [paramKey]: id };
const result = await client.rpc("read", requestName, params);
return {
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
};
}
const requestName = RESOURCE_STATS_REQUEST_MAP[stat_type];
if (!requestName) {
throw new Error(`Unknown resource stat type: ${stat_type}`);
}
const params: Record<string, unknown> = {};
switch (stat_type) {
case "deployment_stats":
case "deployment_container":
if (!id) throw new Error(`${stat_type} requires an id (deployment name or ID)`);
params.deployment = id;
break;
case "build_monthly_stats":
if (!id) throw new Error(`${stat_type} requires an id (build name or ID)`);
params.build = id;
break;
case "concurrency_limit_usage":
// No params needed
break;
case "resource_matching_container":
if (!server) throw new Error(`resource_matching_container requires a server`);
if (!container) throw new Error(`resource_matching_container requires a container name`);
params.server = server;
params.container = container;
break;
}
const result = await client.rpc("read", requestName, params);
return {
content: [
{
type: "text",
text: JSON.stringify(result, null, 2),
},
],
};
}
+75
View File
@@ -0,0 +1,75 @@
import { z } from "zod";
import { KomodoClient } from "../komodo-client.js";
import { SERVER_STATS_REQUEST_MAP } from "../types.js";
const ServerStatType = z.enum([
"system_information",
"system_stats",
"historical_server_stats",
"server_state",
"periphery_information",
"containers_summary",
"version",
"core_info",
]);
export const serverStatsInputSchema = {
stat_type: ServerStatType.describe(
"Server stat to retrieve: system_information (OS/CPU/hardware), system_stats (live CPU/mem/disk), historical_server_stats (timeseries), server_state (connectivity), periphery_information (agent info), containers_summary (aggregate counts), version (Core API version), core_info (Core configuration)."
),
server: z.string().optional().describe("Server name or ID (required for most stats except version, core_info, containers_summary)"),
granularity: z.string().optional().describe("Time granularity for historical_server_stats (e.g. '1-hr', '1-day'). Required for historical_server_stats."),
page: z.number().optional().describe("Page for historical_server_stats pagination"),
};
export async function handleServerStats(
args: {
stat_type: z.infer<typeof ServerStatType>;
server?: string;
granularity?: string;
page?: number;
},
client: KomodoClient,
): Promise<{ content: { type: "text"; text: string }[] }> {
const { stat_type, server, granularity, page } = args;
const requestName = SERVER_STATS_REQUEST_MAP[stat_type];
if (!requestName) {
throw new Error(`Unknown server stat type: ${stat_type}`);
}
const params: Record<string, unknown> = {};
switch (stat_type) {
case "system_information":
case "system_stats":
case "server_state":
case "periphery_information":
if (!server) throw new Error(`${stat_type} requires a server name or ID`);
params.server = server;
break;
case "historical_server_stats":
if (!server) throw new Error(`historical_server_stats requires a server name or ID`);
if (!granularity) throw new Error(`historical_server_stats requires a granularity (e.g. '1-hr', '1-day')`);
params.server = server;
params.granularity = granularity;
if (page !== undefined) params.page = page;
break;
case "containers_summary":
case "version":
case "core_info":
// No params needed
break;
}
const result = await client.rpc("read", requestName, params);
return {
content: [
{
type: "text",
text: JSON.stringify(result, null, 2),
},
],
};
}
+115
View File
@@ -0,0 +1,115 @@
import { z } from "zod";
import { KomodoClient } from "../komodo-client.js";
const UpdateMode = z.enum(["get", "list"]);
export const updatesInputSchema = {
mode: UpdateMode.describe(
"get: retrieve a specific update by ID (returns full execution logs). list: list recent updates (returns summary of recent operations)."
),
id: z.string().optional().describe("Update ID (required for get mode). The update ID from a previous list or execute operation."),
page: z.number().optional().describe("Page number for list mode (0 = most recent, default). Use next_page from response to paginate."),
};
interface UpdateLog {
stage?: string;
command?: string;
stdout?: string;
stderr?: string;
success?: boolean;
start_ts?: number;
end_ts?: number;
}
interface Update {
_id?: { $oid: string };
operation?: string;
start_ts?: number;
success?: boolean;
operator?: string;
target?: { type: string; id: string };
logs?: UpdateLog[];
end_ts?: number;
status?: string;
version?: { major: number; minor: number; patch: number };
commit_hash?: string;
}
function formatUpdate(update: Update): string {
const lines: string[] = [];
const id = update._id?.$oid ?? "unknown";
lines.push(`Update: ${id}`);
lines.push(`Operation: ${update.operation ?? "unknown"}`);
lines.push(`Status: ${update.status ?? "unknown"}`);
lines.push(`Success: ${update.success ?? "unknown"}`);
lines.push(`Operator: ${update.operator ?? "unknown"}`);
if (update.target) lines.push(`Target: ${update.target.type} (${update.target.id})`);
if (update.start_ts) lines.push(`Started: ${new Date(update.start_ts).toISOString()}`);
if (update.end_ts) lines.push(`Ended: ${new Date(update.end_ts).toISOString()}`);
if (update.version) lines.push(`Version: ${update.version.major}.${update.version.minor}.${update.version.patch}`);
if (update.commit_hash) lines.push(`Commit: ${update.commit_hash}`);
if (update.logs && update.logs.length > 0) {
lines.push(`\n--- Logs (${update.logs.length} stages) ---`);
for (const log of update.logs) {
lines.push(`\n[${log.stage ?? "unknown"}] ${log.success ? "OK" : "FAILED"}`);
if (log.command) lines.push(`Command: ${log.command}`);
if (log.start_ts) lines.push(`Start: ${new Date(log.start_ts).toISOString()}`);
if (log.end_ts) lines.push(`End: ${new Date(log.end_ts).toISOString()}`);
if (log.stdout) lines.push(`stdout:\n${log.stdout}`);
if (log.stderr) lines.push(`stderr:\n${log.stderr}`);
}
} else {
lines.push("\nNo logs available.");
}
return lines.join("\n");
}
export async function handleUpdates(
args: {
mode: z.infer<typeof UpdateMode>;
id?: string;
page?: number;
},
client: KomodoClient,
): Promise<{ content: { type: "text"; text: string }[] }> {
const { mode, id, page } = args;
if (mode === "get") {
if (!id) throw new Error(`get mode requires an id (update ID)`);
const result = await client.rpc("read", "GetUpdate", { id });
return {
content: [{ type: "text", text: formatUpdate(result as Update) }],
};
}
// list mode
const params: Record<string, unknown> = {};
if (page !== undefined) params.page = page;
const result = await client.rpc("read", "ListUpdates", params) as {
updates?: Update[];
next_page?: number;
};
const lines: string[] = [];
if (result.updates && result.updates.length > 0) {
lines.push(`Found ${result.updates.length} updates:\n`);
for (const u of result.updates) {
const uId = u._id?.$oid ?? "unknown";
const time = u.start_ts ? new Date(u.start_ts).toISOString() : "unknown";
const target = u.target ? `${u.target.type}` : "unknown";
lines.push(`- [${u.success ? "OK" : "FAIL"}] ${uId} | ${u.operation ?? "?"} | ${target} | ${u.operator ?? "?"} | ${time}`);
}
if (result.next_page !== undefined) {
lines.push(`\nMore pages available. Use page: ${result.next_page}`);
}
} else {
lines.push("No updates found.");
}
return {
content: [{ type: "text", text: lines.join("\n") }],
};
}
+56 -3
View File
@@ -100,7 +100,16 @@ export const ExecuteOperation = z.enum([
"backup_core_database", "backup_core_database",
"global_auto_update", "global_auto_update",
"rotate_all_server_keys", "rotate_all_server_keys",
"rotate_core_keys" "rotate_core_keys",
// Troubleshooting / refresh
"refresh_server",
"refresh_server_containers",
"refresh_stack_cache",
"refresh_stack_content",
"remove_orphan_containers",
"check_stack_for_update",
"check_deployment_for_update",
"get_mcp_token",
]); ]);
export type ExecuteOperation = z.infer<typeof ExecuteOperation>; export type ExecuteOperation = z.infer<typeof ExecuteOperation>;
@@ -294,7 +303,16 @@ export const EXECUTE_REQUEST_MAP: Record<
unpause_container: {"route":"execute","name":"UnpauseContainer"}, unpause_container: {"route":"execute","name":"UnpauseContainer"},
unpause_deployment: {"route":"execute","name":"UnpauseDeployment"}, unpause_deployment: {"route":"execute","name":"UnpauseDeployment"},
unpause_stack: {"route":"execute","name":"UnpauseStack"}, unpause_stack: {"route":"execute","name":"UnpauseStack"},
update_swarm_node: {"route":"execute","name":"UpdateSwarmNode"} update_swarm_node: {"route":"execute","name":"UpdateSwarmNode"},
// Troubleshooting / refresh
refresh_server: {"route":"execute","name":"RunServerRefresh"},
refresh_server_containers: {"route":"execute","name":"RunServerRefreshContainers"},
refresh_stack_cache: {"route":"execute","name":"RunStackRefreshCache"},
refresh_stack_content: {"route":"execute","name":"RunStackRefreshContent"},
remove_orphan_containers: {"route":"execute","name":"RunStackRemoveOrphanContainers"},
check_stack_for_update: {"route":"execute","name":"CheckStackForUpdate"},
check_deployment_for_update: {"route":"execute","name":"CheckDeploymentForUpdate"},
get_mcp_token: {"route":"execute","name":"GetMcpToken"},
}; };
export const SUMMARY_REQUEST_MAP: Partial<Record<ResourceType, string>> = { export const SUMMARY_REQUEST_MAP: Partial<Record<ResourceType, string>> = {
@@ -386,7 +404,11 @@ export const LIST_DETAIL_REQUEST_MAP: Record<string, string> = {
common_stack_build_extra_args: "ListCommonStackBuildExtraArgs", common_stack_build_extra_args: "ListCommonStackBuildExtraArgs",
common_stack_extra_args: "ListCommonStackExtraArgs", common_stack_extra_args: "ListCommonStackExtraArgs",
image_history: "ListImageHistory", image_history: "ListImageHistory",
all_stack_services: "ListAllStackServices" all_stack_services: "ListAllStackServices",
// Troubleshooting additions
terminals: "ListTerminals",
alerts: "ListAlerts",
containers_summary: "GetContainersSummary",
}; };
export const COPY_REQUEST_MAP: Record<string, string> = { export const COPY_REQUEST_MAP: Record<string, string> = {
@@ -418,3 +440,34 @@ export const RENAME_REQUEST_MAP: Record<string, string> = {
tag: "RenameTag", tag: "RenameTag",
user_group: "RenameUserGroup" user_group: "RenameUserGroup"
}; };
export const SERVER_STATS_REQUEST_MAP: Record<string, string> = {
system_information: "GetSystemInformation",
system_stats: "GetSystemStats",
historical_server_stats: "GetHistoricalServerStats",
server_state: "GetServerState",
periphery_information: "GetPeripheryInformation",
containers_summary: "GetContainersSummary",
version: "GetVersion",
core_info: "GetCoreInfo",
};
export const RESOURCE_STATS_REQUEST_MAP: Record<string, string> = {
deployment_stats: "GetDeploymentStats",
deployment_container: "GetDeploymentContainer",
build_monthly_stats: "GetBuildMonthlyStats",
concurrency_limit_usage: "GetConcurrencyLimitUsage",
resource_matching_container: "GetResourceMatchingContainer",
};
export const ACTION_STATE_REQUEST_MAP: Record<string, string> = {
action: "GetActionActionState",
build: "GetBuildActionState",
deployment: "GetDeploymentActionState",
procedure: "GetProcedureActionState",
repo: "GetRepoActionState",
server: "GetServerActionState",
stack: "GetStackActionState",
swarm: "GetSwarmActionState",
sync_resource: "GetResourceSyncActionState",
};