import { ContainerManager } from '../managers/container.js';
import { z } from 'zod';
export function getContainerTools() {
return [
{
name: 'docker_list_containers',
description: 'List Docker containers. Returns all containers including stopped ones if all=true.',
inputSchema: {
type: 'object',
properties: {
all: {
type: 'boolean',
description: 'Show all containers (default shows just running)',
},
filters: {
type: 'object',
description: 'Filter containers by status, name, etc.',
additionalProperties: {
type: 'array',
items: { type: 'string' },
},
},
},
},
},
{
name: 'docker_create_container',
description: 'Create a new Docker container from an image',
inputSchema: {
type: 'object',
properties: {
image: {
type: 'string',
description: 'Image name to use for the container',
},
name: {
type: 'string',
description: 'Name for the container',
},
cmd: {
type: 'array',
items: { type: 'string' },
description: 'Command to run in the container',
},
env: {
type: 'array',
items: { type: 'string' },
description: 'Environment variables (e.g., ["KEY=value"])',
},
ports: {
type: 'object',
description: 'Port mappings (e.g., {"80/tcp": {}})',
additionalProperties: {},
},
labels: {
type: 'object',
description: 'Labels to apply to the container',
additionalProperties: { type: 'string' },
},
workingDir: {
type: 'string',
description: 'Working directory inside the container',
},
user: {
type: 'string',
description: 'User to run as (e.g., "root", "1000:1000")',
},
tty: {
type: 'boolean',
description: 'Allocate a pseudo-TTY',
},
openStdin: {
type: 'boolean',
description: 'Keep STDIN open even if not attached',
},
},
required: ['image'],
},
},
{
name: 'docker_start_container',
description: 'Start a stopped container',
inputSchema: {
type: 'object',
properties: {
id: {
type: 'string',
description: 'Container ID or name',
},
},
required: ['id'],
},
},
{
name: 'docker_stop_container',
description: 'Stop a running container',
inputSchema: {
type: 'object',
properties: {
id: {
type: 'string',
description: 'Container ID or name',
},
timeout: {
type: 'number',
description: 'Timeout in seconds before killing the container',
},
},
required: ['id'],
},
},
{
name: 'docker_restart_container',
description: 'Restart a container',
inputSchema: {
type: 'object',
properties: {
id: {
type: 'string',
description: 'Container ID or name',
},
timeout: {
type: 'number',
description: 'Timeout in seconds before killing the container',
},
},
required: ['id'],
},
},
{
name: 'docker_kill_container',
description: 'Kill a running container. Requires confirmation for safety. First call without confirm to see container details, then call again with confirm=true to proceed.',
inputSchema: {
type: 'object',
properties: {
id: {
type: 'string',
description: 'Container ID or name',
},
signal: {
type: 'string',
description: 'Signal to send (e.g., "SIGKILL", "SIGTERM")',
},
confirm: {
type: 'boolean',
description: 'Confirmation flag. Must be true to actually kill the container. First call without this to see container details.',
},
},
required: ['id'],
},
},
{
name: 'docker_remove_container',
description: 'Remove a container. Requires confirmation for safety. First call without confirm to see container details, then call again with confirm=true to proceed.',
inputSchema: {
type: 'object',
properties: {
id: {
type: 'string',
description: 'Container ID or name',
},
force: {
type: 'boolean',
description: 'Force removal of running container',
},
volumes: {
type: 'boolean',
description: 'Remove associated volumes',
},
confirm: {
type: 'boolean',
description: 'Confirmation flag. Must be true to actually remove the container. First call without this to see container details.',
},
},
required: ['id'],
},
},
{
name: 'docker_inspect_container',
description: 'Get detailed information about a container',
inputSchema: {
type: 'object',
properties: {
id: {
type: 'string',
description: 'Container ID or name',
},
},
required: ['id'],
},
},
{
name: 'docker_container_logs',
description: 'Get logs from a container',
inputSchema: {
type: 'object',
properties: {
id: {
type: 'string',
description: 'Container ID or name',
},
follow: {
type: 'boolean',
description: 'Follow log output',
},
tail: {
type: 'number',
description: 'Number of lines to show from the end',
},
since: {
type: 'string',
description: 'Show logs since timestamp or relative time (e.g., "2023-01-01T00:00:00" or "10m")',
},
timestamps: {
type: 'boolean',
description: 'Show timestamps',
},
},
required: ['id'],
},
},
{
name: 'docker_container_stats',
description: 'Get real-time statistics from a container',
inputSchema: {
type: 'object',
properties: {
id: {
type: 'string',
description: 'Container ID or name',
},
stream: {
type: 'boolean',
description: 'Stream stats continuously',
},
},
required: ['id'],
},
},
{
name: 'docker_pause_container',
description: 'Pause a running container',
inputSchema: {
type: 'object',
properties: {
id: {
type: 'string',
description: 'Container ID or name',
},
},
required: ['id'],
},
},
{
name: 'docker_unpause_container',
description: 'Unpause a paused container',
inputSchema: {
type: 'object',
properties: {
id: {
type: 'string',
description: 'Container ID or name',
},
},
required: ['id'],
},
},
{
name: 'docker_container_health_check',
description: 'Check the health status of a container. Returns health check status (healthy, unhealthy, starting, none), health check configuration, failure streak, and health check log history.',
inputSchema: {
type: 'object',
properties: {
id: {
type: 'string',
description: 'Container ID or name',
},
},
required: ['id'],
},
},
];
}
export async function handleContainerTool(
name: string,
args: any,
containerManager: ContainerManager
): Promise<any> {
try {
switch (name) {
case 'docker_list_containers': {
const parsed = z
.object({
all: z.boolean().optional(),
filters: z.record(z.array(z.string())).optional(),
})
.parse(args);
const containers = await containerManager.listContainers({
all: parsed.all,
filters: parsed.filters,
});
return {
content: [
{
type: 'text',
text: JSON.stringify(containers, null, 2),
},
],
};
}
case 'docker_create_container': {
const parsed = z
.object({
image: z.string(),
name: z.string().optional(),
cmd: z.array(z.string()).optional(),
env: z.array(z.string()).optional(),
ports: z.record(z.string(), z.object({})).optional(),
labels: z.record(z.string()).optional(),
workingDir: z.string().optional(),
user: z.string().optional(),
tty: z.boolean().optional(),
openStdin: z.boolean().optional(),
})
.parse(args);
const container = await containerManager.createContainer({
Image: parsed.image,
name: parsed.name,
Cmd: parsed.cmd,
Env: parsed.env,
ExposedPorts: parsed.ports,
Labels: parsed.labels,
WorkingDir: parsed.workingDir,
User: parsed.user,
Tty: parsed.tty,
OpenStdin: parsed.openStdin,
});
return {
content: [
{
type: 'text',
text: JSON.stringify({ id: container.id, message: 'Container created successfully' }, null, 2),
},
],
};
}
case 'docker_start_container': {
const parsed = z.object({ id: z.string() }).parse(args);
await containerManager.startContainer(parsed.id);
return {
content: [
{
type: 'text',
text: JSON.stringify({ message: 'Container started successfully' }, null, 2),
},
],
};
}
case 'docker_stop_container': {
const parsed = z
.object({
id: z.string(),
timeout: z.number().optional(),
})
.parse(args);
await containerManager.stopContainer(parsed.id, parsed.timeout);
return {
content: [
{
type: 'text',
text: JSON.stringify({ message: 'Container stopped successfully' }, null, 2),
},
],
};
}
case 'docker_restart_container': {
const parsed = z
.object({
id: z.string(),
timeout: z.number().optional(),
})
.parse(args);
await containerManager.restartContainer(parsed.id, parsed.timeout);
return {
content: [
{
type: 'text',
text: JSON.stringify({ message: 'Container restarted successfully' }, null, 2),
},
],
};
}
case 'docker_kill_container': {
const parsed = z
.object({
id: z.string(),
signal: z.string().optional(),
confirm: z.boolean().optional(),
})
.parse(args);
// First call: Show container details and request confirmation
if (!parsed.confirm) {
try {
const containerInfo = await containerManager.inspectContainer(parsed.id);
return {
content: [
{
type: 'text',
text: JSON.stringify({
warning: '⚠️ CONFIRMATION REQUIRED: Killing a container is a destructive operation',
container: {
id: containerInfo.Id,
name: containerInfo.Name,
image: containerInfo.Config?.Image,
status: containerInfo.State?.Status,
running: containerInfo.State?.Running,
},
message: 'To proceed with killing the container, call this tool again with confirm=true',
example: {
name: 'docker_kill_container',
arguments: {
id: parsed.id,
signal: parsed.signal,
confirm: true,
},
},
}, null, 2),
},
],
};
} catch (error: any) {
return {
content: [
{
type: 'text',
text: JSON.stringify({
error: `Container not found: ${parsed.id}`,
message: error.message,
}, null, 2),
},
],
isError: true,
};
}
}
// Second call with confirm=true: Actually kill the container
if (parsed.confirm === true) {
await containerManager.killContainer(parsed.id, parsed.signal);
return {
content: [
{
type: 'text',
text: JSON.stringify({
message: '✅ Container killed successfully',
container: parsed.id,
}, null, 2),
},
],
};
}
return {
content: [
{
type: 'text',
text: JSON.stringify({
error: 'Invalid confirmation state',
message: 'Please set confirm=true to proceed with killing the container',
}, null, 2),
},
],
isError: true,
};
}
case 'docker_remove_container': {
const parsed = z
.object({
id: z.string(),
force: z.boolean().optional(),
volumes: z.boolean().optional(),
confirm: z.boolean().optional(),
})
.parse(args);
// First call: Show container details and request confirmation
if (!parsed.confirm) {
try {
const containerInfo = await containerManager.inspectContainer(parsed.id);
return {
content: [
{
type: 'text',
text: JSON.stringify({
warning: '⚠️ CONFIRMATION REQUIRED: Container removal is a destructive operation',
container: {
id: containerInfo.Id,
name: containerInfo.Name,
image: containerInfo.Config?.Image,
status: containerInfo.State?.Status,
running: containerInfo.State?.Running,
mounts: containerInfo.Mounts?.length || 0,
},
message: 'To proceed with removal, call this tool again with confirm=true',
example: {
name: 'docker_remove_container',
arguments: {
id: parsed.id,
force: parsed.force || false,
volumes: parsed.volumes || false,
confirm: true,
},
},
}, null, 2),
},
],
};
} catch (error: any) {
return {
content: [
{
type: 'text',
text: JSON.stringify({
error: `Container not found: ${parsed.id}`,
message: error.message,
}, null, 2),
},
],
isError: true,
};
}
}
// Second call with confirm=true: Actually remove the container
if (parsed.confirm === true) {
await containerManager.removeContainer(parsed.id, {
force: parsed.force,
volumes: parsed.volumes,
});
return {
content: [
{
type: 'text',
text: JSON.stringify({
message: '✅ Container removed successfully',
removed: parsed.id,
}, null, 2),
},
],
};
}
return {
content: [
{
type: 'text',
text: JSON.stringify({
error: 'Invalid confirmation state',
message: 'Please set confirm=true to proceed with container removal',
}, null, 2),
},
],
isError: true,
};
}
case 'docker_inspect_container': {
const parsed = z.object({ id: z.string() }).parse(args);
const info = await containerManager.inspectContainer(parsed.id);
return {
content: [
{
type: 'text',
text: JSON.stringify(info, null, 2),
},
],
};
}
case 'docker_container_logs': {
const parsed = z
.object({
id: z.string(),
follow: z.boolean().optional(),
tail: z.number().optional(),
since: z.union([z.number(), z.string()]).optional(),
timestamps: z.boolean().optional(),
})
.parse(args);
const logsStream = await containerManager.getContainerLogs(parsed.id, {
follow: parsed.follow,
tail: parsed.tail,
since: parsed.since,
timestamps: parsed.timestamps,
});
const chunks: Buffer[] = [];
for await (const chunk of logsStream) {
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
}
const logs = Buffer.concat(chunks).toString('utf-8');
return {
content: [
{
type: 'text',
text: logs,
},
],
};
}
case 'docker_container_stats': {
const parsed = z
.object({
id: z.string(),
stream: z.boolean().optional(),
})
.parse(args);
const statsStream = await containerManager.getContainerStats(parsed.id, parsed.stream ?? false);
const chunks: Buffer[] = [];
for await (const chunk of statsStream) {
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
}
const stats = Buffer.concat(chunks).toString('utf-8');
return {
content: [
{
type: 'text',
text: stats,
},
],
};
}
case 'docker_pause_container': {
const parsed = z.object({ id: z.string() }).parse(args);
await containerManager.pauseContainer(parsed.id);
return {
content: [
{
type: 'text',
text: JSON.stringify({ message: 'Container paused successfully' }, null, 2),
},
],
};
}
case 'docker_unpause_container': {
const parsed = z.object({ id: z.string() }).parse(args);
await containerManager.unpauseContainer(parsed.id);
return {
content: [
{
type: 'text',
text: JSON.stringify({ message: 'Container unpaused successfully' }, null, 2),
},
],
};
}
case 'docker_container_health_check': {
const parsed = z.object({ id: z.string() }).parse(args);
const healthInfo = await containerManager.getContainerHealthCheck(parsed.id);
let output = `Container Health Check Status: ${healthInfo.status}\n\n`;
if (!healthInfo.hasHealthCheck) {
output += healthInfo.message;
} else {
output += `Failing Streak: ${healthInfo.failingStreak}\n\n`;
if (healthInfo.healthCheckConfig) {
output += `Health Check Configuration:\n`;
output += ` Test: ${JSON.stringify(healthInfo.healthCheckConfig.Test || 'N/A')}\n`;
output += ` Interval: ${healthInfo.healthCheckConfig.Interval || 'N/A'}ns\n`;
output += ` Timeout: ${healthInfo.healthCheckConfig.Timeout || 'N/A'}ns\n`;
output += ` Start Period: ${healthInfo.healthCheckConfig.StartPeriod || 'N/A'}ns\n`;
output += ` Retries: ${healthInfo.healthCheckConfig.Retries || 'N/A'}\n\n`;
}
if (healthInfo.lastHealthCheck) {
output += `Last Health Check:\n`;
output += ` Start: ${healthInfo.lastHealthCheck.Start || 'N/A'}\n`;
output += ` End: ${healthInfo.lastHealthCheck.End || 'N/A'}\n`;
output += ` Exit Code: ${healthInfo.lastHealthCheck.ExitCode || 'N/A'}\n`;
output += ` Output: ${healthInfo.lastHealthCheck.Output || 'N/A'}\n\n`;
}
if (healthInfo.log && healthInfo.log.length > 0) {
output += `Health Check History (last ${Math.min(5, healthInfo.log.length)} entries):\n`;
const recentLogs = healthInfo.log.slice(-5).reverse();
recentLogs.forEach((log: any, index: number) => {
output += `\n [${index + 1}] ${log.Start || 'N/A'} - Exit Code: ${log.ExitCode || 'N/A'}\n`;
if (log.Output) {
output += ` Output: ${log.Output.substring(0, 100)}${log.Output.length > 100 ? '...' : ''}\n`;
}
});
}
}
return {
content: [
{
type: 'text',
text: output,
},
],
};
}
default:
return null; // Let other handlers try
}
} catch (error: any) {
if (error.message?.includes('Unknown tool')) {
return null;
}
throw error;
}
}