@hoststack.dev/mcp 0.6.1 → 0.8.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/hoststack-mcp.js +63 -10
- package/dist/hoststack-mcp.js.map +1 -1
- package/dist/index.js +63 -10
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
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
|
|
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.
|
|
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
|
|
517
|
-
|
|
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) => {
|
|
@@ -672,6 +684,45 @@ defineTool({
|
|
|
672
684
|
return respond({ summary: `Database ${args.database_id} (${type}) is ${status}.`, data });
|
|
673
685
|
}
|
|
674
686
|
});
|
|
687
|
+
defineTool({
|
|
688
|
+
name: "query_database",
|
|
689
|
+
category: "databases",
|
|
690
|
+
description: [
|
|
691
|
+
"Run a single READ-ONLY SQL statement against a managed Postgres database.",
|
|
692
|
+
"",
|
|
693
|
+
"When to use: ad-hoc data inspection \u2014 checking row counts, sampling rows, debugging a constraint. For schema changes or seed data, ship a Drizzle migration instead.",
|
|
694
|
+
"",
|
|
695
|
+
"Safety model (server-side, not bypassable):",
|
|
696
|
+
' - Wrapped in `BEGIN TRANSACTION READ ONLY` \u2014 INSERT/UPDATE/DELETE/DDL fail with a clear "cannot execute \u2026 in a read-only transaction" error.',
|
|
697
|
+
" - 30s statement_timeout \u2014 runaway queries are killed.",
|
|
698
|
+
" - 1000-row cap; result CSV is also size-capped at 1 MiB. Both surface via `truncated: true`.",
|
|
699
|
+
" - Postgres engine only (v1). Other engines return a 400.",
|
|
700
|
+
" - Single statement only; no embedded `;` and no psql meta-commands (`\\\u2026`).",
|
|
701
|
+
' - Every call is audit-logged (success or failure) as `database.query` so an admin can answer "who queried this DB, when, with what SQL".',
|
|
702
|
+
"",
|
|
703
|
+
"Inputs:",
|
|
704
|
+
` - database_id: publicId of the postgres database (e.g. "db_\u2026"). Resolved via the SDK's publicId helper.`,
|
|
705
|
+
" - sql: a single SELECT/WITH/SHOW/EXPLAIN statement, no trailing `;`. Max 16 KiB.",
|
|
706
|
+
"",
|
|
707
|
+
"Returns: { columns: string[], rows: string[][], rowCount: number, truncated: boolean, durationMs: number }. Values come back as strings (psql --csv); cast on the caller side when needed.",
|
|
708
|
+
"",
|
|
709
|
+
"Examples:",
|
|
710
|
+
' - query_database({ database_id: "db_abc", sql: "SELECT count(*) FROM users" }) \u2192 { columns: ["count"], rows: [["1234"]], \u2026 }',
|
|
711
|
+
` - query_database({ database_id: "db_abc", sql: "SELECT id, email FROM users WHERE created_at > now() - interval '1 day' LIMIT 50" })`
|
|
712
|
+
].join("\n"),
|
|
713
|
+
input: {
|
|
714
|
+
database_id: z5.string().describe("Database publicId (e.g. db_abc)."),
|
|
715
|
+
sql: z5.string().min(1).max(16384).describe(
|
|
716
|
+
"A single READ-ONLY SQL statement. No trailing `;`, no embedded `;`, no psql meta-commands."
|
|
717
|
+
)
|
|
718
|
+
},
|
|
719
|
+
handler: async (args, ctx) => {
|
|
720
|
+
const teamId = await ctx.resolveTeamId();
|
|
721
|
+
const result = await ctx.hoststack.databases.query(teamId, args.database_id, args.sql);
|
|
722
|
+
const summary = result.truncated ? `Query returned ${result.rowCount} row${result.rowCount === 1 ? "" : "s"} (truncated; raise LIMIT if needed) in ${result.durationMs}ms.` : `Query returned ${result.rowCount} row${result.rowCount === 1 ? "" : "s"} in ${result.durationMs}ms.`;
|
|
723
|
+
return respond({ summary, data: result });
|
|
724
|
+
}
|
|
725
|
+
});
|
|
675
726
|
|
|
676
727
|
// src/tools/deploys.ts
|
|
677
728
|
import { z as z6 } from "zod";
|
|
@@ -686,7 +737,7 @@ defineTool({
|
|
|
686
737
|
"Inputs:",
|
|
687
738
|
' - service_id: publicId of the service (e.g. "svc_abc123"). Use list_services to find it.',
|
|
688
739
|
"",
|
|
689
|
-
|
|
740
|
+
'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
741
|
"",
|
|
691
742
|
'Example: list_deploys({ service_id: "svc_abc" }) \u2192 { items: [{ publicId: "dpl_\u2026", status: "live", commitMessage: "Fix login", \u2026 }, \u2026] }'
|
|
692
743
|
].join("\n"),
|
|
@@ -713,7 +764,7 @@ defineTool({
|
|
|
713
764
|
" - service_id: publicId of the service.",
|
|
714
765
|
' - deploy_id: publicId of the deploy (e.g. "dpl_\u2026").',
|
|
715
766
|
"",
|
|
716
|
-
"Returns: { deploy: Deploy } \u2014 full deploy record (status, commitSha, commitMessage, branch, buildDurationMs, totalDurationMs, finishedAt, etc).",
|
|
767
|
+
"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
768
|
"",
|
|
718
769
|
'Example: get_deploy({ service_id: "svc_abc", deploy_id: "dpl_xyz" }) \u2192 { deploy: { status: "live", \u2026 } }'
|
|
719
770
|
].join("\n"),
|
|
@@ -1707,7 +1758,9 @@ defineTool({
|
|
|
1707
1758
|
type: z11.enum(["slack", "discord", "email"]).describe("Channel type."),
|
|
1708
1759
|
name: z11.string().min(1).max(128).describe("Human-readable label."),
|
|
1709
1760
|
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(
|
|
1761
|
+
events: z11.array(z11.enum(NOTIFICATION_EVENTS)).describe(
|
|
1762
|
+
"List of events the channel subscribes to. Empty list = subscribe to nothing."
|
|
1763
|
+
)
|
|
1711
1764
|
},
|
|
1712
1765
|
handler: async (args, ctx) => {
|
|
1713
1766
|
const teamId = await ctx.resolveTeamId();
|