feat: agent-friendly status & logs tools, build log resolution, builder inspect
- Add komodo_status: auto-detects resource type by name, shows state + active ops - Add komodo_resource_logs: auto-detects type and fetches appropriate logs - Add shared resource-detect utility for type auto-detection across 11 resource types - Enhance komodo_logs build case: resolves builder→server→container for real logs - Add builder inspect type to komodo_inspect - Fallback to build resource info when container log resolution fails
This commit is contained in:
+16
-2
@@ -19,6 +19,8 @@ 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";
|
||||
import { statusInputSchema, handleStatus } from "./tools/status.js";
|
||||
import { resourceLogsInputSchema, handleResourceLogs } from "./tools/resource-logs.js";
|
||||
|
||||
const PORT = parseInt(process.env.PORT || "9800", 10);
|
||||
|
||||
@@ -83,7 +85,7 @@ function createServerInstance(client: KomodoClient): McpServer {
|
||||
|
||||
server.registerTool("komodo_logs", {
|
||||
description:
|
||||
"Get logs for a Komodo resource. resource_type: deployment (GetDeploymentLog), stack (GetStackLog), container (GetContainerLog), swarm_service (GetSwarmServiceLog). Returns log lines as text.",
|
||||
"Get logs for a Komodo resource. resource_type: deployment (GetDeploymentLog), stack (GetStackLog), container (GetContainerLog), swarm_service (GetSwarmServiceLog), build (resolves builder container and fetches real build logs, falls back to build resource info). Returns log lines as text.",
|
||||
inputSchema: logsInputSchema,
|
||||
}, async (args) => handleLogs(args, client));
|
||||
|
||||
@@ -95,7 +97,7 @@ function createServerInstance(client: KomodoClient): McpServer {
|
||||
|
||||
server.registerTool("komodo_inspect", {
|
||||
description:
|
||||
"Inspect a Docker object. inspect_type: container, image, network, volume, deployment_container, deployment_swarm_service, stack_container, stack_swarm_info, stack_swarm_service, swarm, swarm_config, swarm_node, swarm_secret, swarm_service, swarm_stack, swarm_task. Provide id and optional server name.",
|
||||
"Inspect a Docker object or resource. inspect_type: container, image, network, volume, deployment_container, deployment_swarm_service, stack_container, stack_swarm_info, stack_swarm_service, swarm, swarm_config, swarm_node, swarm_secret, swarm_service, swarm_stack, swarm_task, builder. Provide id and optional server name.",
|
||||
inputSchema: inspectInputSchema,
|
||||
}, async (args) => handleInspect(args, client));
|
||||
|
||||
@@ -141,6 +143,18 @@ function createServerInstance(client: KomodoClient): McpServer {
|
||||
inputSchema: updatesInputSchema,
|
||||
}, async (args) => handleUpdates(args, client));
|
||||
|
||||
server.registerTool("komodo_status", {
|
||||
description:
|
||||
"Check the status of any Komodo resource by name or ID. Auto-detects resource type (stack, build, server, deployment, procedure, repo, action, alerter, builder, swarm, sync_resource). Returns state, health, active operations, and relevant details. Optionally pass type to skip auto-detection.",
|
||||
inputSchema: statusInputSchema,
|
||||
}, async (args) => handleStatus(args, client));
|
||||
|
||||
server.registerTool("komodo_resource_logs", {
|
||||
description:
|
||||
"Get logs for any Komodo resource by name or ID. Auto-detects resource type and fetches the appropriate logs. For builds, resolves the builder container and fetches real build logs. For deployments/stacks, fetches the container/service logs. Optionally pass type to skip auto-detection.",
|
||||
inputSchema: resourceLogsInputSchema,
|
||||
}, async (args) => handleResourceLogs(args, client));
|
||||
|
||||
return server;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,206 @@
|
||||
import { KomodoClient } from "./komodo-client.js";
|
||||
import type { ResourceType } from "./types.js";
|
||||
|
||||
/**
|
||||
* Resource types we try to detect, in priority order.
|
||||
* Covers the most common agent-facing resources.
|
||||
*/
|
||||
const DETECT_ORDER: ResourceType[] = [
|
||||
"stack",
|
||||
"deployment",
|
||||
"build",
|
||||
"server",
|
||||
"procedure",
|
||||
"repo",
|
||||
"action",
|
||||
"alerter",
|
||||
"builder",
|
||||
"swarm",
|
||||
"sync_resource",
|
||||
];
|
||||
|
||||
/** Map resource type → Komodo Get request name */
|
||||
const GET_REQUEST: Record<string, string> = {
|
||||
stack: "GetStack",
|
||||
deployment: "GetDeployment",
|
||||
build: "GetBuild",
|
||||
server: "GetServer",
|
||||
procedure: "GetProcedure",
|
||||
repo: "GetRepo",
|
||||
action: "GetAction",
|
||||
alerter: "GetAlerter",
|
||||
builder: "GetBuilder",
|
||||
swarm: "GetSwarm",
|
||||
sync_resource: "GetResourceSync",
|
||||
};
|
||||
|
||||
/** Map resource type → param key for Get request (most use "id", some differ) */
|
||||
const GET_PARAM_KEY: Record<string, string> = {
|
||||
builder: "builder",
|
||||
action: "action",
|
||||
tag: "tag",
|
||||
variable: "name",
|
||||
user_group: "user_group",
|
||||
sync_resource: "resource_sync",
|
||||
};
|
||||
|
||||
export interface DetectionResult {
|
||||
type: ResourceType;
|
||||
resource: Record<string, unknown>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Auto-detect a resource by trying Get endpoints across types.
|
||||
* Returns the first match, or null if nothing found.
|
||||
*/
|
||||
export async function detectResource(
|
||||
client: KomodoClient,
|
||||
nameOrId: string,
|
||||
hint?: ResourceType,
|
||||
): Promise<DetectionResult | null> {
|
||||
const typesToTry = hint ? [hint, ...DETECT_ORDER.filter((t) => t !== hint)] : DETECT_ORDER;
|
||||
|
||||
for (const type of typesToTry) {
|
||||
const requestName = GET_REQUEST[type];
|
||||
if (!requestName) continue;
|
||||
|
||||
const paramKey = GET_PARAM_KEY[type] || "id";
|
||||
try {
|
||||
const resource = (await client.rpc("read", requestName, {
|
||||
[paramKey]: nameOrId,
|
||||
})) as Record<string, unknown>;
|
||||
|
||||
if (resource && (resource.name || resource._id)) {
|
||||
return { type, resource };
|
||||
}
|
||||
} catch {
|
||||
// Not found on this type — try next
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format a human-readable status summary for a resource.
|
||||
*/
|
||||
export function formatStatus(result: DetectionResult): string {
|
||||
const { type, resource } = result;
|
||||
const name = (resource.name as string) ?? "unknown";
|
||||
const info = resource?.info as Record<string, unknown> | undefined;
|
||||
const config = resource?.config as Record<string, unknown> | undefined;
|
||||
|
||||
const lines: string[] = [];
|
||||
lines.push(`Resource: ${name}`);
|
||||
lines.push(`Type: ${type}`);
|
||||
|
||||
switch (type) {
|
||||
case "server": {
|
||||
const state = info?.state ?? "unknown";
|
||||
lines.push(`State: ${state}`);
|
||||
if (info?.err) lines.push(`Error: ${info.err}`);
|
||||
const stats = info?.stats as Record<string, unknown> | undefined;
|
||||
if (stats) {
|
||||
lines.push(`CPU: ${stats.cpu_perc ?? "?"}%`);
|
||||
const memUsed = stats.mem_used_gb as number | undefined;
|
||||
const memTotal = stats.mem_total_gb as number | undefined;
|
||||
if (memUsed != null && memTotal != null) {
|
||||
lines.push(`Memory: ${memUsed.toFixed(1)} / ${memTotal.toFixed(1)} GB (${((memUsed / memTotal) * 100).toFixed(0)}%)`);
|
||||
}
|
||||
const diskUsed = stats.disk_used_gb as number | undefined;
|
||||
const diskTotal = stats.disk_total_gb as number | undefined;
|
||||
if (diskUsed != null && diskTotal != null) {
|
||||
lines.push(`Disk: ${diskUsed.toFixed(1)} / ${diskTotal.toFixed(1)} GB (${((diskUsed / diskTotal) * 100).toFixed(0)}%)`);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "stack": {
|
||||
const state = info?.state ?? "unknown";
|
||||
lines.push(`State: ${state}`);
|
||||
const status = info?.status as string | undefined;
|
||||
if (status) lines.push(`Status: ${status}`);
|
||||
const services = info?.services as Array<Record<string, unknown>> | undefined;
|
||||
if (services && services.length > 0) {
|
||||
lines.push(`Services (${services.length}):`);
|
||||
for (const svc of services) {
|
||||
lines.push(` - ${svc.service ?? "?"}: ${svc.state ?? "unknown"}`);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "deployment": {
|
||||
const state = info?.state ?? "unknown";
|
||||
lines.push(`State: ${state}`);
|
||||
const status = info?.status as string | undefined;
|
||||
if (status) lines.push(`Status: ${status}`);
|
||||
break;
|
||||
}
|
||||
case "build": {
|
||||
const state = info?.state ?? "unknown";
|
||||
lines.push(`State: ${state}`);
|
||||
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?.remote_error) lines.push(`Remote error: ${info.remote_error}`);
|
||||
const builderId = config?.builder_id as string | undefined;
|
||||
if (builderId) lines.push(`Builder: ${builderId}`);
|
||||
break;
|
||||
}
|
||||
case "procedure": {
|
||||
const running = info?.running ?? false;
|
||||
lines.push(`Running: ${running}`);
|
||||
break;
|
||||
}
|
||||
case "repo": {
|
||||
const state = info?.state ?? "unknown";
|
||||
lines.push(`State: ${state}`);
|
||||
if (info?.last_built_at) {
|
||||
lines.push(`Last built: ${new Date(info.last_built_at as number).toISOString()}`);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "action": {
|
||||
const running = info?.running ?? false;
|
||||
lines.push(`Running: ${running}`);
|
||||
if (info?.last_run_at) {
|
||||
lines.push(`Last run: ${new Date(info.last_run_at as number).toISOString()}`);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "alerter": {
|
||||
const enabled = (config as Record<string, unknown>)?.enabled ?? "unknown";
|
||||
lines.push(`Enabled: ${enabled}`);
|
||||
break;
|
||||
}
|
||||
case "builder": {
|
||||
const builderType = config?.type ?? "unknown";
|
||||
lines.push(`Builder type: ${builderType}`);
|
||||
const params = config?.params as Record<string, unknown> | undefined;
|
||||
if (builderType === "Server" && params?.server_ids) {
|
||||
lines.push(`Server IDs: ${(params.server_ids as string[]).join(", ")}`);
|
||||
} else if (builderType === "Url" && params?.address) {
|
||||
lines.push(`Address: ${params.address}`);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "swarm": {
|
||||
const state = info?.state ?? "unknown";
|
||||
lines.push(`State: ${state}`);
|
||||
break;
|
||||
}
|
||||
case "sync_resource": {
|
||||
const lastStatus = info?.last_sync_status as string | undefined;
|
||||
lines.push(`Last sync status: ${lastStatus ?? "never"}`);
|
||||
if (info?.last_sync_ts) {
|
||||
lines.push(`Last sync: ${new Date(info.last_sync_ts as number).toISOString()}`);
|
||||
}
|
||||
break;
|
||||
}
|
||||
default:
|
||||
lines.push(`State: ${JSON.stringify(info?.state ?? "unknown")}`);
|
||||
}
|
||||
|
||||
return lines.join("\n");
|
||||
}
|
||||
@@ -8,6 +8,7 @@ const InspectType = z.enum([
|
||||
"stack_container", "stack_swarm_info", "stack_swarm_service",
|
||||
"swarm", "swarm_config", "swarm_node", "swarm_secret",
|
||||
"swarm_service", "swarm_stack", "swarm_task",
|
||||
"builder",
|
||||
]);
|
||||
|
||||
// Types that need {deployment/stack, service} instead of {id}
|
||||
@@ -51,6 +52,9 @@ export async function handleInspect(
|
||||
params.stack = id;
|
||||
if (service) params.service = service;
|
||||
if (server) params.server = server;
|
||||
} else if (inspect_type === "builder") {
|
||||
// GetBuilder: {builder}
|
||||
params.builder = id;
|
||||
} else {
|
||||
const paramKey = server ? INSPECT_PARAM_KEY[inspect_type] : undefined;
|
||||
if (paramKey) {
|
||||
|
||||
+52
-2
@@ -5,7 +5,7 @@ const LogResourceType = z.enum(["deployment", "stack", "container", "swarm_servi
|
||||
|
||||
export const logsInputSchema = {
|
||||
resource_type: LogResourceType.describe(
|
||||
"Resource type to get logs for (deployment, stack, container, swarm_service). Returns log lines as text."
|
||||
"Resource type to get logs for (deployment, stack, container, swarm_service, build). Build resolves the builder container and fetches real logs. Returns log lines as text."
|
||||
),
|
||||
id: z.string().describe("Resource ID or name"),
|
||||
tail: z
|
||||
@@ -65,10 +65,60 @@ export async function handleLogs(
|
||||
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 buildName = (build?.name as string) ?? id;
|
||||
|
||||
// Try to resolve the builder and fetch real container logs
|
||||
const builderId = config?.builder_id as string | undefined;
|
||||
if (builderId) {
|
||||
try {
|
||||
const builder = (await client.rpc("read", "GetBuilder", { builder: builderId })) as Record<string, unknown>;
|
||||
const builderConfig = builder?.config as Record<string, unknown> | undefined;
|
||||
const builderName = (builder?.name as string) ?? builderId;
|
||||
const builderType = builderConfig?.type as string | undefined;
|
||||
|
||||
let serverName: string | undefined;
|
||||
|
||||
if (builderType === "Server" && builderConfig?.params) {
|
||||
const params = builderConfig.params as Record<string, unknown>;
|
||||
const serverIds = params.server_ids as string[] | undefined;
|
||||
if (serverIds && serverIds.length > 0) {
|
||||
// Resolve first server_id to server name
|
||||
try {
|
||||
const servers = (await client.rpc("read", "ListServers", {})) as Array<Record<string, unknown>>;
|
||||
const targetId = serverIds[0];
|
||||
const server = servers.find((s) => {
|
||||
const id = s._id as Record<string, unknown> | undefined;
|
||||
return id?.$oid === targetId || s.name === targetId;
|
||||
});
|
||||
if (server) serverName = server.name as string;
|
||||
} catch { /* server lookup failed, try without */ }
|
||||
}
|
||||
} else if (builderType === "Url" && builderConfig?.params) {
|
||||
const params = builderConfig.params as Record<string, unknown>;
|
||||
// Url builder uses a Periphery address directly — use it as server reference
|
||||
serverName = params.address as string | undefined;
|
||||
}
|
||||
|
||||
if (serverName) {
|
||||
const logParams: Record<string, unknown> = {
|
||||
server: serverName,
|
||||
container: builderName,
|
||||
};
|
||||
if (tail !== undefined) logParams.tail = tail;
|
||||
result = await client.rpc("read", "GetContainerLog", logParams);
|
||||
break;
|
||||
}
|
||||
} catch {
|
||||
// Builder lookup or container log fetch failed — fall through to build info
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: return build resource info (limited on failure)
|
||||
const lines: string[] = [];
|
||||
lines.push(`Build: ${build?.name ?? id}`);
|
||||
lines.push(`Build: ${buildName}`);
|
||||
if (config?.repo) lines.push(`Repo: ${config.repo}`);
|
||||
if (config?.branch) lines.push(`Branch: ${config.branch}`);
|
||||
if (builderId) lines.push(`Builder: ${builderId}`);
|
||||
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}`);
|
||||
|
||||
@@ -0,0 +1,221 @@
|
||||
import { z } from "zod";
|
||||
import { KomodoClient } from "../komodo-client.js";
|
||||
import { ResourceType } from "../types.js";
|
||||
import { detectResource } from "../resource-detect.js";
|
||||
|
||||
export const resourceLogsInputSchema = {
|
||||
resource: z
|
||||
.string()
|
||||
.describe("Name or ID of the resource to get logs for"),
|
||||
type: ResourceType.optional().describe(
|
||||
"Resource type hint to skip auto-detection (e.g. 'stack', 'deployment', 'container'). If omitted, auto-detects by trying all types."
|
||||
),
|
||||
tail: z
|
||||
.number()
|
||||
.optional()
|
||||
.describe("Number of recent log lines to return"),
|
||||
services: z
|
||||
.array(z.string())
|
||||
.optional()
|
||||
.describe("Service names to include (used for stack logs)"),
|
||||
};
|
||||
|
||||
/** Map resource type → log request name */
|
||||
const LOG_REQUEST: Record<string, string> = {
|
||||
deployment: "GetDeploymentLog",
|
||||
stack: "GetStackLog",
|
||||
};
|
||||
|
||||
export async function handleResourceLogs(
|
||||
args: {
|
||||
resource: string;
|
||||
type?: z.infer<typeof ResourceType>;
|
||||
tail?: number;
|
||||
services?: string[];
|
||||
},
|
||||
client: KomodoClient,
|
||||
): Promise<{ content: { type: "text"; text: string }[] }> {
|
||||
const { resource, type, tail, services } = args;
|
||||
|
||||
// Handle container type directly (not a Komodo resource — needs server name)
|
||||
if (type === "container" as string) {
|
||||
return fetchContainerLogs(client, resource, tail);
|
||||
}
|
||||
|
||||
// Auto-detect the resource
|
||||
const detected = await detectResource(client, resource, type);
|
||||
if (!detected) {
|
||||
return {
|
||||
content: [{
|
||||
type: "text",
|
||||
text: `Resource not found: "${resource}". Try a different name or ID, or specify the type parameter.`,
|
||||
}],
|
||||
};
|
||||
}
|
||||
|
||||
const { type: resType, resource: res } = detected;
|
||||
const name = (res.name as string) ?? resource;
|
||||
|
||||
// Get logs based on resource type
|
||||
const logRequest = LOG_REQUEST[resType];
|
||||
|
||||
if (resType === "build") {
|
||||
// Build logs need special handling: resolve builder container
|
||||
return fetchBuildLogs(client, res, tail);
|
||||
}
|
||||
|
||||
if (!logRequest) {
|
||||
return {
|
||||
content: [{
|
||||
type: "text",
|
||||
text: `Logs not available for resource type "${resType}" (${name}). Use komodo_get to view the resource details.`,
|
||||
}],
|
||||
};
|
||||
}
|
||||
|
||||
// Build params based on resource type
|
||||
const params: Record<string, unknown> = {};
|
||||
|
||||
switch (resType as string) {
|
||||
case "deployment":
|
||||
params.id = name;
|
||||
break;
|
||||
case "stack":
|
||||
params.id = name;
|
||||
params.services = services ?? [];
|
||||
break;
|
||||
}
|
||||
|
||||
if (tail !== undefined) params.tail = tail;
|
||||
|
||||
try {
|
||||
const result = await client.rpc("read", logRequest, params);
|
||||
return {
|
||||
content: [{
|
||||
type: "text",
|
||||
text: typeof result === "string" ? result : JSON.stringify(result, null, 2),
|
||||
}],
|
||||
};
|
||||
} catch (err) {
|
||||
return {
|
||||
content: [{
|
||||
type: "text",
|
||||
text: `Failed to fetch logs for ${resType} "${name}": ${err instanceof Error ? err.message : err}`,
|
||||
}],
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchContainerLogs(
|
||||
client: KomodoClient,
|
||||
container: string,
|
||||
tail?: number,
|
||||
): Promise<{ content: { type: "text"; text: string }[] }> {
|
||||
// Container requires server name — try to find it via resource_matching_container
|
||||
try {
|
||||
const match = (await client.rpc("read", "GetResourceMatchingContainer", {
|
||||
container,
|
||||
})) as Record<string, unknown> | null;
|
||||
|
||||
if (match?.server) {
|
||||
const params: Record<string, unknown> = {
|
||||
server: match.server,
|
||||
container,
|
||||
};
|
||||
if (tail !== undefined) params.tail = tail;
|
||||
const result = await client.rpc("read", "GetContainerLog", params);
|
||||
return {
|
||||
content: [{
|
||||
type: "text",
|
||||
text: typeof result === "string" ? result : JSON.stringify(result, null, 2),
|
||||
}],
|
||||
};
|
||||
}
|
||||
} catch {
|
||||
// Fall through
|
||||
}
|
||||
|
||||
return {
|
||||
content: [{
|
||||
type: "text",
|
||||
text: `Cannot fetch logs for container "${container}": server name is required. Use komodo_logs with resource_type="container" and provide the server parameter.`,
|
||||
}],
|
||||
};
|
||||
}
|
||||
|
||||
async function fetchBuildLogs(
|
||||
client: KomodoClient,
|
||||
build: Record<string, unknown>,
|
||||
tail?: number,
|
||||
): Promise<{ content: { type: "text"; text: string }[] }> {
|
||||
const config = build?.config as Record<string, unknown> | undefined;
|
||||
const info = build?.info as Record<string, unknown> | undefined;
|
||||
const buildName = (build?.name as string) ?? "unknown";
|
||||
const builderId = config?.builder_id as string | undefined;
|
||||
|
||||
if (!builderId) {
|
||||
return {
|
||||
content: [{
|
||||
type: "text",
|
||||
text: `Build "${buildName}" has no builder configured.`,
|
||||
}],
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
const builder = (await client.rpc("read", "GetBuilder", {
|
||||
builder: builderId,
|
||||
})) as Record<string, unknown>;
|
||||
const builderConfig = builder?.config as Record<string, unknown> | undefined;
|
||||
const builderName = (builder?.name as string) ?? builderId;
|
||||
const builderType = builderConfig?.type as string | undefined;
|
||||
|
||||
let serverName: string | undefined;
|
||||
|
||||
if (builderType === "Server" && builderConfig?.params) {
|
||||
const params = builderConfig.params as Record<string, unknown>;
|
||||
const serverIds = params.server_ids as string[] | undefined;
|
||||
if (serverIds && serverIds.length > 0) {
|
||||
const servers = (await client.rpc("read", "ListServers", {})) as Array<Record<string, unknown>>;
|
||||
const targetId = serverIds[0];
|
||||
const server = servers.find((s) => {
|
||||
const id = s._id as Record<string, unknown> | undefined;
|
||||
return id?.$oid === targetId || s.name === targetId;
|
||||
});
|
||||
if (server) serverName = server.name as string;
|
||||
}
|
||||
} else if (builderType === "Url" && builderConfig?.params) {
|
||||
const params = builderConfig.params as Record<string, unknown>;
|
||||
serverName = params.address as string | undefined;
|
||||
}
|
||||
|
||||
if (serverName) {
|
||||
const logParams: Record<string, unknown> = {
|
||||
server: serverName,
|
||||
container: builderName,
|
||||
};
|
||||
if (tail !== undefined) logParams.tail = tail;
|
||||
const result = await client.rpc("read", "GetContainerLog", logParams);
|
||||
return {
|
||||
content: [{
|
||||
type: "text",
|
||||
text: typeof result === "string" ? result : JSON.stringify(result, null, 2),
|
||||
}],
|
||||
};
|
||||
}
|
||||
} catch {
|
||||
// Fall through to build info
|
||||
}
|
||||
|
||||
// Fallback: build resource info
|
||||
const lines: string[] = [];
|
||||
lines.push(`Build: ${buildName} (builder container logs unavailable)`);
|
||||
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?.remote_error) lines.push(`Remote error: ${info.remote_error}`);
|
||||
return {
|
||||
content: [{ type: "text", text: lines.join("\n") }],
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
import { z } from "zod";
|
||||
import { KomodoClient } from "../komodo-client.js";
|
||||
import { ResourceType } from "../types.js";
|
||||
import { detectResource, formatStatus } from "../resource-detect.js";
|
||||
|
||||
export const statusInputSchema = {
|
||||
resource: z
|
||||
.string()
|
||||
.describe("Name or ID of the resource to check"),
|
||||
type: ResourceType.optional().describe(
|
||||
"Resource type hint to skip auto-detection (e.g. 'stack', 'build', 'server'). If omitted, auto-detects by trying all types."
|
||||
),
|
||||
};
|
||||
|
||||
export async function handleStatus(
|
||||
args: {
|
||||
resource: string;
|
||||
type?: z.infer<typeof ResourceType>;
|
||||
},
|
||||
client: KomodoClient,
|
||||
): Promise<{ content: { type: "text"; text: string }[] }> {
|
||||
const { resource, type } = args;
|
||||
|
||||
const detected = await detectResource(client, resource, type);
|
||||
if (!detected) {
|
||||
return {
|
||||
content: [{
|
||||
type: "text",
|
||||
text: `Resource not found: "${resource}". Try a different name or ID, or specify the type parameter.`,
|
||||
}],
|
||||
};
|
||||
}
|
||||
|
||||
const statusText = formatStatus(detected);
|
||||
|
||||
// Also fetch action state to show if anything is currently running
|
||||
const actionStateMap: Record<string, string> = {
|
||||
stack: "GetStackActionState",
|
||||
deployment: "GetDeploymentActionState",
|
||||
build: "GetBuildActionState",
|
||||
server: "GetServerActionState",
|
||||
procedure: "GetProcedureActionState",
|
||||
repo: "GetRepoActionState",
|
||||
action: "GetActionActionState",
|
||||
sync_resource: "GetResourceSyncActionState",
|
||||
swarm: "GetSwarmActionState",
|
||||
};
|
||||
|
||||
const actionRequest = actionStateMap[detected.type];
|
||||
let actionStateText = "";
|
||||
|
||||
if (actionRequest) {
|
||||
try {
|
||||
const paramKey = detected.type === "sync_resource" ? "resource_sync" : detected.type;
|
||||
const state = (await client.rpc("read", actionRequest, {
|
||||
[paramKey]: resource,
|
||||
})) as Record<string, unknown>;
|
||||
|
||||
// Check if any field is truthy (meaning an operation is in progress)
|
||||
const activeOps = Object.entries(state).filter(
|
||||
([, v]) => v === true || (typeof v === "number" && v > 0)
|
||||
);
|
||||
|
||||
if (activeOps.length > 0) {
|
||||
actionStateText = `\nActive operations: ${activeOps.map(([k]) => k).join(", ")}`;
|
||||
} else {
|
||||
actionStateText = "\nNo active operations";
|
||||
}
|
||||
} catch {
|
||||
// Action state not available — not critical
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
content: [{
|
||||
type: "text",
|
||||
text: statusText + actionStateText,
|
||||
}],
|
||||
};
|
||||
}
|
||||
+2
-1
@@ -352,7 +352,8 @@ export const INSPECT_REQUEST_MAP: Record<string, string> = {
|
||||
swarm_secret: "InspectSwarmSecret",
|
||||
swarm_service: "InspectSwarmService",
|
||||
swarm_stack: "InspectSwarmStack",
|
||||
swarm_task: "InspectSwarmTask"
|
||||
swarm_task: "InspectSwarmTask",
|
||||
builder: "GetBuilder"
|
||||
};
|
||||
|
||||
export const SEARCH_LOG_REQUEST_MAP: Record<string, string> = {
|
||||
|
||||
Reference in New Issue
Block a user