Skip to main content
Glama

homelab

Manage Docker containers, images, and compose projects across multiple homelab hosts. Monitor resources, retrieve logs, and execute remote file operations via SSH from a unified interface.

Instructions

Unified homelab Docker management tool.

ACTIONS: container - Container operations list - List containers with filters start/stop/restart - Control container state pause/unpause - Pause/unpause container logs - Get container logs stats - Get resource usage stats inspect - Get detailed container info search - Search containers by query pull - Pull latest image for container recreate - Recreate container with latest image

compose - Docker Compose operations list - List compose projects status - Get project status up/down/restart - Control project state logs - Get project logs build - Build project images pull - Pull project images recreate - Force recreate containers

host - Host operations status - Check host connectivity resources - Get CPU/memory/disk via SSH

docker - Docker daemon operations (host parameter required) info - Get Docker system info df - Get disk usage prune - Remove unused resources

image - Image operations list - List images pull - Pull an image build - Build from Dockerfile remove - Remove an image

scout - Remote file operations via SSH read - Read file content list - List directory contents tree - Show directory tree exec - Execute command find - Find files by pattern transfer - Transfer file between hosts diff - Diff files across hosts

EXAMPLES: { action: "container", subaction: "list", state: "running" } { action: "container", subaction: "restart", container_id: "plex" } { action: "compose", subaction: "up", host: "tootie", project: "plex" } { action: "host", subaction: "resources", host: "tootie" } { action: "docker", subaction: "info", host: "tootie" } { action: "docker", subaction: "df", host: "tootie" } { action: "docker", subaction: "prune", host: "tootie", prune_target: "images", force: true } { action: "image", subaction: "pull", host: "tootie", image: "nginx:latest" } { action: "scout", subaction: "read", host: "tootie", path: "/etc/hosts" } { action: "scout", subaction: "list", host: "tootie", path: "/var/log" } { action: "scout", subaction: "exec", host: "tootie", path: "/tmp", command: "ls -la" }

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault

No arguments

Implementation Reference

  • MCP tool registration for 'homelab' tool, including schema reference, annotations, and handler function that validates input and routes to action handlers.
    server.registerTool(
      "homelab",
      {
        title: "Homelab Manager",
        description: TOOL_DESCRIPTION,
        inputSchema: UnifiedHomelabSchema,
        annotations: {
          readOnlyHint: false,
          destructiveHint: false,
          idempotentHint: false,
          openWorldHint: true
        }
      },
      async (params: unknown) => {
        try {
          // Validate and parse input with Zod
          const validated = UnifiedHomelabSchema.parse(params);
          return await routeAction(validated, hosts, container);
        } catch (error) {
          return {
            isError: true,
            content: [
              {
                type: "text" as const,
                text: `Error: ${error instanceof Error ? error.message : "Unknown error"}`
              }
            ]
          };
        }
      }
    );
  • Zod schema definition for the homelab tool using discriminated union on 'action_subaction' for efficient validation across all container, compose, host, docker, image, and scout operations.
    // ===== Unified schema using z.discriminatedUnion for O(1) lookup =====
    // Uses action_subaction composite key as discriminator for constant-time schema lookup
    // instead of O(n) sequential validation with z.union()
    const UnifiedHomelabUnion = z.discriminatedUnion("action_subaction", [
      // Container actions (12 schemas)
      containerListSchema,
      containerStartSchema,
      containerStopSchema,
      containerRestartSchema,
      containerPauseSchema,
      containerUnpauseSchema,
      containerLogsSchema,
      containerStatsSchema,
      containerInspectSchema,
      containerSearchSchema,
      containerPullSchema,
      containerRecreateSchema,
      // Compose actions (9 schemas)
      composeListSchema,
      composeStatusSchema,
      composeUpSchema,
      composeDownSchema,
      composeRestartSchema,
      composeLogsSchema,
      composeBuildSchema,
      composeRecreateSchema,
      composePullSchema,
      // Host actions (2 schemas)
      hostStatusSchema,
      hostResourcesSchema,
      // Docker actions (3 schemas)
      dockerInfoSchema,
      dockerDfSchema,
      dockerPruneSchema,
      // Image actions (4 schemas)
      imageListSchema,
      imagePullSchema,
      imageBuildSchema,
      imageRemoveSchema,
      // Scout actions (7 schemas)
      scoutReadSchema,
      scoutListSchema,
      scoutTreeSchema,
      scoutExecSchema,
      scoutFindSchema,
      scoutTransferSchema,
      scoutDiffSchema
    ]);
    
    // Export with preprocess wrapper to automatically inject discriminator
    export const UnifiedHomelabSchema = z.preprocess(preprocessWithDiscriminator, UnifiedHomelabUnion);
    
    export type UnifiedHomelabInput = z.infer<typeof UnifiedHomelabSchema>;
  • Central router function that dispatches validated input to specific action handlers (container, compose, host, docker, image, scout).
    async function routeAction(
      params: UnifiedHomelabInput,
      hosts: HostConfig[],
      container: ServiceContainer
    ): Promise<{
      isError?: boolean;
      content: Array<{ type: "text"; text: string }>;
      structuredContent?: Record<string, unknown>;
    }> {
      const { action } = params;
    
      switch (action) {
        case "container":
          return handleContainerAction(params, hosts, container);
        case "compose":
          return handleComposeAction(params, hosts, container);
        case "host":
          return handleHostAction(params, hosts, container);
        case "docker":
          return handleDockerAction(params, hosts, container);
        case "image":
          return handleImageAction(params, hosts, container);
        case "scout":
          return handleScoutAction(params, hosts, container);
        default:
          throw new Error(`Unknown action: ${action}`);
      }
    }
  • Handler for container operations: list, start/stop/restart/pause/unpause, logs, stats, inspect, search, pull image, recreate. Supports multi-host, pagination, filtering, and parallel stats collection.
    async function handleContainerAction(
      params: UnifiedHomelabInput,
      hosts: HostConfig[],
      container: ServiceContainer
    ): Promise<{
      isError?: boolean;
      content: Array<{ type: "text"; text: string }>;
      structuredContent?: Record<string, unknown>;
    }> {
      if (params.action !== "container") throw new Error("Invalid action");
      const { subaction } = params;
      const dockerService = container.getDockerService();
    
      switch (subaction) {
        case "list": {
          const targetHosts = params.host ? hosts.filter((h) => h.name === params.host) : hosts;
          if (params.host && targetHosts.length === 0) {
            return errorResponse(
              `Host '${params.host}' not found. Available: ${hosts.map((h) => h.name).join(", ")}`
            );
          }
    
          const containers = await dockerService.listContainers(targetHosts, {
            state: params.state,
            nameFilter: params.name_filter,
            imageFilter: params.image_filter,
            labelFilter: params.label_filter
          });
    
          const total = containers.length;
          const paginated = containers.slice(params.offset, params.offset + params.limit);
          const hasMore = total > params.offset + params.limit;
    
          const output = {
            total,
            count: paginated.length,
            offset: params.offset,
            containers: paginated,
            has_more: hasMore
          };
          const text =
            params.response_format === ResponseFormat.JSON
              ? JSON.stringify(output, null, 2)
              : formatContainersMarkdown(paginated, total, params.offset, hasMore);
    
          return successResponse(text, output);
        }
    
        case "start":
        case "stop":
        case "restart":
        case "pause":
        case "unpause": {
          const targetHost = await resolveContainerHost(params.container_id, params.host, hosts, dockerService);
          if (!targetHost) {
            return errorResponse(`Container '${params.container_id}' not found.`);
          }
    
          await dockerService.containerAction(params.container_id, subaction, targetHost);
          return successResponse(
            `✓ Successfully performed '${subaction}' on container '${params.container_id}' (host: ${targetHost.name})`
          );
        }
    
        case "logs": {
          const targetHost = await resolveContainerHost(params.container_id, params.host, hosts, dockerService);
          if (!targetHost) {
            return errorResponse(`Container '${params.container_id}' not found.`);
          }
    
          let logs = await dockerService.getContainerLogs(params.container_id, targetHost, {
            lines: params.lines,
            since: params.since,
            until: params.until,
            stream: params.stream
          });
    
          if (params.grep) {
            const grepLower = params.grep.toLowerCase();
            logs = logs.filter((l) => l.message.toLowerCase().includes(grepLower));
          }
    
          const output = {
            container: params.container_id,
            host: targetHost.name,
            count: logs.length,
            logs
          };
          const text =
            params.response_format === ResponseFormat.JSON
              ? JSON.stringify(output, null, 2)
              : formatLogsMarkdown(logs, params.container_id, targetHost.name);
    
          return successResponse(text, output);
        }
    
        case "stats": {
          if (params.container_id) {
            const targetHost = await resolveContainerHost(params.container_id, params.host, hosts, dockerService);
            if (!targetHost) {
              return errorResponse(`Container '${params.container_id}' not found.`);
            }
    
            const stats = await dockerService.getContainerStats(params.container_id, targetHost);
            const output = { ...stats, host: targetHost.name };
            const text =
              params.response_format === ResponseFormat.JSON
                ? JSON.stringify(output, null, 2)
                : formatStatsMarkdown([stats], targetHost.name);
    
            return successResponse(text, output);
          } else {
            const targetHosts = params.host ? hosts.filter((h) => h.name === params.host) : hosts;
    
            // Collect stats in parallel across all hosts and containers
            const allStats = await collectStatsParallel(targetHosts, dockerService, 20);
    
            const output = { stats: allStats.map((s) => ({ ...s.stats, host: s.host })) };
            const text =
              params.response_format === ResponseFormat.JSON
                ? JSON.stringify(output, null, 2)
                : formatMultiStatsMarkdown(allStats);
    
            return successResponse(text, output);
          }
        }
    
        case "inspect": {
          const targetHost = await resolveContainerHost(params.container_id, params.host, hosts, dockerService);
          if (!targetHost) {
            return errorResponse(`Container '${params.container_id}' not found.`);
          }
    
          const info = await dockerService.inspectContainer(params.container_id, targetHost);
    
          // Summary mode returns condensed output to save tokens
          if (params.summary) {
            const summary = {
              id: info.Id?.slice(0, 12),
              name: info.Name?.replace(/^\//, ""),
              image: info.Config?.Image,
              state: info.State?.Status,
              created: info.Created,
              started: info.State?.StartedAt,
              restartCount: info.RestartCount,
              ports: Object.keys(info.NetworkSettings?.Ports || {}).filter(
                (p) => info.NetworkSettings?.Ports?.[p]
              ),
              mounts: (info.Mounts || []).map(
                (m: { Source?: string; Destination?: string; Type?: string }) => ({
                  src: m.Source,
                  dst: m.Destination,
                  type: m.Type
                })
              ),
              networks: Object.keys(info.NetworkSettings?.Networks || {}),
              env_count: (info.Config?.Env || []).length,
              labels_count: Object.keys(info.Config?.Labels || {}).length,
              host: targetHost.name
            };
            const text =
              params.response_format === ResponseFormat.JSON
                ? JSON.stringify(summary, null, 2)
                : formatInspectSummaryMarkdown(summary);
    
            return successResponse(text, summary);
          }
    
          // Full mode returns complete inspect output
          const output = { ...info, _host: targetHost.name };
          const text =
            params.response_format === ResponseFormat.JSON
              ? JSON.stringify(output, null, 2)
              : formatInspectMarkdown(info, targetHost.name);
    
          return successResponse(text, output);
        }
    
        case "search": {
          const targetHosts = params.host ? hosts.filter((h) => h.name === params.host) : hosts;
          const allContainers = await dockerService.listContainers(targetHosts, {});
          const query = params.query.toLowerCase();
    
          const matches = allContainers.filter((c) => {
            const searchText = [c.name, c.image, ...Object.keys(c.labels), ...Object.values(c.labels)]
              .join(" ")
              .toLowerCase();
            return searchText.includes(query);
          });
    
          const total = matches.length;
          const paginated = matches.slice(params.offset, params.offset + params.limit);
          const hasMore = total > params.offset + params.limit;
    
          const output = {
            query: params.query,
            total,
            count: paginated.length,
            containers: paginated,
            has_more: hasMore
          };
          const text =
            params.response_format === ResponseFormat.JSON
              ? JSON.stringify(output, null, 2)
              : formatSearchResultsMarkdown(paginated, params.query, total);
    
          return successResponse(text, output);
        }
    
        case "pull": {
          const targetHost = await resolveContainerHost(params.container_id, params.host, hosts, dockerService);
          if (!targetHost) {
            return errorResponse(`Container '${params.container_id}' not found.`);
          }
    
          const info = await dockerService.inspectContainer(params.container_id, targetHost);
          const imageName = info.Config.Image;
          await dockerService.pullImage(imageName, targetHost);
    
          return successResponse(
            `✓ Successfully pulled latest image '${imageName}' for container '${params.container_id}'`
          );
        }
    
        case "recreate": {
          const targetHost = await resolveContainerHost(params.container_id, params.host, hosts, dockerService);
          if (!targetHost) {
            return errorResponse(`Container '${params.container_id}' not found.`);
          }
    
          const result = await dockerService.recreateContainer(params.container_id, targetHost, {
            pull: params.pull
          });
          return successResponse(
            `✓ ${result.status}. New container ID: ${result.containerId.slice(0, 12)}`
          );
        }
    
        default:
          throw new Error(`Unknown container subaction: ${subaction}`);
      }
    }
  • Top-level tool registration entrypoint that invokes unified homelab tool registration.
    export function registerTools(server: McpServer, container?: ServiceContainer): void {
      if (!container) {
        throw new Error("ServiceContainer is required for tool registration");
      }
      registerUnifiedTool(server, container);
    }
Install Server

Other Tools

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/jmagar/homelab-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server