Skip to main content
Glama
YawLabs

SSH MCP Server

by YawLabs

ssh_service_status

Check the status of a systemd service on a remote SSH host. Returns active state, PID, uptime, and service description.

Instructions

Check the status of a systemd service on a remote host. Returns whether it's active, its PID, uptime, and description. Use this instead of ssh_exec with systemctl.

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
hostYesSSH hostname or IP address
portNoSSH port (default: 22)
usernameNoSSH username (default: current user)
privateKeyPathNoPath to SSH private key
passwordNoSSH password. STRONGLY prefer key-based auth (privateKeyPath or ssh-agent). Passwords pass through MCP protocol frames as plaintext and may be logged by the transport or host process.
serviceYesSystemd service name (e.g. nginx, sshd, docker)
timeoutNoCommand timeout in milliseconds (default: 30000)

Implementation Reference

  • MCP tool handler for 'ssh_service_status'. Calls serviceStatus() helper and formats the result (service name, status, description, PID, since, raw output). Sets isError if the service is not active.
    server.tool(
      "ssh_service_status",
      "Check the status of a systemd service on a remote host. Returns whether it's active, its PID, uptime, and description. Use this instead of ssh_exec with systemctl.",
      {
        ...connectionParams,
        service: z.string().describe("Systemd service name (e.g. nginx, sshd, docker)"),
        timeout: TimeoutSchema,
      },
      async ({ service, timeout, ...conn }) => {
        return connectionPool.withConnection(conn, async (client) => {
          const status = await serviceStatus(client, service, timeout || 30000);
          const lines: string[] = [];
          lines.push(`Service: ${status.name}`);
          lines.push(`Status: ${status.status}`);
          if (status.description) lines.push(`Description: ${status.description}`);
          if (status.pid) lines.push(`PID: ${status.pid}`);
          if (status.since) lines.push(`Since: ${status.since}`);
          lines.push("");
          lines.push(status.raw);
          return { content: [{ type: "text", text: lines.join("\n") }], isError: !status.active };
        });
      },
    );
  • Input schema for ssh_service_status: spreads connectionParams (host, port, username, privateKeyPath, password) plus 'service' string and optional TimeoutSchema.
    {
      ...connectionParams,
      service: z.string().describe("Systemd service name (e.g. nginx, sshd, docker)"),
      timeout: TimeoutSchema,
    },
  • src/tools.ts:34-421 (registration)
    All MCP tools are registered via registerTools() in src/tools.ts. The ssh_service_status tool is registered via server.tool("ssh_service_status", ...) on line 398-420.
    export function registerTools(server: McpServer, pool?: ConnectionPool) {
      const connectionPool = pool ?? new ConnectionPool();
    
      server.tool(
        "ssh_exec",
        "Execute a command on a remote host via SSH. The command is interpreted by the remote login shell — pipes, redirects, globs, and other shell metacharacters work as expected. Returns stdout, stderr, and exit code.",
        {
          ...connectionParams,
          command: z
            .string()
            .describe("Shell command to execute on the remote host (interpreted by the remote login shell)"),
          timeout: TimeoutSchema,
        },
        async ({ command, timeout, ...conn }) => {
          return connectionPool.withConnection(conn, async (client) => {
            const result = await exec(client, command, timeout || 30000);
            const parts: string[] = [];
            if (result.stdout) parts.push(result.stdout);
            if (result.stderr) parts.push(`[stderr]\n${result.stderr}`);
            parts.push(`[exit code: ${result.code}]`);
            return { content: [{ type: "text", text: parts.join("\n") }] };
          });
        },
      );
    
      server.tool(
        "ssh_read_file",
        "Read a file from a remote host via SFTP.",
        {
          ...connectionParams,
          path: z.string().describe("Absolute path to the remote file"),
        },
        async ({ path, ...conn }) => {
          return connectionPool.withConnection(conn, async (client) => {
            const content = await readFile(client, path);
            return { content: [{ type: "text", text: content }] };
          });
        },
      );
    
      server.tool(
        "ssh_write_file",
        "Write content to a file on a remote host via SFTP. Creates or overwrites the file.",
        {
          ...connectionParams,
          path: z.string().describe("Absolute path to the remote file"),
          content: z.string().describe("File content to write"),
        },
        async ({ path, content, ...conn }) => {
          return connectionPool.withConnection(conn, async (client) => {
            await writeFile(client, path, content);
            return { content: [{ type: "text", text: `Wrote ${content.length} bytes to ${path}` }] };
          });
        },
      );
    
      server.tool(
        "ssh_upload",
        "Upload a local file to a remote host via SFTP.",
        {
          ...connectionParams,
          localPath: z.string().describe("Path to the local file to upload"),
          remotePath: z.string().describe("Absolute path on the remote host"),
        },
        async ({ localPath, remotePath, ...conn }) => {
          return connectionPool.withConnection(conn, async (client) => {
            await uploadFile(client, localPath, remotePath);
            return { content: [{ type: "text", text: `Uploaded ${localPath} → ${remotePath}` }] };
          });
        },
      );
    
      server.tool(
        "ssh_download",
        "Download a file from a remote host to local filesystem via SFTP.",
        {
          ...connectionParams,
          remotePath: z.string().describe("Absolute path to the remote file"),
          localPath: z.string().describe("Local path to save the downloaded file"),
        },
        async ({ remotePath, localPath, ...conn }) => {
          return connectionPool.withConnection(conn, async (client) => {
            await downloadFile(client, remotePath, localPath);
            return { content: [{ type: "text", text: `Downloaded ${remotePath} → ${localPath}` }] };
          });
        },
      );
    
      server.tool(
        "ssh_ls",
        "List files in a directory on a remote host via SFTP.",
        {
          ...connectionParams,
          path: z.string().describe("Absolute path to the remote directory"),
        },
        async ({ path, ...conn }) => {
          return connectionPool.withConnection(conn, async (client) => {
            const files = await listDir(client, path);
            return { content: [{ type: "text", text: files.join("\n") }] };
          });
        },
      );
    
      server.tool(
        "ssh_diagnose",
        "Diagnose SSH connectivity issues. Checks ssh-agent status, loaded keys, known_hosts, SSH config, and attempts a test connection. Use this BEFORE attempting SSH operations if you suspect connectivity issues, or AFTER a failed SSH operation to understand why it failed.",
        {
          host: HostSchema,
          port: PortSchema,
        },
        async ({ host, port }) => {
          const report = diagnose(host, port || 22);
    
          const lines: string[] = [];
          lines.push(`SSH Diagnostic Report for ${host}:${port || 22}`);
          lines.push(`Overall: ${report.overall.toUpperCase()}`);
          lines.push("");
    
          for (const check of report.checks) {
            const icon = check.status === "ok" ? "PASS" : check.status === "warning" ? "WARN" : "FAIL";
            lines.push(`[${icon}] ${check.name}`);
            lines.push(`  ${check.message}`);
            lines.push("");
          }
    
          if (report.suggestions.length > 0) {
            lines.push("Suggested fixes:");
            for (const s of report.suggestions) {
              lines.push(`  - ${s}`);
            }
          }
    
          return { content: [{ type: "text", text: lines.join("\n") }], isError: report.overall === "error" };
        },
      );
    
      // --- SSH Environment Management ---
    
      server.tool(
        "ssh_agent_ensure",
        "Ensure ssh-agent is running and reachable. Starts a new agent if needed and sets environment variables so subsequent SSH operations work. Use this FIRST when SSH operations fail with agent-related errors.",
        {},
        async () => {
          const result = ensureAgent();
          const lines: string[] = [];
          lines.push(result.message);
          if (result.socket) lines.push(`Socket: ${result.socket}`);
          if (result.keys.length > 0) {
            lines.push("Loaded keys:");
            for (const k of result.keys) lines.push(`  ${k}`);
          }
          if (result.env) {
            lines.push("Environment variables set in this session:");
            if (result.env.SSH_AUTH_SOCK) lines.push(`  SSH_AUTH_SOCK=${result.env.SSH_AUTH_SOCK}`);
            if (result.env.SSH_AGENT_PID) lines.push(`  SSH_AGENT_PID=${result.env.SSH_AGENT_PID}`);
          }
          return { content: [{ type: "text", text: lines.join("\n") }], isError: !result.reachable };
        },
      );
    
      server.tool(
        "ssh_key_list",
        "List all SSH private keys in ~/.ssh/ with their type, fingerprint, and whether they are loaded in the agent. Use this to find which keys are available and which ones need to be loaded.",
        {},
        async () => {
          const keys = listSshKeys();
          if (keys.length === 0) {
            return {
              content: [
                {
                  type: "text",
                  text: 'No SSH private keys found in ~/.ssh/. Generate one: ssh-keygen -t ed25519 -C "your@email.com"',
                },
              ],
            };
          }
    
          const lines: string[] = [`Found ${keys.length} SSH key(s):`, ""];
          for (const key of keys) {
            const status = key.loadedInAgent ? "LOADED" : "not loaded";
            lines.push(`${key.name} (${key.type}) [${status}]`);
            lines.push(`  Path: ${key.path}`);
            if (key.fingerprint) lines.push(`  Fingerprint: ${key.fingerprint}`);
            lines.push("");
          }
          return { content: [{ type: "text", text: lines.join("\n") }] };
        },
      );
    
      server.tool(
        "ssh_key_load",
        "Load an SSH private key into the running agent. Ensures the agent is running first. Use this after ssh_key_list shows a key that is not loaded.",
        {
          keyPath: z.string().describe("Path to the SSH private key to load (e.g. ~/.ssh/id_ed25519)"),
        },
        async ({ keyPath }) => {
          const result = loadKey(keyPath);
          return { content: [{ type: "text", text: result.message }], isError: result.status === "error" };
        },
      );
    
      server.tool(
        "ssh_config_lookup",
        "Resolve the effective SSH configuration for a host. Shows hostname, user, port, identity files, proxy settings, and all other options from ~/.ssh/config. Use this to understand how SSH will connect to a host.",
        {
          host: HostSchema,
        },
        async ({ host }) => {
          const result = configLookup(host);
          if ("error" in result) {
            return { content: [{ type: "text", text: result.error }], isError: true };
          }
    
          const lines: string[] = [`SSH config for "${host}":`, ""];
          lines.push(`  Hostname: ${result.hostname}`);
          lines.push(`  User: ${result.user}`);
          lines.push(`  Port: ${result.port}`);
          if (result.identityFile.length > 0) {
            lines.push(`  Identity files: ${result.identityFile.join(", ")}`);
          }
          if (result.proxyJump) lines.push(`  ProxyJump: ${result.proxyJump}`);
          if (result.proxyCommand) lines.push(`  ProxyCommand: ${result.proxyCommand}`);
          return { content: [{ type: "text", text: lines.join("\n") }] };
        },
      );
    
      server.tool(
        "ssh_known_hosts_fix",
        "Remove a stale host key from known_hosts and re-scan the host to add the current key. Use this when you see 'Host key verification failed' errors, typically after a server has been recreated or reprovisioned.",
        {
          host: HostSchema,
          port: PortSchema,
        },
        async ({ host, port }) => {
          const result = fixKnownHosts(host, port || 22);
          const lines: string[] = [result.message];
          if (result.actions.length > 0) {
            lines.push("");
            lines.push("Actions taken:");
            for (const a of result.actions) lines.push(`  - ${a}`);
          }
          return { content: [{ type: "text", text: lines.join("\n") }], isError: result.status === "error" };
        },
      );
    
      server.tool(
        "ssh_test",
        "Quick connectivity test to an SSH host. Reports success/failure with timing and actionable error details. Lighter and faster than ssh_diagnose — use this for a quick check before running operations.",
        {
          host: HostSchema,
          port: PortSchema,
        },
        async ({ host, port }) => {
          const result = testConnection(host, port || 22);
          return { content: [{ type: "text", text: result.message }], isError: result.status === "error" };
        },
      );
    
      server.tool(
        "ssh_git_check",
        "Test Git-over-SSH authentication to a hosting provider (GitHub, GitLab, Bitbucket, etc). Verifies your SSH key is registered and working. Use this when git clone/pull/push fails with SSH errors.",
        {
          host: z.string().optional().describe('Git hosting hostname (default: "github.com")'),
          user: z.string().optional().describe('SSH user for the git host (default: "git")'),
        },
        async ({ host, user }) => {
          const result = checkGitSsh(host || "github.com", user || "git");
          const lines: string[] = [result.message];
          if (result.authenticatedAs) {
            lines.push(`Authenticated as: ${result.authenticatedAs}`);
          }
          return { content: [{ type: "text", text: lines.join("\n") }], isError: result.status === "error" };
        },
      );
    
      // --- Higher-level operations ---
    
      server.tool(
        "ssh_multi_exec",
        "Execute a command on multiple remote hosts in parallel. Returns results per host. Use this instead of calling ssh_exec multiple times — it's faster and shows results side by side.",
        {
          hosts: z.array(z.string()).describe("List of SSH hostnames or IPs"),
          command: z.string().describe("Shell command to execute on all hosts"),
          port: PortSchema,
          username: UsernameSchema,
          privateKeyPath: KeyPathSchema,
          password: PasswordSchema,
          timeout: TimeoutSchema,
        },
        async ({ hosts, command, port, username, privateKeyPath, password, timeout }) => {
          const hostConfigs = hosts.map((host) => ({ host, port, username, privateKeyPath, password }));
          const results = await multiExec(connectionPool, hostConfigs, command, timeout || 30000);
    
          const lines: string[] = [];
          for (const r of results) {
            lines.push(`--- ${r.host} ---`);
            if (r.error) {
              lines.push(`[ERROR] ${r.error}`);
            } else {
              if (r.stdout) lines.push(r.stdout);
              if (r.stderr) lines.push(`[stderr] ${r.stderr}`);
              lines.push(`[exit code: ${r.code}]`);
            }
            lines.push("");
          }
          const hasErrors = results.some((r) => r.error || r.code !== 0);
          return { content: [{ type: "text", text: lines.join("\n") }], isError: hasErrors };
        },
      );
    
      server.tool(
        "ssh_find",
        "Search for files on a remote host. Wraps the find command with structured parameters so you don't have to construct find syntax manually.",
        {
          ...connectionParams,
          path: z.string().describe("Directory to search in (e.g. /var/log, /home/user)"),
          name: z.string().optional().describe("Filename pattern with wildcards (e.g. '*.log', 'config.*')"),
          type: z.enum(["f", "d", "l"]).optional().describe("File type: f=file, d=directory, l=symlink"),
          maxdepth: z.number().optional().describe("Maximum directory depth to search"),
          minsize: z.string().optional().describe("Minimum file size (e.g. '1M', '100k')"),
          maxsize: z.string().optional().describe("Maximum file size (e.g. '10M', '500k')"),
          timeout: TimeoutSchema,
        },
        async ({ path, name, type, maxdepth, minsize, maxsize, timeout, ...conn }) => {
          return connectionPool.withConnection(conn, async (client) => {
            const files = await find(client, { path, name, type, maxdepth, minsize, maxsize }, timeout || 30000);
            if (files.length === 0) {
              return { content: [{ type: "text", text: "No files found." }] };
            }
            return { content: [{ type: "text", text: `Found ${files.length} result(s):\n${files.join("\n")}` }] };
          });
        },
      );
    
      server.tool(
        "ssh_tail",
        "Read the last N lines of a file on a remote host, optionally filtering by a grep pattern. Use this for reading log files instead of ssh_exec with manual tail/grep commands.",
        {
          ...connectionParams,
          path: z.string().describe("Absolute path to the file to tail"),
          lines: z.number().optional().describe("Number of lines to read from the end (default: 100)"),
          grep: z.string().optional().describe("Case-insensitive pattern to filter lines"),
          timeout: TimeoutSchema,
        },
        async ({ path, lines, grep, timeout, ...conn }) => {
          return connectionPool.withConnection(conn, async (client) => {
            const output = await tail(client, path, lines || 100, grep, timeout || 30000);
            if (!output.trim()) {
              return {
                content: [
                  {
                    type: "text",
                    text: grep
                      ? `No lines matching "${grep}" in last ${lines || 100} lines.`
                      : "File is empty or does not exist.",
                  },
                ],
              };
            }
            return { content: [{ type: "text", text: output }] };
          });
        },
      );
    
      server.tool(
        "ssh_service_status",
        "Check the status of a systemd service on a remote host. Returns whether it's active, its PID, uptime, and description. Use this instead of ssh_exec with systemctl.",
        {
          ...connectionParams,
          service: z.string().describe("Systemd service name (e.g. nginx, sshd, docker)"),
          timeout: TimeoutSchema,
        },
        async ({ service, timeout, ...conn }) => {
          return connectionPool.withConnection(conn, async (client) => {
            const status = await serviceStatus(client, service, timeout || 30000);
            const lines: string[] = [];
            lines.push(`Service: ${status.name}`);
            lines.push(`Status: ${status.status}`);
            if (status.description) lines.push(`Description: ${status.description}`);
            if (status.pid) lines.push(`PID: ${status.pid}`);
            if (status.since) lines.push(`Since: ${status.since}`);
            lines.push("");
            lines.push(status.raw);
            return { content: [{ type: "text", text: lines.join("\n") }], isError: !status.active };
          });
        },
      );
    }
  • The core serviceStatus() helper function. Executes 'systemctl status -- <service>' on the remote host via SSH and parses the output to extract active status, description, PID, and since timestamp.
    export async function serviceStatus(client: Client, serviceName: string, timeoutMs = 30000): Promise<ServiceStatus> {
      // `--` so a service name starting with `-` isn't parsed as a systemctl flag.
      const result = await exec(client, `systemctl status -- ${shellQuote(serviceName)} 2>&1`, timeoutMs);
      const raw = result.stdout;
    
      const activeMatch = raw.match(/Active:\s+(\S+)\s+\(([^)]+)\)/);
      const descMatch = raw.match(/^\s+.*?-\s+(.+)$/m);
      const pidMatch = raw.match(/Main PID:\s+(\d+)/);
      const sinceMatch = raw.match(/since\s+(.+?);/);
    
      // No `Active:` line on a non-zero exit usually means the unit doesn't exist or is
      // inactive. "unknown" was misleading -- callers couldn't distinguish "down" from
      // "we couldn't tell." Prefer "inactive" since that's what the exit code maps to.
      const fallbackStatus = result.code === 0 ? "active" : "inactive";
    
      return {
        name: serviceName,
        active: activeMatch?.[1] === "active",
        status: activeMatch ? `${activeMatch[1]} (${activeMatch[2]})` : fallbackStatus,
        description: descMatch?.[1]?.trim(),
        since: sinceMatch?.[1]?.trim(),
        pid: pidMatch ? Number.parseInt(pidMatch[1], 10) : undefined,
        raw,
      };
    }
  • ServiceStatus interface definition with fields: name, active (boolean), status, description?, since?, pid?, and raw.
    export interface ServiceStatus {
      name: string;
      active: boolean;
      status: string;
      description?: string;
      since?: string;
      pid?: number;
      raw: string;
    }
Behavior3/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

No annotations are provided, so the description carries the full burden. It discloses that the tool returns status details but does not mention authentication requirements, side effects, or error conditions. The password warning is in the schema, not the description, so transparency is adequate but not rich.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is two sentences long, front-loads the purpose, and contains zero superfluous words. It is efficient and easy to scan.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness4/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Despite missing output schema, the description lists the expected return fields (active, PID, uptime, description). It includes guidance for using SSH keys and mentions the service name. For a read-only status check with standard SSH parameters, this is sufficiently complete.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema description coverage is 100%, so the schema already documents all parameters. The description adds no additional parameter semantics beyond the main functionality, meeting the baseline of 3.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description explicitly states the verb 'Check' and the resource 'systemd service on a remote host', and lists the returned fields (active, PID, uptime, description). It differentiates from siblings by advising to use this instead of ssh_exec with systemctl.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines4/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description provides direct usage guidance: 'Use this instead of ssh_exec with systemctl.' This clarifies when to choose this tool over a similar sibling, though it does not elaborate on exclusions or prerequisites.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

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/YawLabs/ssh-mcp'

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