feat: add MCP healthcheck probes and new templates (grafana, home-assistant, node-red)
Some checks failed
CI / lint (pull_request) Has been cancelled
CI / typecheck (pull_request) Has been cancelled
CI / test (pull_request) Has been cancelled
CI / build (pull_request) Has been cancelled
CI / package (pull_request) Has been cancelled

- Add healthCheck spec to templates and servers (tool, arguments, interval, timeout, failureThreshold)
- Add healthStatus, lastHealthCheck, events fields to instances
- Create grafana, home-assistant, node-red templates with healthcheck probes
- Add healthcheck probes to existing templates (github, slack, postgres, jira)
- Show HEALTH column in `get instances` and Events section in `describe instance`
- Display healthCheck details in `describe server` and `describe template`
- Schema + storage + display only; actual probe runner is future work

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Michal
2026-02-22 22:48:59 +00:00
parent 93adcd4be7
commit c97219f85e
22 changed files with 178 additions and 8 deletions

View File

@@ -69,6 +69,7 @@ async function main(): Promise<void> {
transport: parsed.transport ?? 'STDIO',
version: parsed.version ?? '1.0.0',
description: parsed.description ?? '',
...(parsed.healthCheck ? { healthCheck: parsed.healthCheck } : {}),
};
});
await seedTemplates(prisma, templates);

View File

@@ -16,7 +16,7 @@ export interface IMcpInstanceRepository {
findById(id: string): Promise<McpInstance | null>;
findByContainerId(containerId: string): Promise<McpInstance | null>;
create(data: { serverId: string; containerId?: string; status?: InstanceStatus; port?: number; metadata?: Record<string, unknown> }): Promise<McpInstance>;
updateStatus(id: string, status: InstanceStatus, fields?: { containerId?: string; port?: number; metadata?: Record<string, unknown> }): Promise<McpInstance>;
updateStatus(id: string, status: InstanceStatus, fields?: { containerId?: string; port?: number; metadata?: Record<string, unknown>; healthStatus?: string; lastHealthCheck?: Date; events?: unknown[] }): Promise<McpInstance>;
delete(id: string): Promise<void>;
}

View File

@@ -44,7 +44,7 @@ export class McpInstanceRepository implements IMcpInstanceRepository {
async updateStatus(
id: string,
status: InstanceStatus,
fields?: { containerId?: string; port?: number; metadata?: Record<string, unknown> },
fields?: { containerId?: string; port?: number; metadata?: Record<string, unknown>; healthStatus?: string; lastHealthCheck?: Date; events?: unknown[] },
): Promise<McpInstance> {
const updateData: Prisma.McpInstanceUpdateInput = {
status,
@@ -59,6 +59,15 @@ export class McpInstanceRepository implements IMcpInstanceRepository {
if (fields?.metadata !== undefined) {
updateData.metadata = fields.metadata as Prisma.InputJsonValue;
}
if (fields?.healthStatus !== undefined) {
updateData.healthStatus = fields.healthStatus;
}
if (fields?.lastHealthCheck !== undefined) {
updateData.lastHealthCheck = fields.lastHealthCheck;
}
if (fields?.events !== undefined) {
updateData.events = fields.events as unknown as Prisma.InputJsonValue;
}
return this.prisma.mcpInstance.update({
where: { id },
data: updateData,

View File

@@ -31,6 +31,7 @@ export class McpServerRepository implements IMcpServerRepository {
containerPort: data.containerPort ?? null,
replicas: data.replicas,
env: data.env,
healthCheck: (data.healthCheck ?? Prisma.JsonNull) as Prisma.InputJsonValue,
},
});
}
@@ -47,6 +48,7 @@ export class McpServerRepository implements IMcpServerRepository {
if (data.containerPort !== undefined) updateData['containerPort'] = data.containerPort;
if (data.replicas !== undefined) updateData['replicas'] = data.replicas;
if (data.env !== undefined) updateData['env'] = data.env;
if (data.healthCheck !== undefined) updateData['healthCheck'] = (data.healthCheck ?? Prisma.JsonNull) as Prisma.InputJsonValue;
return this.prisma.mcpServer.update({ where: { id }, data: updateData });
}

View File

@@ -50,6 +50,7 @@ export class TemplateRepository implements ITemplateRepository {
containerPort: data.containerPort ?? null,
replicas: data.replicas,
env: (data.env ?? []) as unknown as Prisma.InputJsonValue,
healthCheck: (data.healthCheck ?? Prisma.JsonNull) as Prisma.InputJsonValue,
},
});
}
@@ -67,6 +68,7 @@ export class TemplateRepository implements ITemplateRepository {
if (data.containerPort !== undefined) updateData.containerPort = data.containerPort;
if (data.replicas !== undefined) updateData.replicas = data.replicas;
if (data.env !== undefined) updateData.env = (data.env ?? []) as Prisma.InputJsonValue;
if (data.healthCheck !== undefined) updateData.healthCheck = (data.healthCheck ?? Prisma.JsonNull) as Prisma.InputJsonValue;
return this.prisma.mcpTemplate.update({
where: { id },

View File

@@ -24,6 +24,7 @@ function loadTemplatesFromDir(dir: string): SeedTemplate[] {
transport: parsed.transport ?? 'STDIO',
version: parsed.version ?? '1.0.0',
description: parsed.description ?? '',
...(parsed.healthCheck ? { healthCheck: parsed.healthCheck } : {}),
});
}
}

View File

@@ -1,4 +1,5 @@
import { z } from 'zod';
import { HealthCheckSchema } from './template.schema.js';
const SecretRefSchema = z.object({
name: z.string().min(1),
@@ -30,6 +31,7 @@ export const CreateMcpServerSchema = z.object({
containerPort: z.number().int().min(1).max(65535).optional(),
replicas: z.number().int().min(0).max(10).default(1),
env: z.array(ServerEnvEntrySchema).default([]),
healthCheck: HealthCheckSchema.optional(),
});
export const UpdateMcpServerSchema = z.object({
@@ -43,6 +45,7 @@ export const UpdateMcpServerSchema = z.object({
containerPort: z.number().int().min(1).max(65535).nullable().optional(),
replicas: z.number().int().min(0).max(10).optional(),
env: z.array(ServerEnvEntrySchema).optional(),
healthCheck: HealthCheckSchema.nullable().optional(),
});
export type CreateMcpServerInput = z.infer<typeof CreateMcpServerSchema>;

View File

@@ -7,6 +7,16 @@ const TemplateEnvEntrySchema = z.object({
defaultValue: z.string().optional(),
});
export const HealthCheckSchema = z.object({
tool: z.string().min(1),
arguments: z.record(z.unknown()).default({}),
intervalSeconds: z.number().int().min(5).max(3600).default(60),
timeoutSeconds: z.number().int().min(1).max(120).default(10),
failureThreshold: z.number().int().min(1).max(20).default(3),
});
export type HealthCheckInput = z.infer<typeof HealthCheckSchema>;
export const CreateTemplateSchema = z.object({
name: z.string().min(1).max(100).regex(/^[a-z0-9-]+$/, 'Name must be lowercase alphanumeric with hyphens'),
version: z.string().default('1.0.0'),
@@ -20,6 +30,7 @@ export const CreateTemplateSchema = z.object({
containerPort: z.number().int().min(1).max(65535).optional(),
replicas: z.number().int().min(0).max(10).default(1),
env: z.array(TemplateEnvEntrySchema).default([]),
healthCheck: HealthCheckSchema.optional(),
});
export const UpdateTemplateSchema = CreateTemplateSchema.partial().omit({ name: true });