Skip to main content
Glama

site-health-timeline

Fetch a per-site health snapshot over a lookback window, including device stability, reboots, WAN uptime, and optional client count. Replaces multiple API calls.

Instructions

Per-site health snapshot over a lookback window: devices with stability scores, reboots, WAN uptime, optional client count. Replaces 5+ sequential calls (devices + wan + reboots + clients). Caveats[] surfaces partial-data and API limitations.

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
hostNameYesSite host name (e.g. 'USM')
lookbackDaysNoWindow for reboot detection in days (1-90, default 7)
extractFieldsNoComma-separated dotted paths to project from response (e.g. 'id,name,owner.name,columns.*.name'). Use `*` as wildcard for arrays/objects. Wrap field names with dots in backticks. Reduces response tokens dramatically on large entities.

Implementation Reference

  • Main handler function for site-health-timeline. Resolves devices by hostName, fetches WAN uptime from /sites, detects reboots from device startupTime (1 per device max), optionally fetches client count via connector. Returns devices with stability scores, reboot summaries, WAN status, client count, and caveats.
    export async function siteHealthTimeline(params: z.infer<typeof siteHealthTimelineSchema>) {
      const lookbackDays = params.lookbackDays ?? 7;
      const lookbackHours = lookbackDays * 24;
      const now = new Date();
      const start = new Date(now.getTime() - lookbackHours * 60 * 60 * 1000);
      const period = {
        start: start.toISOString(),
        end: now.toISOString(),
        lookbackDays,
      };
    
      const caveats: string[] = [];
    
      const hostEntry = await resolveDevicesByHostName(params.hostName);
      if (!hostEntry) {
        return {
          hostName: params.hostName,
          hostId: null,
          period,
          devices: [] as DeviceTimelineEntry[],
          reboots: [] as RebootSummary[],
          wan: { uptimePct: null, samples: 0, status: "healthy" as WanStatus },
          clients: { connector: "unavailable" as const, count: null },
          caveats: [`site '${params.hostName}' not found`],
        };
      }
    
      const connectorAvailable = isConnectorAvailable();
    
      const { sites, connectorContext } = await aggregate(
        {
          sites: () =>
            unifiClient.get<{ data: Array<{ hostId: string; statistics: { wans?: Record<string, { wanUptime?: number; externalIp?: string }> } }> }>("/sites"),
          connectorContext: connectorAvailable ? () => resolveConnectorContext(params.hostName) : () => Promise.resolve(null),
        },
        caveats,
      );
    
      // --- WAN aggregation (current / lifetime state from /sites) ---
      let wanUptimePct: number | null = null;
      let wanSamples = 0;
      if (sites) {
        const siteData = sites.data.find((s) => s.hostId === hostEntry.hostId);
        const wans = siteData?.statistics.wans ?? {};
        const uptimes = Object.values(wans)
          .map((w) => w.wanUptime)
          .filter((u): u is number => typeof u === "number");
        wanSamples = uptimes.length;
        if (uptimes.length > 0) {
          wanUptimePct = Math.round((uptimes.reduce((a, b) => a + b, 0) / uptimes.length) * 10) / 10;
        }
      }
      caveats.push("wan.uptimePct is sourced from /sites.statistics (current/lifetime state); the lookback window applies only to reboot detection");
    
      // --- Devices + reboot detection (reuses logic from detect-recent-reboots) ---
      const deviceEntries: DeviceTimelineEntry[] = [];
      const rebootList: RebootSummary[] = [];
    
      for (const d of hostEntry.devices) {
        const hours = hoursAgo(d.startupTime);
        const rebootedInWindow = hours !== null && hours < lookbackHours;
        // The Site Manager API exposes only the last startupTime, so reboots
        // within the window can only ever be 0 or 1 per device. Surface this
        // limitation in caveats below rather than fabricating a higher count.
        const rebootsInWindow = rebootedInWindow ? 1 : 0;
    
        deviceEntries.push({
          id: d.id,
          name: d.name,
          model: d.model,
          status: d.status,
          startupTime: d.startupTime,
          rebootsInWindow,
          stabilityScore: classifyReboots(rebootsInWindow),
        });
    
        if (rebootedInWindow && hours !== null) {
          let severity: RebootSummary["severity"] = "info";
          if (hours < 1) severity = "critical";
          else if (hours < 24) severity = "warning";
          rebootList.push({
            deviceId: d.id,
            deviceName: d.name,
            severity,
            hoursAgo: Math.round(hours * 10) / 10,
          });
        }
      }
      rebootList.sort((a, b) => a.hoursAgo - b.hoursAgo);
      caveats.push("rebootsInWindow is at most 1 per device — Site Manager API exposes only the most recent startupTime, not full reboot history");
    
      // --- Optional connector clients ---
      let clientsBlock: { connector: "available" | "unavailable"; count: number | null };
      if (!connectorAvailable) {
        clientsBlock = { connector: "unavailable", count: null };
        caveats.push("UNIFI_API_KEY_OWNER not set — client count omitted");
      } else if (!connectorContext) {
        clientsBlock = { connector: "unavailable", count: null };
        caveats.push(`connector context unavailable for '${params.hostName}'`);
      } else {
        const ctx = connectorContext;
        const clientsResp = await connectorClient.get<{ data: unknown[] }>(
          ctx.hostId,
          `network/integration/v1/sites/${ctx.localSiteId}/clients`,
          { limit: 1000 },
        ).catch((err: unknown) => {
          caveats.push(`connector clients fetch failed: ${err instanceof Error ? err.message : String(err)}`);
          return null;
        });
        if (clientsResp && Array.isArray(clientsResp.data)) {
          clientsBlock = { connector: "available", count: clientsResp.data.length };
        } else {
          clientsBlock = { connector: "available", count: null };
        }
      }
    
      return {
        hostName: hostEntry.hostName,
        hostId: hostEntry.hostId,
        period,
        devices: deviceEntries,
        reboots: rebootList,
        wan: {
          uptimePct: wanUptimePct,
          samples: wanSamples,
          status: classifyWan(wanUptimePct),
        },
        clients: clientsBlock,
        caveats,
      };
    }
  • Input schema for site-health-timeline: hostName (string), lookbackDays (1-90, default 7), and extractFields.
    export const siteHealthTimelineSchema = z.object({
      hostName: z.string().describe("Site host name (e.g. 'USM')"),
      lookbackDays: z.coerce.number().int().min(1).max(90).optional().default(7)
        .describe("Window for reboot detection in days (1-90, default 7)"),
      extractFields: ef,
    });
  • src/index.ts:345-347 (registration)
    Registration of the 'site-health-timeline' tool via the tool() function, wiring the schema shape and wrapped handler.
    tool("site-health-timeline",
      "Per-site health snapshot over a lookback window: devices with stability scores, reboots, WAN uptime, optional client count. Replaces 5+ sequential calls (devices + wan + reboots + clients). Caveats[] surfaces partial-data and API limitations.",
      siteHealthTimelineSchema.shape, wrapToolHandler(siteHealthTimeline));
  • src/index.ts:52-55 (registration)
    Import of siteHealthTimelineSchema and siteHealthTimeline from ./tools/aggregations.js at the top of index.ts.
    import {
      summarizeSiteSchema, summarizeSite,
      siteHealthTimelineSchema, siteHealthTimeline,
    } from "./tools/aggregations.js";
  • Helper functions used by the handler: hoursAgo (calculates hours since a timestamp), classifyReboots (maps reboot count to stability score), classifyWan (maps uptime percentage to status).
    function hoursAgo(isoTime: string | null | undefined): number | null {
      if (!isoTime) return null;
      const diff = Date.now() - new Date(isoTime).getTime();
      return diff / (1000 * 60 * 60);
    }
    
    function classifyReboots(count: number): StabilityScore {
      if (count < 1) return "stable";
      if (count <= 3) return "intermittent";
      return "unstable";
    }
    
    function classifyWan(uptimePct: number | null): WanStatus {
      if (uptimePct === null) return "healthy";
      if (uptimePct < 90) return "critical";
      if (uptimePct < 95) return "warning";
      return "healthy";
    }
Behavior3/5

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

With no annotations provided, the description carries the burden of behavioral disclosure. It notes the presence of caveats for partial data and API limitations, which is helpful, but does not indicate whether the tool is read-only, destructive, or its error handling. This leaves some gaps in transparency.

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 extremely concise: two sentences that clearly convey the purpose and a key benefit. The mention of caveats is efficiently embedded. Zero wasted words.

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?

Given the tool's complexity (aggregating multiple data points) and the absence of an output schema, the description covers the essential elements: what data is included and that there are caveats. It could mention pagination or rate limits, but for a snapshot tool, it is largely 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?

All three parameters have descriptions in the input schema (100% coverage), so the description does not need to add much. The description adds context about the output fields (e.g., stability scores, client counts) but does not explain parameter semantics beyond what the schema already provides. Baseline of 3 is appropriate.

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 it provides a per-site health snapshot including devices, stability scores, reboots, WAN uptime, and optional client counts. It also distinguishes itself by noting it replaces 5+ sequential calls, making its purpose clear and differentiated from siblings.

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 implies when to use it (instead of multiple individual calls) and mentions caveats for partial data and API limitations. However, it lacks explicit when-not-to-use guidance or specific alternatives from the sibling list, which slightly reduces clarity.

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/us-all/unifi-mcp-server'

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