@hoststack.dev/mcp 0.6.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -487,21 +487,31 @@ defineTool({
487
487
  "",
488
488
  'By default events are AGGREGATED by (action, resourceId) so flapping events collapse to one row with a fire count + first/last timestamps \u2014 e.g. "service.auto_restarted on service 31, 8 times in the last hour, last at 14:22". Pass aggregate=false to see every raw row.',
489
489
  "",
490
+ 'ACTIVE vs HISTORICAL (v89): each aggregated entry now has an `active` boolean and a `lastResolvedAt` timestamp. `active=true` means the most recent occurrence has NOT been resolved \u2014 the alert is still on fire. The default `active=true` filter hides resolved/historical alerts so triage starts with "what is broken right now". Pass `active=false` to include resolved entries too (useful for post-mortems). Resolution is automatic for some alert kinds \u2014 e.g. `deploy.failed_consecutive` is cleared when the next deploy of the same service goes live.',
491
+ "",
492
+ 'Database backup_failed entries (v89) include `containerStatus` / `containerHealth` / `containerExitCode` in lastMetadata when the agent could resolve the container at failure time, so you can tell "Redis is overloaded" apart from "Redis is restarting" without a second tool call.',
493
+ "",
494
+ "Deploy.failed_consecutive entries (v89) carry the offending `commitHash` in lastMetadata, and the streak now dedupes per (service, commitHash) \u2014 one critical per bad commit, not one every 6 retries.",
495
+ "",
490
496
  "Inputs (all optional):",
491
497
  ' - since: ISO-8601 timestamp OR relative offset like "-1h" / "-2d". Default: -24h.',
492
498
  " - until: ISO-8601 upper bound (ignored when aggregating \u2014 aggregated view always extends to now).",
493
499
  " - limit: max rows (default 100, hard cap 500).",
494
500
  " - aggregate: true (default) collapses by (action, resourceId); false returns raw rows.",
501
+ " - active: true (default) shows only alerts that are still on fire (resolved_at IS NULL on the most recent row). false includes resolved historical alerts.",
495
502
  "",
496
- "Returns: { alerts: Array, aggregated: boolean }. Each aggregated entry includes action, resourceType, resourceId, severity, count, firstFiredAt, lastFiredAt, lastMetadata. Each raw entry includes id, action, resourceType, resourceId, severity, metadata, createdAt.",
503
+ "Returns: { alerts: Array, aggregated: boolean, activeOnly: boolean }. Each aggregated entry includes action, resourceType, resourceId, severity, count, firstFiredAt, lastFiredAt, lastResolvedAt (nullable), active (boolean), lastMetadata. Each raw entry adds resolvedAt (nullable).",
497
504
  "",
498
- "Example: list_alerts({ since: '-1h' }) \u2192 { alerts: [{ action: 'deploy.failed', resourceId: 31, severity: 'error', count: 3, lastFiredAt: '\u2026', \u2026 }] }."
505
+ "Example: list_alerts({ since: '-1h' }) \u2192 { alerts: [{ action: 'deploy.failed_consecutive', resourceId: 31, severity: 'critical', active: true, count: 3, lastFiredAt: '\u2026', lastResolvedAt: null, lastMetadata: { commitHash: 'abc1234' }, \u2026 }] }."
499
506
  ].join("\n"),
500
507
  input: {
501
508
  since: z3.string().optional().describe('ISO-8601 timestamp or relative offset (e.g. "-1h", "-2d"). Default: -24h.'),
502
509
  until: z3.string().optional().describe("ISO-8601 upper bound. Only honored when aggregate=false."),
503
510
  limit: z3.number().int().positive().max(500).optional().describe("Max rows (default 100, hard cap 500)."),
504
- aggregate: z3.boolean().optional().describe("Collapse by (action, resourceId). Default true.")
511
+ aggregate: z3.boolean().optional().describe("Collapse by (action, resourceId). Default true."),
512
+ active: z3.boolean().optional().describe(
513
+ "Only return alerts still on fire (resolved_at IS NULL on the most recent row). Default true. Pass false to include resolved historical alerts."
514
+ )
505
515
  },
506
516
  handler: async (args, ctx) => {
507
517
  const teamId = await ctx.resolveTeamId();
@@ -510,11 +520,13 @@ defineTool({
510
520
  if (args.until !== void 0) params["until"] = args.until;
511
521
  if (args.limit !== void 0) params["limit"] = String(args.limit);
512
522
  if (args.aggregate === false) params["aggregate"] = "0";
523
+ if (args.active === false) params["active"] = "0";
513
524
  const response = await ctx.api.get(`/api/alerts/${teamId}`, params);
514
525
  const items = Array.isArray(response.alerts) ? response.alerts.map(shape) : [];
515
526
  const aggregated = Boolean(response.aggregated);
516
- const summary = items.length === 0 ? "No alerts in the requested window \u2014 everything is operating normally." : aggregated ? `Returned ${items.length} alert group${items.length === 1 ? "" : "s"} (flapping events collapsed).` : `Returned ${items.length} raw alert event${items.length === 1 ? "" : "s"}.`;
517
- return respond({ summary, data: { alerts: items, aggregated } });
527
+ const activeOnly = args.active !== false;
528
+ const summary = items.length === 0 ? activeOnly ? "No active alerts in the requested window \u2014 everything is operating normally." : "No alerts in the requested window (active or resolved)." : aggregated ? `Returned ${items.length} alert group${items.length === 1 ? "" : "s"}${activeOnly ? " (active only \u2014 pass active=false for resolved history)" : ""} (flapping events collapsed).` : `Returned ${items.length} raw alert event${items.length === 1 ? "" : "s"}.`;
529
+ return respond({ summary, data: { alerts: items, aggregated, activeOnly } });
518
530
  }
519
531
  });
520
532
 
@@ -617,7 +629,7 @@ defineTool({
617
629
  "Inputs:",
618
630
  ' - database_id: publicId of the database (e.g. "db_\u2026").',
619
631
  " - name (optional): new database name.",
620
- ' - plan (optional): "free" | "starter" | "standard" | "pro" \u2014 changes memory tier.',
632
+ ' - plan (optional): "free" | "micro" | "starter" | "standard" | "pro" \u2014 changes memory tier.',
621
633
  " - disk_size_gb (optional): new disk size in GB (must be \u2265 current).",
622
634
  "",
623
635
  "Returns: { database: Database } \u2014 the updated record.",
@@ -627,7 +639,7 @@ defineTool({
627
639
  input: {
628
640
  database_id: z5.string().describe("Database publicId (e.g. db_xyz)."),
629
641
  name: z5.string().min(1).max(100).optional().describe("New database name."),
630
- plan: z5.enum(["free", "starter", "standard", "pro"]).optional().describe("Plan tier (memory/CPU)."),
642
+ plan: z5.enum(["free", "micro", "starter", "standard", "pro"]).optional().describe("Plan tier (memory/CPU)."),
631
643
  disk_size_gb: z5.number().int().min(1).max(1024).optional().describe("New disk size in GB. Must be \u2265 current.")
632
644
  },
633
645
  handler: async (args, ctx) => {
@@ -686,7 +698,7 @@ defineTool({
686
698
  "Inputs:",
687
699
  ' - service_id: publicId of the service (e.g. "svc_abc123"). Use list_services to find it.',
688
700
  "",
689
- "Returns: { items: Deploy[] } \u2014 each deploy includes id, publicId, status (pending|building|deploying|live|failed|cancelled), commitSha, commitMessage, branch, triggeredBy, startedAt, finishedAt, buildDurationMs (just the docker build / image-pull step), totalDurationMs (full deploy wall-clock = finishedAt \u2212 startedAt).",
701
+ 'Returns: { items: Deploy[] } \u2014 each deploy includes id, publicId, status (pending|building|deploying|live|failed|cancelled), commitSha, commitMessage, branch, triggeredBy, startedAt, finishedAt, imageBuildMs (docker build / image-pull only; null on skip-build redeploys \u2014 v89), containerBootMs (deploying \u2192 live wall-clock; null on builds that failed before container start \u2014 v89), buildDurationMs (legacy alias of imageBuildMs kept for back-compat), totalDurationMs (full deploy wall-clock = finishedAt \u2212 startedAt). Use imageBuildMs + containerBootMs together to tell "build is slow" apart from "boot is slow".',
690
702
  "",
691
703
  'Example: list_deploys({ service_id: "svc_abc" }) \u2192 { items: [{ publicId: "dpl_\u2026", status: "live", commitMessage: "Fix login", \u2026 }, \u2026] }'
692
704
  ].join("\n"),
@@ -713,7 +725,7 @@ defineTool({
713
725
  " - service_id: publicId of the service.",
714
726
  ' - deploy_id: publicId of the deploy (e.g. "dpl_\u2026").',
715
727
  "",
716
- "Returns: { deploy: Deploy } \u2014 full deploy record (status, commitSha, commitMessage, branch, buildDurationMs, totalDurationMs, finishedAt, etc).",
728
+ "Returns: { deploy: Deploy } \u2014 full deploy record (status, commitSha, commitMessage, branch, imageBuildMs + containerBootMs (v89 split timings; legacy buildDurationMs preserved as alias), totalDurationMs, finishedAt, etc).",
717
729
  "",
718
730
  'Example: get_deploy({ service_id: "svc_abc", deploy_id: "dpl_xyz" }) \u2192 { deploy: { status: "live", \u2026 } }'
719
731
  ].join("\n"),
@@ -1707,7 +1719,9 @@ defineTool({
1707
1719
  type: z11.enum(["slack", "discord", "email"]).describe("Channel type."),
1708
1720
  name: z11.string().min(1).max(128).describe("Human-readable label."),
1709
1721
  webhook_url: z11.string().max(500).describe("Slack/Discord webhook URL or email address (when type=email)."),
1710
- events: z11.array(z11.enum(NOTIFICATION_EVENTS)).describe("List of events the channel subscribes to. Empty list = subscribe to nothing.")
1722
+ events: z11.array(z11.enum(NOTIFICATION_EVENTS)).describe(
1723
+ "List of events the channel subscribes to. Empty list = subscribe to nothing."
1724
+ )
1711
1725
  },
1712
1726
  handler: async (args, ctx) => {
1713
1727
  const teamId = await ctx.resolveTeamId();
@@ -2008,24 +2022,30 @@ defineTool({
2008
2022
  name: "get_service",
2009
2023
  category: "services",
2010
2024
  description: [
2011
- "Fetch a single service by ID, including its current status and configuration summary.",
2025
+ "Fetch a single service by ID with its current status AND its service_config row (resources, health-check tuning, scaling, restart policy).",
2012
2026
  "",
2013
- "When to use: drilling into a service after list_services, checking deploy/runtime status, or grabbing the repo+branch before triggering a deploy.",
2027
+ "When to use: drilling into a service after list_services, checking deploy/runtime status, grabbing the repo+branch before triggering a deploy, or inspecting the health-check / autoscale knobs before tweaking them via update_service_config.",
2014
2028
  "",
2015
2029
  "Inputs:",
2016
2030
  ' - service_id: publicId of the service (e.g. "svc_abc123").',
2017
2031
  "",
2018
- "Returns: { service: Service } \u2014 type, status, runtime, repoUrl, branch, autoDeploy, region, plan, createdAt, updatedAt.",
2032
+ "Returns: { service: Service, config: ServiceConfig } \u2014 service has type/status/runtime/repoUrl/branch/autoDeploy/region/plan/timestamps; config has memoryMb, cpuShares, diskSizeGb, port, protocol, healthCheckEnabled, healthCheckInterval, healthCheckTimeout, healthCheckGracePeriodSec, restartPolicy, preDeployCommand, min/maxInstances, scale thresholds.",
2019
2033
  "",
2020
- 'Example: get_service({ service_id: "svc_abc" }) \u2192 { service: { type: "web", status: "running", \u2026 } }'
2034
+ 'Example: get_service({ service_id: "svc_abc" }) \u2192 { service: { type: "web", status: "running", \u2026 }, config: { healthCheckGracePeriodSec: 120, \u2026 } }'
2021
2035
  ].join("\n"),
2022
2036
  input: {
2023
2037
  service_id: z13.string().describe("Service publicId (e.g. svc_abc123).")
2024
2038
  },
2025
2039
  handler: async (args, ctx) => {
2026
2040
  const teamId = await ctx.resolveTeamId();
2027
- const response = await ctx.hoststack.services.get(teamId, args.service_id);
2028
- const data = { service: shapeService(response.service) };
2041
+ const [serviceResponse, configResponse] = await Promise.all([
2042
+ ctx.hoststack.services.get(teamId, args.service_id),
2043
+ ctx.hoststack.services.getConfig(teamId, args.service_id)
2044
+ ]);
2045
+ const data = {
2046
+ service: shapeService(serviceResponse.service),
2047
+ config: shape(configResponse.config)
2048
+ };
2029
2049
  const status = data.service && "status" in data.service ? data.service.status : "unknown";
2030
2050
  return respond({ summary: `Service ${args.service_id} is ${status}.`, data });
2031
2051
  }
@@ -2137,9 +2157,9 @@ defineTool({
2137
2157
  name: "update_service_config",
2138
2158
  category: "services",
2139
2159
  description: [
2140
- "Update build/runtime configuration for a service: build command, start command, install command, branch, root directory, dockerfile path, auto-deploy flag, instance count. All fields optional \u2014 pass only what you want to change.",
2160
+ "Update build/runtime configuration for a service. All fields optional \u2014 pass only what you want to change.",
2141
2161
  "",
2142
- "When to use: the user wants to tweak how a service builds or runs. Build/runtime fields (branch, install/build/start command, root, dockerfile) take effect on the next deploy \u2014 call trigger_deploy after if you need them applied immediately. instance_count rescales without a redeploy.",
2162
+ "When to use: the user wants to tweak how a service builds, runs, scales, or health-checks. Build/runtime fields (branch, install/build/start command, root, dockerfile) take effect on the next deploy \u2014 call trigger_deploy after if you need them applied immediately. Instance_count, resource and health-check changes rescale or rewire without a redeploy.",
2143
2163
  "",
2144
2164
  "Inputs:",
2145
2165
  " - service_id: publicId of the service.",
@@ -2148,12 +2168,26 @@ defineTool({
2148
2168
  " - root_directory (optional): build context root inside the repo.",
2149
2169
  " - dockerfile_path (optional): path to Dockerfile relative to root_directory. Pass null to clear.",
2150
2170
  " - auto_deploy (optional): boolean \u2014 auto-deploy on git push.",
2151
- " - instance_count (optional): integer \u22651 \u2014 pin both min and max instances to this value.",
2171
+ ' - health_check_path (optional): HTTP path the platform GETs to verify liveness (e.g. "/health"). Pass null for TCP-only check.',
2172
+ " - health_check_enabled (optional): boolean \u2014 toggle health checking on/off.",
2173
+ " - health_check_interval (optional): integer 5\u2013300 seconds \u2014 how often the check runs.",
2174
+ " - health_check_timeout (optional): integer 1\u201360 seconds \u2014 single-attempt timeout.",
2175
+ ' - health_check_grace_period_sec (optional): integer 1\u20131800 seconds \u2014 startup tolerance before failures count. RAISE THIS (e.g. 180) when the agent reports "Health check timed out" on a cold-boot app (Bun + Vite SSR typically need 90\u2013180s).',
2176
+ " - memory_mb (optional): integer 128\u201316384 \u2014 container memory cap.",
2177
+ " - cpu_shares (optional): integer 128\u20134096 \u2014 relative CPU weight.",
2178
+ " - disk_size_gb (optional): integer 1\u2013100 \u2014 ephemeral disk cap.",
2179
+ " - port (optional): integer 1\u201365535 \u2014 container port the platform forwards traffic to.",
2180
+ ' - protocol (optional): "http" | "tcp".',
2181
+ ' - restart_policy (optional): "always" | "on-failure" | "no".',
2182
+ " - pre_deploy_command (optional): shell command run before the new release accepts traffic (typical use: migrations).",
2183
+ " - instance_count (optional): integer 1\u201350 \u2014 pin both min and max instances to this value.",
2184
+ " - min_instances, max_instances (optional): integers \u2014 autoscale bounds. Use instead of instance_count when you want a range.",
2185
+ " - scale_cpu_threshold, scale_memory_threshold (optional): integer 10\u2013100 \u2014 autoscale trigger percentage.",
2152
2186
  ' - log_filter_rules (optional): list of { pattern, action } rules applied to runtime logs at query time. Pattern matches the message by case-insensitive substring; action is "drop" (filter out) or "downgrade" (flip stderr \u2192 stdout so it stops looking like an error). Pass [] to clear all rules. Capped at 50 rules.',
2153
2187
  "",
2154
2188
  "Returns: { service?: Service, config?: ServiceConfig } \u2014 whichever rows were touched.",
2155
2189
  "",
2156
- 'Example: update_service_config({ service_id: "svc_abc", start_command: "bun apps/api/src/index.ts" }) \u2192 { service: { startCommand: "bun apps/api/src/index.ts", \u2026 } }'
2190
+ 'Example: update_service_config({ service_id: "svc_abc", health_check_grace_period_sec: 180 }) \u2192 { config: { healthCheckGracePeriodSec: 180, \u2026 } }'
2157
2191
  ].join("\n"),
2158
2192
  input: {
2159
2193
  service_id: z13.string().describe("Service publicId."),
@@ -2164,7 +2198,25 @@ defineTool({
2164
2198
  root_directory: z13.string().optional().describe("Build context root."),
2165
2199
  dockerfile_path: z13.string().nullable().optional().describe("Path to Dockerfile relative to root. Null clears."),
2166
2200
  auto_deploy: z13.boolean().optional().describe("Auto-deploy on push."),
2201
+ health_check_path: z13.string().nullable().optional().describe('HTTP health-check path (e.g. "/health"). Null = TCP-only check.'),
2202
+ health_check_enabled: z13.boolean().optional().describe("Toggle health checking on/off."),
2203
+ health_check_interval: z13.number().int().min(5).max(300).optional().describe("How often the check runs, in seconds (5\u2013300)."),
2204
+ health_check_timeout: z13.number().int().min(1).max(60).optional().describe("Single-attempt timeout in seconds (1\u201360)."),
2205
+ health_check_grace_period_sec: z13.number().int().min(1).max(1800).optional().describe(
2206
+ "Startup grace period in seconds (1\u20131800). Raise this if the app needs more time to boot before health checks start counting failures."
2207
+ ),
2208
+ memory_mb: z13.number().int().min(128).max(16384).optional().describe("Container memory cap in MB (128\u201316384)."),
2209
+ cpu_shares: z13.number().int().min(128).max(4096).optional().describe("Relative CPU weight (128\u20134096)."),
2210
+ disk_size_gb: z13.number().int().min(1).max(100).optional().describe("Ephemeral disk size in GB (1\u2013100)."),
2211
+ port: z13.number().int().min(1).max(65535).optional().describe("Container port the platform forwards traffic to."),
2212
+ protocol: z13.enum(["http", "tcp"]).optional().describe("Traffic protocol."),
2213
+ restart_policy: z13.enum(["always", "on-failure", "no"]).optional().describe("Docker restart policy."),
2214
+ pre_deploy_command: z13.string().optional().describe("Shell command run before the new release accepts traffic."),
2167
2215
  instance_count: z13.number().int().positive().max(50).optional().describe("Pin min and max instances to this value (1\u201350)."),
2216
+ min_instances: z13.number().int().min(0).max(50).optional().describe("Autoscale lower bound. Use with max_instances for a range."),
2217
+ max_instances: z13.number().int().min(1).max(50).optional().describe("Autoscale upper bound. Use with min_instances for a range."),
2218
+ scale_cpu_threshold: z13.number().int().min(10).max(100).optional().describe("Autoscale CPU trigger percentage (10\u2013100)."),
2219
+ scale_memory_threshold: z13.number().int().min(10).max(100).optional().describe("Autoscale memory trigger percentage (10\u2013100)."),
2168
2220
  log_filter_rules: z13.array(
2169
2221
  z13.object({
2170
2222
  pattern: z13.string().min(1).max(200),
@@ -2186,11 +2238,35 @@ defineTool({
2186
2238
  if (args.branch !== void 0) serviceUpdate["branch"] = args.branch;
2187
2239
  if (args.root_directory !== void 0) serviceUpdate["rootDirectory"] = args.root_directory;
2188
2240
  if (args.auto_deploy !== void 0) serviceUpdate["autoDeploy"] = args.auto_deploy;
2241
+ if (args.health_check_path !== void 0)
2242
+ serviceUpdate["healthCheckPath"] = args.health_check_path;
2189
2243
  const configUpdate = {};
2244
+ if (args.health_check_enabled !== void 0)
2245
+ configUpdate["healthCheckEnabled"] = args.health_check_enabled;
2246
+ if (args.health_check_interval !== void 0)
2247
+ configUpdate["healthCheckInterval"] = args.health_check_interval;
2248
+ if (args.health_check_timeout !== void 0)
2249
+ configUpdate["healthCheckTimeout"] = args.health_check_timeout;
2250
+ if (args.health_check_grace_period_sec !== void 0)
2251
+ configUpdate["healthCheckGracePeriodSec"] = args.health_check_grace_period_sec;
2252
+ if (args.memory_mb !== void 0) configUpdate["memoryMb"] = args.memory_mb;
2253
+ if (args.cpu_shares !== void 0) configUpdate["cpuShares"] = args.cpu_shares;
2254
+ if (args.disk_size_gb !== void 0) configUpdate["diskSizeGb"] = args.disk_size_gb;
2255
+ if (args.port !== void 0) configUpdate["port"] = args.port;
2256
+ if (args.protocol !== void 0) configUpdate["protocol"] = args.protocol;
2257
+ if (args.restart_policy !== void 0) configUpdate["restartPolicy"] = args.restart_policy;
2258
+ if (args.pre_deploy_command !== void 0)
2259
+ configUpdate["preDeployCommand"] = args.pre_deploy_command;
2190
2260
  if (args.instance_count !== void 0) {
2191
2261
  configUpdate["minInstances"] = args.instance_count;
2192
2262
  configUpdate["maxInstances"] = args.instance_count;
2193
2263
  }
2264
+ if (args.min_instances !== void 0) configUpdate["minInstances"] = args.min_instances;
2265
+ if (args.max_instances !== void 0) configUpdate["maxInstances"] = args.max_instances;
2266
+ if (args.scale_cpu_threshold !== void 0)
2267
+ configUpdate["scaleCpuThreshold"] = args.scale_cpu_threshold;
2268
+ if (args.scale_memory_threshold !== void 0)
2269
+ configUpdate["scaleMemoryThreshold"] = args.scale_memory_threshold;
2194
2270
  if (args.log_filter_rules !== void 0) {
2195
2271
  configUpdate["logFilterRules"] = args.log_filter_rules;
2196
2272
  }