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
/**
* Scout (SSH) operation formatting utilities
*
* Provides consistent markdown formatting for file operations:
* read, list, tree, exec, find, transfer, and diff.
*
* ## STYLE.md Compliance
*
* All formatters follow STYLE.md specifications:
* - Section 3.1: Plain text titles (no markdown ## prefix)
* - Section 4.1: Canonical symbols only (✓✗⚠, NO emoji)
* - Section 3.6: Freshness timestamps for volatile operations (TODO: Phase 2.2)
* - Section 3.2: Summary lines (TODO: Phase 2.2)
*
* Symbol Legend:
* - ✓ = Success (command exit 0)
* - ✗ = Error (command exit non-zero)
* - ⚠ = Warning (truncation, non-fatal issues)
*/
import { formatBytes } from "../services/docker/utils/formatters.js";
import { getTimestamp, truncateIfNeeded } from "./utils.js";
/**
* Format file read result as markdown
*
* Title format per STYLE.md 3.1: Plain text, no markdown ## prefix
* Summary per STYLE.md 3.2: Pipe-separated segments
* Warning symbol: ⚠ (canonical non-emoji variant)
*
* @example
* ```ts
* const output = formatScoutReadMarkdown("squirts", "/etc/hostname", "squirts\n", 8, false);
* // Returns:
* // File Read: squirts:/etc/hostname
* // Size: 8.0 B | truncated: no
* //
* // ```
* // squirts
* // ```
* ```
*/
export function formatScoutReadMarkdown(
host: string,
path: string,
content: string,
size: number,
truncated: boolean
): string {
const lines = [
`File Read: ${host}:${path}`,
`Size: ${formatBytes(size)} | truncated: ${truncated ? "yes" : "no"}`,
"",
"```",
content,
"```",
];
if (truncated) {
lines.push("");
lines.push("⚠ *File was truncated to fit size limit*");
}
return truncateIfNeeded(lines.join("\n"));
}
/**
* Format directory listing as markdown
*
* Title format per STYLE.md 3.1: Plain text, no markdown ## prefix
* Summary per STYLE.md 3.2: Pipe-separated segments
*
* @example
* ```ts
* formatScoutListMarkdown("squirts", "/etc", "file1\nfile2\nfile3");
* // Directory Listing: squirts:/etc
* // Items: 3
* //
* // ```
* // file1
* // file2
* // file3
* // ```
* ```
*/
export function formatScoutListMarkdown(host: string, path: string, listing: string): string {
const items = listing.split("\n").filter((l) => l.trim()).length;
return truncateIfNeeded(
[`Directory Listing: ${host}:${path}`, `Items: ${items}`, "", "```", listing, "```"].join("\n")
);
}
/**
* Format tree output as markdown
*
* Title format per STYLE.md 3.1: Plain text, no markdown ## prefix
* Summary per STYLE.md 3.2: Depth displayed as separate line
*
* @example
* ```ts
* const output = formatScoutTreeMarkdown("squirts", "/opt", "dir1/\n file1\ndir2/", 2);
* // Returns:
* // Directory Tree: squirts:/opt
* // Depth: 2
* //
* // ```
* // dir1/
* // file1
* // dir2/
* // ```
* ```
*/
export function formatScoutTreeMarkdown(
host: string,
path: string,
tree: string,
depth: number
): string {
return truncateIfNeeded(
[`Directory Tree: ${host}:${path}`, `Depth: ${depth}`, "", "```", tree, "```"].join("\n")
);
}
/**
* Format command execution result as markdown
*
* Title format per STYLE.md 3.1: Plain text, no markdown ## prefix
* Symbols per STYLE.md 4.1: ✓ for success, ✗ for error
* Freshness per STYLE.md 3.6: Command execution is volatile data
*
* @example
* ```ts
* const output = formatScoutExecMarkdown("squirts", "/tmp", "uptime", "15:23:45 up 3 days", 0);
* // Returns:
* // ✓ Command Execution: squirts:/tmp
* // Exit: 0
* // As of (EST): 11:05:20 | 02/13/2026
* //
* // **Command:** `uptime`
* // **Exit:** 0
* //
* // **Output:**
* // ```
* // 15:23:45 up 3 days
* // ```
* ```
*/
export function formatScoutExecMarkdown(
host: string,
path: string,
command: string,
stdout: string,
exitCode: number
): string {
const statusSymbol = exitCode === 0 ? "✓" : "✗";
return truncateIfNeeded(
[
`${statusSymbol} Command Execution: ${host}:${path}`,
`Exit: ${exitCode}`,
getTimestamp(),
"",
`**Command:** \`${command}\``,
`**Exit:** ${exitCode}`,
"",
"**Output:**",
"```",
stdout,
"```",
].join("\n")
);
}
/**
* Format find results as markdown
*
* Title format per STYLE.md 3.1: Plain text, no markdown ## prefix
* Summary per STYLE.md 3.2: Pattern and result count in pipe-separated format
*
* @example
* ```ts
* const output = formatScoutFindMarkdown("squirts", "/opt", "*.log", "access.log\nerror.log");
* // Returns:
* // Find Results: squirts:/opt
* // Pattern: *.log | Results: 2
* //
* // **Pattern:** `*.log`
* // **Results:** 2 files
* //
* // ```
* // access.log
* // error.log
* // ```
* ```
*/
export function formatScoutFindMarkdown(
host: string,
path: string,
pattern: string,
results: string
): string {
const lines = results.split("\n").filter((l) => l.trim());
return truncateIfNeeded(
[
`Find Results: ${host}:${path}`,
`Pattern: ${pattern} | Results: ${lines.length}`,
"",
`**Pattern:** \`${pattern}\``,
`**Results:** ${lines.length} files`,
"",
"```",
results,
"```",
].join("\n")
);
}
/**
* Format file transfer result as markdown
*
* Title format per STYLE.md 3.1: Plain text, no markdown ## prefix
* Summary per STYLE.md 3.2: Source, target, and size in pipe-separated format
* Warning symbol: ⚠ (canonical non-emoji variant)
*
* @example
* ```ts
* const output = formatScoutTransferMarkdown("squirts", "/data/backup.tar", "boops", "/backups/backup.tar", 10485760);
* // Returns:
* // File Transfer Complete
* // From: squirts | To: boops | Size: 10.0 MB
* //
* // **From:** squirts:/data/backup.tar
* // **To:** boops:/backups/backup.tar
* // **Size:** 10.0 MB
* ```
*/
export function formatScoutTransferMarkdown(
sourceHost: string,
sourcePath: string,
targetHost: string,
targetPath: string,
bytesTransferred: number,
warning?: string
): string {
const lines = [
"File Transfer Complete",
`From: ${sourceHost} | To: ${targetHost} | Size: ${formatBytes(bytesTransferred)}`,
"",
`**From:** ${sourceHost}:${sourcePath}`,
`**To:** ${targetHost}:${targetPath}`,
`**Size:** ${formatBytes(bytesTransferred)}`,
];
if (warning) {
lines.push("");
lines.push(`⚠ ${warning}`);
}
return lines.join("\n");
}
/**
* Format file diff result as markdown
*
* Title format per STYLE.md 3.1: Plain text, no markdown ## prefix
* Summary per STYLE.md 3.2: Both file paths in pipe-separated format
*
* @example
* ```ts
* const output = formatScoutDiffMarkdown("squirts", "/etc/config.old", "boops", "/etc/config.new", "- old line\n+ new line");
* // Returns:
* // File Diff
* // File 1: squirts:/etc/config.old | File 2: boops:/etc/config.new
* //
* // **File 1:** squirts:/etc/config.old
* // **File 2:** boops:/etc/config.new
* //
* // ```diff
* // - old line
* // + new line
* // ```
* ```
*/
export function formatScoutDiffMarkdown(
host1: string,
path1: string,
host2: string,
path2: string,
diff: string
): string {
return truncateIfNeeded(
[
"File Diff",
`File 1: ${host1}:${path1} | File 2: ${host2}:${path2}`,
"",
`**File 1:** ${host1}:${path1}`,
`**File 2:** ${host2}:${path2}`,
"",
"```diff",
diff,
"```",
].join("\n")
);
}
/**
* Format list of hosts as markdown table
*
* Title format per STYLE.md 3.1: Plain text, no markdown ## prefix
* Summary per STYLE.md 3.2: Host count
* Table format per STYLE.md 7.2: ASCII table with aligned columns
*
* @example
* ```ts
* const output = formatScoutNodesMarkdown(["squirts", "boops", "nicks"]);
* // Returns:
* // Scout Nodes
* // Hosts: 3
* //
* // | Host |
* // |------|
* // | squirts |
* // | boops |
* // | nicks |
* ```
*/
export function formatScoutNodesMarkdown(hosts: string[]): string {
const lines = ["Scout Nodes", `Hosts: ${hosts.length}`, "", "| Host |", "|------|"];
for (const host of hosts) {
lines.push(`| ${host} |`);
}
return lines.join("\n");
}
/**
* Format process listing as markdown
*
* Title format per STYLE.md 3.1: Plain text, no markdown ## prefix
* Summary per STYLE.md 3.2: Process count
* Freshness per STYLE.md 3.6: Process data is volatile
*
* @example
* ```ts
* const output = formatScoutPsMarkdown("squirts", " PID COMMAND\n 1234 nginx\n 5678 node\n 9012 postgres");
* // Returns:
* // Process Listing: squirts
* // Processes: 3
* // As of (EST): 11:27:00 | 02/13/2026
* //
* // ```
* // PID COMMAND
* // 1234 nginx
* // 5678 node
* // 9012 postgres
* // ```
* ```
*/
export function formatScoutPsMarkdown(host: string, processes: string): string {
const lines = processes.split("\n").filter((l) => l.trim());
const processCount = lines.length > 1 ? lines.length - 1 : 0; // Subtract header line
return truncateIfNeeded(
[
`Process Listing: ${host}`,
`Processes: ${processCount}`,
getTimestamp(),
"",
"```",
processes,
"```",
].join("\n")
);
}
/**
* Format disk usage as markdown with warnings for high usage
*
* Title format per STYLE.md 3.1: Plain text, no markdown ## prefix
* Summary per STYLE.md 3.2: Filesystem count
* Freshness per STYLE.md 3.6: Disk data is volatile
* Warning per STYLE.md 10.2: ⚠ for filesystems >85% usage
*
* @example
* ```ts
* const output = formatScoutDfMarkdown("squirts", "Filesystem Size Used Avail Use% Mounted on\n/dev/sda1 100G 90G 10G 90% /\n/dev/sdb1 50G 20G 30G 40% /data");
* // Returns:
* // Disk Usage: squirts
* // Filesystems: 2 ⚠
* // As of (EST): 11:28:20 | 02/13/2026
* //
* // ```
* // Filesystem Size Used Avail Use% Mounted on
* // /dev/sda1 100G 90G 10G 90% /
* // /dev/sdb1 50G 20G 30G 40% /data
* // ```
* //
* // ⚠ *One or more filesystems exceed 85% usage*
* ```
*/
export function formatScoutDfMarkdown(host: string, diskUsage: string): string {
const lines = diskUsage.split("\n").filter((l) => l.trim());
const fsCount = lines.length > 1 ? lines.length - 1 : 0; // Subtract header line
// Parse for high usage warnings (>85%)
const hasWarnings = lines.some((line) => {
const match = line.match(/(\d+)%/);
return match && Number.parseInt(match[1], 10) > 85;
});
const outputLines = [
`Disk Usage: ${host}`,
`Filesystems: ${fsCount}${hasWarnings ? " ⚠" : ""}`,
getTimestamp(),
"",
"```",
diskUsage,
"```",
];
if (hasWarnings) {
outputLines.push("");
outputLines.push("⚠ *One or more filesystems exceed 85% usage*");
}
return truncateIfNeeded(outputLines.join("\n"));
}
/**
* Format syslog output as markdown
*
* Title format per STYLE.md 3.1: Plain text, no markdown ## prefix
* Summary per STYLE.md 3.2: Lines count
* Freshness per STYLE.md 3.6: Log data is volatile
* Request echo per STYLE.md 3.5: Show lines requested and optional grep filter
*
* @example
* ```ts
* const output = formatScoutSyslogMarkdown("squirts", 50, "Feb 13 11:00:00 squirts sshd[1234]: Accepted key for user from 192.168.1.1\nFeb 13 11:01:00 squirts systemd[1]: Started session.", "sshd");
* // Returns:
* // Syslog: squirts
* // Lines requested: 50 | Returned: 2 | Filter: sshd
* // As of (EST): 11:29:50 | 02/13/2026
* //
* // ```
* // Feb 13 11:00:00 squirts sshd[1234]: Accepted key for user from 192.168.1.1
* // Feb 13 11:01:00 squirts systemd[1]: Started session.
* // ```
* ```
*/
export function formatScoutSyslogMarkdown(
host: string,
linesRequested: number,
logs: string,
grepFilter?: string
): string {
const lines = logs.split("\n").filter((l) => l.trim());
const actualLines = lines.length;
const outputLines = [
`Syslog: ${host}`,
`Lines requested: ${linesRequested} | Returned: ${actualLines}${grepFilter ? ` | Filter: ${grepFilter}` : ""}`,
getTimestamp(),
"",
"```",
logs,
"```",
];
return truncateIfNeeded(outputLines.join("\n"));
}
/**
* Format journal (journalctl) output as markdown
*
* Title format per STYLE.md 3.1: Plain text, no markdown ## prefix
* Summary per STYLE.md 3.2: Lines count
* Freshness per STYLE.md 3.6: Log data is volatile
* Request echo per STYLE.md 3.5: Show lines requested and optional grep filter
*
* @example
* ```ts
* const output = formatScoutJournalMarkdown("squirts", 50, "Feb 13 11:30:00 squirts systemd[1]: Started service\nFeb 13 11:31:00 squirts systemd[1]: Stopped service");
* // Returns:
* // Journal: squirts
* // Lines requested: 50 | Returned: 2
* // As of (EST): 11:30:20 | 02/13/2026
* //
* // ```
* // Feb 13 11:30:00 squirts systemd[1]: Started service
* // Feb 13 11:31:00 squirts systemd[1]: Stopped service
* // ```
* ```
*/
export function formatScoutJournalMarkdown(
host: string,
linesRequested: number,
logs: string,
grepFilter?: string
): string {
const lines = logs.split("\n").filter((l) => l.trim());
const actualLines = lines.length;
const outputLines = [
`Journal: ${host}`,
`Lines requested: ${linesRequested} | Returned: ${actualLines}${grepFilter ? ` | Filter: ${grepFilter}` : ""}`,
getTimestamp(),
"",
"```",
logs,
"```",
];
return truncateIfNeeded(outputLines.join("\n"));
}
/**
* Format kernel ring buffer (dmesg) logs as markdown
*
* Title format per STYLE.md 3.1: Plain text, no markdown ## prefix
* Summary per STYLE.md 3.2: Pipe-separated with request echo
* Request echo per STYLE.md 3.5: Shows lines requested parameter
* Freshness per STYLE.md 3.6: Volatile kernel log data
*
* @param host - Host name the logs are from
* @param linesRequested - Number of lines requested from dmesg
* @param logs - Raw dmesg log output
* @param grepFilter - Optional grep pattern applied to logs
* @returns Formatted markdown string
*
* @example
* ```typescript
* const output = formatScoutDmesgMarkdown("squirts", 100, "[ 0.000000] Linux version 6.1.0-generic\n[ 0.001234] Command line: BOOT_IMAGE=/boot/vmlinuz");
* // Dmesg: squirts
* // Lines requested: 100 | Returned: 2
* // As of (EST): 11:45:30 | 02/13/2026
* //
* // ```
* // [ 0.000000] Linux version 6.1.0-generic
* // [ 0.001234] Command line: BOOT_IMAGE=/boot/vmlinuz
* // ```
* ```
*/
export function formatScoutDmesgMarkdown(
host: string,
linesRequested: number,
logs: string,
grepFilter?: string
): string {
const lines = logs.split("\n").filter((l) => l.trim());
const actualLines = lines.length;
const outputLines = [
`Dmesg: ${host}`,
`Lines requested: ${linesRequested} | Returned: ${actualLines}${grepFilter ? ` | Filter: ${grepFilter}` : ""}`,
getTimestamp(),
"",
"```",
logs,
"```",
];
return truncateIfNeeded(outputLines.join("\n"));
}
/**
* Format authentication logs as markdown
*
* Title format per STYLE.md 3.1: Plain text, no markdown ## prefix
* Summary per STYLE.md 3.2: Pipe-separated with request echo
* Request echo per STYLE.md 3.5: Shows lines requested parameter
* Freshness per STYLE.md 3.6: Volatile authentication log data
*
* @param host - Host name the logs are from
* @param linesRequested - Number of lines requested from auth log
* @param logs - Raw authentication log output
* @param grepFilter - Optional grep pattern applied to logs
* @returns Formatted markdown string
*
* @example
* ```typescript
* const output = formatScoutAuthMarkdown("squirts", 50, "Feb 13 11:00:00 squirts sshd[1234]: Accepted publickey for user from 192.168.1.100\nFeb 13 11:00:05 squirts sshd[1234]: pam_unix(sshd:session): session opened");
* // Auth Logs: squirts
* // Lines requested: 50 | Returned: 2
* // As of (EST): 11:00:30 | 02/13/2026
* //
* // ```
* // Feb 13 11:00:00 squirts sshd[1234]: Accepted publickey for user from 192.168.1.100
* // Feb 13 11:00:05 squirts sshd[1234]: pam_unix(sshd:session): session opened
* // ```
* ```
*/
export function formatScoutAuthMarkdown(
host: string,
linesRequested: number,
logs: string,
grepFilter?: string
): string {
const lines = logs.split("\n").filter((l) => l.trim());
const actualLines = lines.length;
const outputLines = [
`Auth Logs: ${host}`,
`Lines requested: ${linesRequested} | Returned: ${actualLines}${grepFilter ? ` | Filter: ${grepFilter}` : ""}`,
getTimestamp(),
"",
"```",
logs,
"```",
];
return truncateIfNeeded(outputLines.join("\n"));
}
/**
* Format ZFS pool list as markdown with health status symbols
*
* Title format per STYLE.md 3.1: Plain text, no markdown ## prefix
* Summary per STYLE.md 3.2: Pipe-separated pool count
* Symbols per STYLE.md 4.1: ● for ONLINE, ⚠ for degraded, ✗ for faulted
*
* Health status mapping:
* - ONLINE → ● (healthy)
* - DEGRADED/UNAVAIL → ⚠ (warning)
* - FAULTED/OFFLINE/REMOVED → ✗ (error)
*
* @param host - Host name where pools exist
* @param pools - Raw zpool list output
* @returns Formatted markdown string with health symbols
*
* @example
* ```typescript
* const output = formatScoutZfsPoolsMarkdown("squirts", "NAME SIZE ALLOC FREE HEALTH ALTROOT\ntank 10.9T 8.2T 2.7T ONLINE -");
* // ZFS Pools: squirts
* // Pools: 1
* //
* // ```
* // NAME SIZE ALLOC FREE HEALTH STATUS
* // tank 10.9T 8.2T 2.7T ONLINE ●
* // ```
* ```
*/
export function formatScoutZfsPoolsMarkdown(host: string, pools: string): string {
const lines = pools.split("\n").filter((l) => l.trim());
const poolLines = lines.slice(1); // Skip header
const poolCount = poolLines.length;
// Add health symbols to each pool line
const annotatedLines = [`${lines[0]} STATUS`];
for (const line of poolLines) {
let symbol = "—";
if (line.includes("ONLINE")) symbol = "●";
else if (line.includes("DEGRADED") || line.includes("UNAVAIL")) symbol = "⚠";
else if (line.includes("FAULTED") || line.includes("OFFLINE") || line.includes("REMOVED"))
symbol = "✗";
annotatedLines.push(`${line} ${symbol}`);
}
const outputLines = [
`ZFS Pools: ${host}`,
`Pools: ${poolCount}`,
"",
"```",
...annotatedLines,
"```",
];
return truncateIfNeeded(outputLines.join("\n"));
}
/**
* Format ZFS datasets with usage warnings
*
* Title format per STYLE.md 3.1: Plain text, no markdown ## prefix
* Summary per STYLE.md 3.2: Pipe-separated dataset count
* Warning per STYLE.md 10.2: ⚠ for datasets >85% used
*
* @param host - Host name where datasets exist
* @param datasets - Raw zfs list output
* @returns Formatted markdown string with usage warnings
*
* @example
* ```typescript
* const output = formatScoutZfsDatasetsMarkdown("squirts", "NAME USED AVAIL REFER MOUNTPOINT\ntank/media 9.3T 1.0T 9.3T /mnt/media");
* // ZFS Datasets: squirts
* // Datasets: 1 ⚠
* //
* // ```
* // NAME USED AVAIL REFER MOUNTPOINT
* // tank/media 9.3T 1.0T 9.3T /mnt/media
* // ```
* //
* // ⚠ *One or more datasets exceed 85% usage*
* ```
*/
export function formatScoutZfsDatasetsMarkdown(host: string, datasets: string): string {
const lines = datasets.split("\n").filter((l) => l.trim());
const datasetLines = lines.slice(1); // Skip header
const datasetCount = datasetLines.length;
// Check for high usage (>85%)
let hasWarnings = false;
for (const line of datasetLines) {
const parts = line.trim().split(/\s+/);
if (parts.length >= 3) {
const used = parts[1];
const avail = parts[2];
// Parse sizes (assume T/G/M/K suffixes)
const usedNum = parseSize(used);
const availNum = parseSize(avail);
if (usedNum + availNum > 0) {
const usagePercent = (usedNum / (usedNum + availNum)) * 100;
if (usagePercent > 85) {
hasWarnings = true;
break;
}
}
}
}
const outputLines = [
`ZFS Datasets: ${host}`,
`Datasets: ${datasetCount}${hasWarnings ? " ⚠" : ""}`,
"",
"```",
datasets,
"```",
];
if (hasWarnings) {
outputLines.push("");
outputLines.push("⚠ *One or more datasets exceed 85% usage*");
}
return truncateIfNeeded(outputLines.join("\n"));
}
/**
* Format ZFS snapshots list as markdown
*
* Title format per STYLE.md 3.1: Plain text, no markdown ## prefix
* Summary per STYLE.md 3.2: Pipe-separated snapshot count
*
* @param host - Host name where snapshots exist
* @param snapshots - Raw zfs list -t snapshot output
* @returns Formatted markdown string
*
* @example
* ```typescript
* const output = formatScoutZfsSnapshotsMarkdown("squirts", "NAME USED REFER\ntank/media@2024-02-13 512M 8.2T");
* // ZFS Snapshots: squirts
* // Snapshots: 1
* //
* // ```
* // NAME USED REFER
* // tank/media@2024-02-13 512M 8.2T
* // ```
* ```
*/
export function formatScoutZfsSnapshotsMarkdown(host: string, snapshots: string): string {
const lines = snapshots.split("\n").filter((l) => l.trim());
const snapshotLines = lines.slice(1); // Skip header
const snapshotCount = snapshotLines.length;
const outputLines = [
`ZFS Snapshots: ${host}`,
`Snapshots: ${snapshotCount}`,
"",
"```",
snapshots,
"```",
];
return truncateIfNeeded(outputLines.join("\n"));
}
/**
* Parse ZFS size string (e.g., "8.2T", "512M") to numeric bytes
* Used for usage percentage calculations
*/
function parseSize(sizeStr: string): number {
const match = sizeStr.trim().match(/^([\d.]+)\s*([BKMGTPEZ])?$/i);
if (!match) return 0;
const [, numStr, suffix] = match;
const num = Number.parseFloat(numStr);
const multipliers: Record<string, number> = {
B: 1,
K: 1024,
M: 1024 ** 2,
G: 1024 ** 3,
T: 1024 ** 4,
P: 1024 ** 5,
E: 1024 ** 6,
Z: 1024 ** 7,
};
return num * (suffix ? multipliers[suffix.toUpperCase()] || 1 : 1);
}