Merge pull request 'feat: MCP healthcheck probes + new templates' (#9) from feat/healthcheck-probes into main
This commit was merged in pull request #9.
This commit is contained in:
@@ -288,7 +288,7 @@ update_stack() {
|
|||||||
"env": $env,
|
"env": $env,
|
||||||
"stackFileContent": $stackFileContent,
|
"stackFileContent": $stackFileContent,
|
||||||
"prune": true,
|
"prune": true,
|
||||||
"pullImage": false
|
"pullImage": true
|
||||||
}')
|
}')
|
||||||
|
|
||||||
local response
|
local response
|
||||||
|
|||||||
@@ -4,6 +4,14 @@ import yaml from 'js-yaml';
|
|||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
import type { ApiClient } from '../api-client.js';
|
import type { ApiClient } from '../api-client.js';
|
||||||
|
|
||||||
|
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),
|
||||||
|
});
|
||||||
|
|
||||||
const ServerEnvEntrySchema = z.object({
|
const ServerEnvEntrySchema = z.object({
|
||||||
name: z.string().min(1),
|
name: z.string().min(1),
|
||||||
value: z.string().optional(),
|
value: z.string().optional(),
|
||||||
@@ -24,6 +32,7 @@ const ServerSpecSchema = z.object({
|
|||||||
containerPort: z.number().int().min(1).max(65535).optional(),
|
containerPort: z.number().int().min(1).max(65535).optional(),
|
||||||
replicas: z.number().int().min(0).max(10).default(1),
|
replicas: z.number().int().min(0).max(10).default(1),
|
||||||
env: z.array(ServerEnvEntrySchema).default([]),
|
env: z.array(ServerEnvEntrySchema).default([]),
|
||||||
|
healthCheck: HealthCheckSchema.optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
const SecretSpecSchema = z.object({
|
const SecretSpecSchema = z.object({
|
||||||
@@ -51,6 +60,7 @@ const TemplateSpecSchema = z.object({
|
|||||||
containerPort: z.number().int().min(1).max(65535).optional(),
|
containerPort: z.number().int().min(1).max(65535).optional(),
|
||||||
replicas: z.number().int().min(0).max(10).default(1),
|
replicas: z.number().int().min(0).max(10).default(1),
|
||||||
env: z.array(TemplateEnvEntrySchema).default([]),
|
env: z.array(TemplateEnvEntrySchema).default([]),
|
||||||
|
healthCheck: HealthCheckSchema.optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
const ProjectSpecSchema = z.object({
|
const ProjectSpecSchema = z.object({
|
||||||
|
|||||||
@@ -50,6 +50,19 @@ function formatServerDetail(server: Record<string, unknown>): string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const hc = server.healthCheck as { tool: string; arguments?: Record<string, unknown>; intervalSeconds?: number; timeoutSeconds?: number; failureThreshold?: number } | null;
|
||||||
|
if (hc) {
|
||||||
|
lines.push('');
|
||||||
|
lines.push('Health Check:');
|
||||||
|
lines.push(` ${pad('Tool:', 22)}${hc.tool}`);
|
||||||
|
if (hc.arguments && Object.keys(hc.arguments).length > 0) {
|
||||||
|
lines.push(` ${pad('Arguments:', 22)}${JSON.stringify(hc.arguments)}`);
|
||||||
|
}
|
||||||
|
lines.push(` ${pad('Interval:', 22)}${hc.intervalSeconds ?? 60}s`);
|
||||||
|
lines.push(` ${pad('Timeout:', 22)}${hc.timeoutSeconds ?? 10}s`);
|
||||||
|
lines.push(` ${pad('Failure Threshold:', 22)}${hc.failureThreshold ?? 3}`);
|
||||||
|
}
|
||||||
|
|
||||||
lines.push('');
|
lines.push('');
|
||||||
lines.push('Metadata:');
|
lines.push('Metadata:');
|
||||||
lines.push(` ${pad('ID:', 12)}${server.id}`);
|
lines.push(` ${pad('ID:', 12)}${server.id}`);
|
||||||
@@ -67,6 +80,16 @@ function formatInstanceDetail(instance: Record<string, unknown>, inspect?: Recor
|
|||||||
lines.push(`${pad('Container ID:')}${instance.containerId ?? '-'}`);
|
lines.push(`${pad('Container ID:')}${instance.containerId ?? '-'}`);
|
||||||
lines.push(`${pad('Port:')}${instance.port ?? '-'}`);
|
lines.push(`${pad('Port:')}${instance.port ?? '-'}`);
|
||||||
|
|
||||||
|
// Health section
|
||||||
|
const healthStatus = instance.healthStatus as string | null;
|
||||||
|
const lastHealthCheck = instance.lastHealthCheck as string | null;
|
||||||
|
if (healthStatus || lastHealthCheck) {
|
||||||
|
lines.push('');
|
||||||
|
lines.push('Health:');
|
||||||
|
lines.push(` ${pad('Status:', 16)}${healthStatus ?? 'unknown'}`);
|
||||||
|
if (lastHealthCheck) lines.push(` ${pad('Last Check:', 16)}${lastHealthCheck}`);
|
||||||
|
}
|
||||||
|
|
||||||
const metadata = instance.metadata as Record<string, unknown> | undefined;
|
const metadata = instance.metadata as Record<string, unknown> | undefined;
|
||||||
if (metadata && Object.keys(metadata).length > 0) {
|
if (metadata && Object.keys(metadata).length > 0) {
|
||||||
lines.push('');
|
lines.push('');
|
||||||
@@ -88,6 +111,19 @@ function formatInstanceDetail(instance: Record<string, unknown>, inspect?: Recor
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Events section (k8s-style)
|
||||||
|
const events = instance.events as Array<{ timestamp: string; type: string; message: string }> | undefined;
|
||||||
|
if (events && events.length > 0) {
|
||||||
|
lines.push('');
|
||||||
|
lines.push('Events:');
|
||||||
|
const tsW = 26;
|
||||||
|
const typeW = 10;
|
||||||
|
lines.push(` ${'TIMESTAMP'.padEnd(tsW)}${'TYPE'.padEnd(typeW)}MESSAGE`);
|
||||||
|
for (const ev of events) {
|
||||||
|
lines.push(` ${(ev.timestamp ?? '').padEnd(tsW)}${(ev.type ?? '').padEnd(typeW)}${ev.message ?? ''}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
lines.push('');
|
lines.push('');
|
||||||
lines.push(` ${pad('ID:', 12)}${instance.id}`);
|
lines.push(` ${pad('ID:', 12)}${instance.id}`);
|
||||||
if (instance.createdAt) lines.push(` ${pad('Created:', 12)}${instance.createdAt}`);
|
if (instance.createdAt) lines.push(` ${pad('Created:', 12)}${instance.createdAt}`);
|
||||||
@@ -177,6 +213,19 @@ function formatTemplateDetail(template: Record<string, unknown>): string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const hc = template.healthCheck as { tool: string; arguments?: Record<string, unknown>; intervalSeconds?: number; timeoutSeconds?: number; failureThreshold?: number } | null;
|
||||||
|
if (hc) {
|
||||||
|
lines.push('');
|
||||||
|
lines.push('Health Check:');
|
||||||
|
lines.push(` ${pad('Tool:', 22)}${hc.tool}`);
|
||||||
|
if (hc.arguments && Object.keys(hc.arguments).length > 0) {
|
||||||
|
lines.push(` ${pad('Arguments:', 22)}${JSON.stringify(hc.arguments)}`);
|
||||||
|
}
|
||||||
|
lines.push(` ${pad('Interval:', 22)}${hc.intervalSeconds ?? 60}s`);
|
||||||
|
lines.push(` ${pad('Timeout:', 22)}${hc.timeoutSeconds ?? 10}s`);
|
||||||
|
lines.push(` ${pad('Failure Threshold:', 22)}${hc.failureThreshold ?? 3}`);
|
||||||
|
}
|
||||||
|
|
||||||
lines.push('');
|
lines.push('');
|
||||||
lines.push('Usage:');
|
lines.push('Usage:');
|
||||||
lines.push(` mcpctl create server my-${template.name} --from-template=${template.name}`);
|
lines.push(` mcpctl create server my-${template.name} --from-template=${template.name}`);
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ interface InstanceRow {
|
|||||||
status: string;
|
status: string;
|
||||||
containerId: string | null;
|
containerId: string | null;
|
||||||
port: number | null;
|
port: number | null;
|
||||||
|
healthStatus: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const serverColumns: Column<ServerRow>[] = [
|
const serverColumns: Column<ServerRow>[] = [
|
||||||
@@ -78,6 +79,7 @@ const templateColumns: Column<TemplateRow>[] = [
|
|||||||
|
|
||||||
const instanceColumns: Column<InstanceRow>[] = [
|
const instanceColumns: Column<InstanceRow>[] = [
|
||||||
{ header: 'STATUS', key: 'status', width: 10 },
|
{ header: 'STATUS', key: 'status', width: 10 },
|
||||||
|
{ header: 'HEALTH', key: (r) => r.healthStatus ?? '-', width: 10 },
|
||||||
{ header: 'SERVER ID', key: 'serverId' },
|
{ header: 'SERVER ID', key: 'serverId' },
|
||||||
{ header: 'PORT', key: (r) => r.port != null ? String(r.port) : '-', width: 6 },
|
{ header: 'PORT', key: (r) => r.port != null ? String(r.port) : '-', width: 6 },
|
||||||
{ header: 'CONTAINER', key: (r) => r.containerId ? r.containerId.slice(0, 12) : '-', width: 14 },
|
{ header: 'CONTAINER', key: (r) => r.containerId ? r.containerId.slice(0, 12) : '-', width: 14 },
|
||||||
|
|||||||
@@ -62,6 +62,7 @@ model McpServer {
|
|||||||
containerPort Int?
|
containerPort Int?
|
||||||
replicas Int @default(1)
|
replicas Int @default(1)
|
||||||
env Json @default("[]")
|
env Json @default("[]")
|
||||||
|
healthCheck Json?
|
||||||
version Int @default(1)
|
version Int @default(1)
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
@@ -96,6 +97,7 @@ model McpTemplate {
|
|||||||
containerPort Int?
|
containerPort Int?
|
||||||
replicas Int @default(1)
|
replicas Int @default(1)
|
||||||
env Json @default("[]")
|
env Json @default("[]")
|
||||||
|
healthCheck Json?
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
|
|
||||||
@@ -141,6 +143,9 @@ model McpInstance {
|
|||||||
status InstanceStatus @default(STOPPED)
|
status InstanceStatus @default(STOPPED)
|
||||||
port Int?
|
port Int?
|
||||||
metadata Json @default("{}")
|
metadata Json @default("{}")
|
||||||
|
healthStatus String?
|
||||||
|
lastHealthCheck DateTime?
|
||||||
|
events Json @default("[]")
|
||||||
version Int @default(1)
|
version Int @default(1)
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
|
|||||||
@@ -15,4 +15,4 @@ export type {
|
|||||||
} from '@prisma/client';
|
} from '@prisma/client';
|
||||||
|
|
||||||
export { seedTemplates } from './seed/index.js';
|
export { seedTemplates } from './seed/index.js';
|
||||||
export type { SeedTemplate, TemplateEnvEntry } from './seed/index.js';
|
export type { SeedTemplate, TemplateEnvEntry, HealthCheckSpec } from './seed/index.js';
|
||||||
|
|||||||
@@ -7,6 +7,14 @@ export interface TemplateEnvEntry {
|
|||||||
defaultValue?: string;
|
defaultValue?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface HealthCheckSpec {
|
||||||
|
tool: string;
|
||||||
|
arguments?: Record<string, unknown>;
|
||||||
|
intervalSeconds?: number;
|
||||||
|
timeoutSeconds?: number;
|
||||||
|
failureThreshold?: number;
|
||||||
|
}
|
||||||
|
|
||||||
export interface SeedTemplate {
|
export interface SeedTemplate {
|
||||||
name: string;
|
name: string;
|
||||||
version: string;
|
version: string;
|
||||||
@@ -20,6 +28,7 @@ export interface SeedTemplate {
|
|||||||
containerPort?: number;
|
containerPort?: number;
|
||||||
replicas?: number;
|
replicas?: number;
|
||||||
env?: TemplateEnvEntry[];
|
env?: TemplateEnvEntry[];
|
||||||
|
healthCheck?: HealthCheckSpec;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function seedTemplates(
|
export async function seedTemplates(
|
||||||
@@ -43,6 +52,7 @@ export async function seedTemplates(
|
|||||||
containerPort: tpl.containerPort ?? null,
|
containerPort: tpl.containerPort ?? null,
|
||||||
replicas: tpl.replicas ?? 1,
|
replicas: tpl.replicas ?? 1,
|
||||||
env: (tpl.env ?? []) as unknown as Prisma.InputJsonValue,
|
env: (tpl.env ?? []) as unknown as Prisma.InputJsonValue,
|
||||||
|
healthCheck: (tpl.healthCheck ?? Prisma.JsonNull) as unknown as Prisma.InputJsonValue,
|
||||||
},
|
},
|
||||||
create: {
|
create: {
|
||||||
name: tpl.name,
|
name: tpl.name,
|
||||||
@@ -57,6 +67,7 @@ export async function seedTemplates(
|
|||||||
containerPort: tpl.containerPort ?? null,
|
containerPort: tpl.containerPort ?? null,
|
||||||
replicas: tpl.replicas ?? 1,
|
replicas: tpl.replicas ?? 1,
|
||||||
env: (tpl.env ?? []) as unknown as Prisma.InputJsonValue,
|
env: (tpl.env ?? []) as unknown as Prisma.InputJsonValue,
|
||||||
|
healthCheck: (tpl.healthCheck ?? Prisma.JsonNull) as unknown as Prisma.InputJsonValue,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
upserted++;
|
upserted++;
|
||||||
|
|||||||
@@ -69,6 +69,7 @@ async function main(): Promise<void> {
|
|||||||
transport: parsed.transport ?? 'STDIO',
|
transport: parsed.transport ?? 'STDIO',
|
||||||
version: parsed.version ?? '1.0.0',
|
version: parsed.version ?? '1.0.0',
|
||||||
description: parsed.description ?? '',
|
description: parsed.description ?? '',
|
||||||
|
...(parsed.healthCheck ? { healthCheck: parsed.healthCheck } : {}),
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
await seedTemplates(prisma, templates);
|
await seedTemplates(prisma, templates);
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ export interface IMcpInstanceRepository {
|
|||||||
findById(id: string): Promise<McpInstance | null>;
|
findById(id: string): Promise<McpInstance | null>;
|
||||||
findByContainerId(containerId: 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>;
|
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>;
|
delete(id: string): Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ export class McpInstanceRepository implements IMcpInstanceRepository {
|
|||||||
async updateStatus(
|
async updateStatus(
|
||||||
id: string,
|
id: string,
|
||||||
status: InstanceStatus,
|
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> {
|
): Promise<McpInstance> {
|
||||||
const updateData: Prisma.McpInstanceUpdateInput = {
|
const updateData: Prisma.McpInstanceUpdateInput = {
|
||||||
status,
|
status,
|
||||||
@@ -59,6 +59,15 @@ export class McpInstanceRepository implements IMcpInstanceRepository {
|
|||||||
if (fields?.metadata !== undefined) {
|
if (fields?.metadata !== undefined) {
|
||||||
updateData.metadata = fields.metadata as Prisma.InputJsonValue;
|
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({
|
return this.prisma.mcpInstance.update({
|
||||||
where: { id },
|
where: { id },
|
||||||
data: updateData,
|
data: updateData,
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ export class McpServerRepository implements IMcpServerRepository {
|
|||||||
containerPort: data.containerPort ?? null,
|
containerPort: data.containerPort ?? null,
|
||||||
replicas: data.replicas,
|
replicas: data.replicas,
|
||||||
env: data.env,
|
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.containerPort !== undefined) updateData['containerPort'] = data.containerPort;
|
||||||
if (data.replicas !== undefined) updateData['replicas'] = data.replicas;
|
if (data.replicas !== undefined) updateData['replicas'] = data.replicas;
|
||||||
if (data.env !== undefined) updateData['env'] = data.env;
|
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 });
|
return this.prisma.mcpServer.update({ where: { id }, data: updateData });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -50,6 +50,7 @@ export class TemplateRepository implements ITemplateRepository {
|
|||||||
containerPort: data.containerPort ?? null,
|
containerPort: data.containerPort ?? null,
|
||||||
replicas: data.replicas,
|
replicas: data.replicas,
|
||||||
env: (data.env ?? []) as unknown as Prisma.InputJsonValue,
|
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.containerPort !== undefined) updateData.containerPort = data.containerPort;
|
||||||
if (data.replicas !== undefined) updateData.replicas = data.replicas;
|
if (data.replicas !== undefined) updateData.replicas = data.replicas;
|
||||||
if (data.env !== undefined) updateData.env = (data.env ?? []) as Prisma.InputJsonValue;
|
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({
|
return this.prisma.mcpTemplate.update({
|
||||||
where: { id },
|
where: { id },
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ function loadTemplatesFromDir(dir: string): SeedTemplate[] {
|
|||||||
transport: parsed.transport ?? 'STDIO',
|
transport: parsed.transport ?? 'STDIO',
|
||||||
version: parsed.version ?? '1.0.0',
|
version: parsed.version ?? '1.0.0',
|
||||||
description: parsed.description ?? '',
|
description: parsed.description ?? '',
|
||||||
|
...(parsed.healthCheck ? { healthCheck: parsed.healthCheck } : {}),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
import { HealthCheckSchema } from './template.schema.js';
|
||||||
|
|
||||||
const SecretRefSchema = z.object({
|
const SecretRefSchema = z.object({
|
||||||
name: z.string().min(1),
|
name: z.string().min(1),
|
||||||
@@ -30,6 +31,7 @@ export const CreateMcpServerSchema = z.object({
|
|||||||
containerPort: z.number().int().min(1).max(65535).optional(),
|
containerPort: z.number().int().min(1).max(65535).optional(),
|
||||||
replicas: z.number().int().min(0).max(10).default(1),
|
replicas: z.number().int().min(0).max(10).default(1),
|
||||||
env: z.array(ServerEnvEntrySchema).default([]),
|
env: z.array(ServerEnvEntrySchema).default([]),
|
||||||
|
healthCheck: HealthCheckSchema.optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const UpdateMcpServerSchema = z.object({
|
export const UpdateMcpServerSchema = z.object({
|
||||||
@@ -43,6 +45,7 @@ export const UpdateMcpServerSchema = z.object({
|
|||||||
containerPort: z.number().int().min(1).max(65535).nullable().optional(),
|
containerPort: z.number().int().min(1).max(65535).nullable().optional(),
|
||||||
replicas: z.number().int().min(0).max(10).optional(),
|
replicas: z.number().int().min(0).max(10).optional(),
|
||||||
env: z.array(ServerEnvEntrySchema).optional(),
|
env: z.array(ServerEnvEntrySchema).optional(),
|
||||||
|
healthCheck: HealthCheckSchema.nullable().optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export type CreateMcpServerInput = z.infer<typeof CreateMcpServerSchema>;
|
export type CreateMcpServerInput = z.infer<typeof CreateMcpServerSchema>;
|
||||||
|
|||||||
@@ -7,6 +7,16 @@ const TemplateEnvEntrySchema = z.object({
|
|||||||
defaultValue: z.string().optional(),
|
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({
|
export const CreateTemplateSchema = z.object({
|
||||||
name: z.string().min(1).max(100).regex(/^[a-z0-9-]+$/, 'Name must be lowercase alphanumeric with hyphens'),
|
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'),
|
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(),
|
containerPort: z.number().int().min(1).max(65535).optional(),
|
||||||
replicas: z.number().int().min(0).max(10).default(1),
|
replicas: z.number().int().min(0).max(10).default(1),
|
||||||
env: z.array(TemplateEnvEntrySchema).default([]),
|
env: z.array(TemplateEnvEntrySchema).default([]),
|
||||||
|
healthCheck: HealthCheckSchema.optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const UpdateTemplateSchema = CreateTemplateSchema.partial().omit({ name: true });
|
export const UpdateTemplateSchema = CreateTemplateSchema.partial().omit({ name: true });
|
||||||
|
|||||||
@@ -4,6 +4,10 @@ description: GitHub MCP server for repos, issues, PRs, and code search
|
|||||||
packageName: "@anthropic/github-mcp"
|
packageName: "@anthropic/github-mcp"
|
||||||
transport: STDIO
|
transport: STDIO
|
||||||
repositoryUrl: https://github.com/modelcontextprotocol/servers/tree/main/src/github
|
repositoryUrl: https://github.com/modelcontextprotocol/servers/tree/main/src/github
|
||||||
|
healthCheck:
|
||||||
|
tool: search_repositories
|
||||||
|
arguments:
|
||||||
|
query: "test"
|
||||||
env:
|
env:
|
||||||
- name: GITHUB_TOKEN
|
- name: GITHUB_TOKEN
|
||||||
description: Personal access token with repo scope
|
description: Personal access token with repo scope
|
||||||
|
|||||||
16
templates/grafana.yaml
Normal file
16
templates/grafana.yaml
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
name: grafana
|
||||||
|
version: "1.0.0"
|
||||||
|
description: Grafana MCP server for dashboards, datasources, and alerts
|
||||||
|
packageName: "@leval/mcp-grafana"
|
||||||
|
transport: STDIO
|
||||||
|
repositoryUrl: https://github.com/levalhq/mcp-grafana
|
||||||
|
healthCheck:
|
||||||
|
tool: list_datasources
|
||||||
|
arguments: {}
|
||||||
|
env:
|
||||||
|
- name: GRAFANA_URL
|
||||||
|
description: Grafana instance URL (e.g. https://grafana.example.com)
|
||||||
|
required: true
|
||||||
|
- name: GRAFANA_SERVICE_ACCOUNT_TOKEN
|
||||||
|
description: Grafana service account token (glsa_...)
|
||||||
|
required: true
|
||||||
16
templates/home-assistant.yaml
Normal file
16
templates/home-assistant.yaml
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
name: home-assistant
|
||||||
|
version: "1.0.0"
|
||||||
|
description: Home Assistant MCP server for smart home control and entity management
|
||||||
|
packageName: "home-assistant-mcp-server"
|
||||||
|
transport: STDIO
|
||||||
|
repositoryUrl: https://github.com/tevonsb/homeassistant-mcp
|
||||||
|
healthCheck:
|
||||||
|
tool: get_entities
|
||||||
|
arguments: {}
|
||||||
|
env:
|
||||||
|
- name: HASS_URL
|
||||||
|
description: Home Assistant instance URL (e.g. http://homeassistant.local:8123)
|
||||||
|
required: true
|
||||||
|
- name: HASS_TOKEN
|
||||||
|
description: Home Assistant long-lived access token
|
||||||
|
required: true
|
||||||
@@ -4,6 +4,11 @@ description: Jira MCP server for issues, projects, and boards
|
|||||||
packageName: "@anthropic/jira-mcp"
|
packageName: "@anthropic/jira-mcp"
|
||||||
transport: STDIO
|
transport: STDIO
|
||||||
repositoryUrl: https://github.com/modelcontextprotocol/servers/tree/main/src/jira
|
repositoryUrl: https://github.com/modelcontextprotocol/servers/tree/main/src/jira
|
||||||
|
healthCheck:
|
||||||
|
tool: search_issues
|
||||||
|
arguments:
|
||||||
|
jql: "created >= -1d"
|
||||||
|
maxResults: 1
|
||||||
env:
|
env:
|
||||||
- name: JIRA_URL
|
- name: JIRA_URL
|
||||||
description: Jira instance URL (e.g. https://company.atlassian.net)
|
description: Jira instance URL (e.g. https://company.atlassian.net)
|
||||||
|
|||||||
16
templates/node-red.yaml
Normal file
16
templates/node-red.yaml
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
name: node-red
|
||||||
|
version: "1.0.0"
|
||||||
|
description: Node-RED MCP server for flow management and automation
|
||||||
|
packageName: "mcp-node-red"
|
||||||
|
transport: STDIO
|
||||||
|
repositoryUrl: https://github.com/fx/mcp-node-red
|
||||||
|
healthCheck:
|
||||||
|
tool: get_settings
|
||||||
|
arguments: {}
|
||||||
|
env:
|
||||||
|
- name: NODE_RED_URL
|
||||||
|
description: Node-RED instance URL (e.g. http://nodered.local:1880)
|
||||||
|
required: true
|
||||||
|
- name: NODE_RED_TOKEN
|
||||||
|
description: Node-RED access token (optional if no auth)
|
||||||
|
required: false
|
||||||
@@ -4,6 +4,10 @@ description: PostgreSQL MCP server for database queries and schema inspection
|
|||||||
packageName: "@anthropic/postgres-mcp"
|
packageName: "@anthropic/postgres-mcp"
|
||||||
transport: STDIO
|
transport: STDIO
|
||||||
repositoryUrl: https://github.com/modelcontextprotocol/servers/tree/main/src/postgres
|
repositoryUrl: https://github.com/modelcontextprotocol/servers/tree/main/src/postgres
|
||||||
|
healthCheck:
|
||||||
|
tool: query
|
||||||
|
arguments:
|
||||||
|
sql: "SELECT 1"
|
||||||
env:
|
env:
|
||||||
- name: POSTGRES_CONNECTION_STRING
|
- name: POSTGRES_CONNECTION_STRING
|
||||||
description: PostgreSQL connection string (e.g. postgresql://user:pass@host:5432/db)
|
description: PostgreSQL connection string (e.g. postgresql://user:pass@host:5432/db)
|
||||||
|
|||||||
@@ -4,6 +4,9 @@ description: Slack MCP server for reading channels, messages, and user info
|
|||||||
packageName: "@anthropic/slack-mcp"
|
packageName: "@anthropic/slack-mcp"
|
||||||
transport: STDIO
|
transport: STDIO
|
||||||
repositoryUrl: https://github.com/modelcontextprotocol/servers/tree/main/src/slack
|
repositoryUrl: https://github.com/modelcontextprotocol/servers/tree/main/src/slack
|
||||||
|
healthCheck:
|
||||||
|
tool: list_channels
|
||||||
|
arguments: {}
|
||||||
env:
|
env:
|
||||||
- name: SLACK_BOT_TOKEN
|
- name: SLACK_BOT_TOKEN
|
||||||
description: Slack bot token (xoxb-...)
|
description: Slack bot token (xoxb-...)
|
||||||
|
|||||||
Reference in New Issue
Block a user