@prajwolkc/stk 0.7.0 → 0.7.1

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.
@@ -0,0 +1,263 @@
1
+ import { z } from "zod";
2
+ import { loadConfig, enabledServices } from "../../lib/config.js";
3
+ import { getChecker, allCheckerNames, loadPluginCheckers } from "../../services/registry.js";
4
+ import { execSync } from "child_process";
5
+ export function registerInfraTools(server) {
6
+ // ──────────────────────────────────────────
7
+ // Tool: stk_health
8
+ // ──────────────────────────────────────────
9
+ server.tool("stk_health", "Check the health of all configured infrastructure services (databases, deploy providers, storage, billing). Returns structured results with status, latency, and details for each service.", {
10
+ all: z.boolean().optional().describe("Check all known services, not just configured ones"),
11
+ }, async ({ all }) => {
12
+ await loadPluginCheckers();
13
+ const config = loadConfig();
14
+ const serviceList = all ? allCheckerNames() : enabledServices(config);
15
+ const checks = serviceList.map(async (name) => {
16
+ const checker = getChecker(name);
17
+ if (!checker) {
18
+ return { name, status: "skipped", detail: `unknown service "${name}"` };
19
+ }
20
+ return checker();
21
+ });
22
+ const results = await Promise.all(checks);
23
+ const down = results.filter((r) => r.status === "down");
24
+ return {
25
+ content: [
26
+ {
27
+ type: "text",
28
+ text: JSON.stringify({
29
+ project: config.name,
30
+ services: results,
31
+ summary: {
32
+ healthy: results.filter((r) => r.status === "healthy").length,
33
+ down: down.length,
34
+ skipped: results.filter((r) => r.status === "skipped").length,
35
+ total: results.length,
36
+ },
37
+ ok: down.length === 0,
38
+ }, null, 2),
39
+ },
40
+ ],
41
+ };
42
+ });
43
+ // ──────────────────────────────────────────
44
+ // Tool: stk_status
45
+ // ──────────────────────────────────────────
46
+ server.tool("stk_status", "Get a complete status overview: git state, service health, last deploy, and open issues — everything in one call.", {}, async () => {
47
+ const config = loadConfig();
48
+ const status = { project: config.name };
49
+ // Git
50
+ try {
51
+ status.git = {
52
+ branch: execSync("git rev-parse --abbrev-ref HEAD", { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }).trim(),
53
+ dirty: execSync("git status --porcelain", { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }).trim().split("\n").filter(Boolean).length,
54
+ lastCommit: execSync('git log -1 --format="%s"', { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }).trim(),
55
+ lastCommitAge: execSync('git log -1 --format="%cr"', { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }).trim(),
56
+ };
57
+ }
58
+ catch {
59
+ status.git = null;
60
+ }
61
+ // Services
62
+ await loadPluginCheckers();
63
+ const serviceList = enabledServices(config);
64
+ if (serviceList.length > 0) {
65
+ const checks = serviceList.map(async (name) => {
66
+ const checker = getChecker(name);
67
+ if (!checker)
68
+ return { name, status: "skipped" };
69
+ return checker();
70
+ });
71
+ const results = await Promise.all(checks);
72
+ status.services = {
73
+ healthy: results.filter((r) => r.status === "healthy").length,
74
+ down: results.filter((r) => r.status === "down").map((r) => r.name),
75
+ skipped: results.filter((r) => r.status === "skipped").length,
76
+ total: results.length,
77
+ };
78
+ }
79
+ else {
80
+ status.services = { total: 0, note: "no services configured" };
81
+ }
82
+ // Deploy
83
+ if (process.env.VERCEL_TOKEN) {
84
+ try {
85
+ const res = await fetch("https://api.vercel.com/v6/deployments?limit=1", {
86
+ headers: { Authorization: `Bearer ${process.env.VERCEL_TOKEN}` },
87
+ });
88
+ const data = (await res.json());
89
+ const dep = data.deployments?.[0];
90
+ if (dep) {
91
+ status.lastDeploy = {
92
+ provider: "vercel",
93
+ state: dep.readyState ?? dep.state,
94
+ url: dep.url,
95
+ created: dep.created,
96
+ };
97
+ }
98
+ }
99
+ catch { /* skip */ }
100
+ }
101
+ return {
102
+ content: [{ type: "text", text: JSON.stringify(status, null, 2) }],
103
+ };
104
+ });
105
+ // ──────────────────────────────────────────
106
+ // Tool: stk_doctor
107
+ // ──────────────────────────────────────────
108
+ server.tool("stk_doctor", "Diagnose infrastructure configuration issues. Checks for missing env vars, mismatched config, invalid URLs, and suggests fixes with documentation links.", {}, async () => {
109
+ const config = loadConfig();
110
+ const enabled = enabledServices(config);
111
+ const issues = [];
112
+ const ENV_REQS = {
113
+ railway: { required: ["RAILWAY_API_TOKEN"], optional: ["RAILWAY_PROJECT_ID", "RAILWAY_ENVIRONMENT_ID", "RAILWAY_SERVICE_ID"] },
114
+ vercel: { required: ["VERCEL_TOKEN"], optional: ["VERCEL_PROJECT_ID"] },
115
+ fly: { required: ["FLY_API_TOKEN"], optional: ["FLY_APP_NAME"] },
116
+ render: { required: ["RENDER_API_KEY"], optional: [] },
117
+ aws: { required: ["AWS_ACCESS_KEY_ID", "AWS_SECRET_ACCESS_KEY"], optional: ["AWS_REGION"] },
118
+ database: { required: ["DATABASE_URL"], optional: [] },
119
+ mongodb: { required: ["MONGODB_URL"], optional: [] },
120
+ redis: { required: ["REDIS_URL"], optional: [] },
121
+ supabase: { required: ["SUPABASE_URL"], optional: ["SUPABASE_SERVICE_KEY"] },
122
+ r2: { required: ["CLOUDFLARE_ACCOUNT_ID", "CLOUDFLARE_API_TOKEN"], optional: [] },
123
+ stripe: { required: ["STRIPE_SECRET_KEY"], optional: [] },
124
+ };
125
+ for (const svc of enabled) {
126
+ const reqs = ENV_REQS[svc];
127
+ if (!reqs)
128
+ continue;
129
+ const missingReq = reqs.required.filter((v) => !process.env[v]);
130
+ const missingOpt = reqs.optional.filter((v) => !process.env[v]);
131
+ if (missingReq.length > 0) {
132
+ issues.push({ level: "error", service: svc, message: `Missing required: ${missingReq.join(", ")}` });
133
+ }
134
+ else {
135
+ issues.push({ level: "ok", service: svc, message: "Configured correctly" });
136
+ }
137
+ if (missingOpt.length > 0) {
138
+ issues.push({ level: "warn", service: svc, message: `Missing optional: ${missingOpt.join(", ")}`, fix: "Needed for logs, env sync, deploy watching" });
139
+ }
140
+ }
141
+ return {
142
+ content: [{
143
+ type: "text",
144
+ text: JSON.stringify({
145
+ project: config.name,
146
+ issues,
147
+ summary: {
148
+ errors: issues.filter((i) => i.level === "error").length,
149
+ warnings: issues.filter((i) => i.level === "warn").length,
150
+ ok: issues.filter((i) => i.level === "ok").length,
151
+ },
152
+ }, null, 2),
153
+ }],
154
+ };
155
+ });
156
+ // ──────────────────────────────────────────
157
+ // Tool: stk_config
158
+ // ──────────────────────────────────────────
159
+ server.tool("stk_config", "Read the current stk configuration for this project. Shows which services are enabled, deploy settings, and project name.", {}, async () => {
160
+ const config = loadConfig();
161
+ return {
162
+ content: [{ type: "text", text: JSON.stringify(config, null, 2) }],
163
+ };
164
+ });
165
+ // ──────────────────────────────────────────
166
+ // Tool: stk_alerts
167
+ // ──────────────────────────────────────────
168
+ server.tool("stk_alerts", "Scan for problems across your entire stack: failed deploys, down services, error logs, Stripe failures, and database issues. Returns actionable alerts.", {}, async () => {
169
+ await loadPluginCheckers();
170
+ const config = loadConfig();
171
+ const alerts = [];
172
+ // 1. Check all service health
173
+ const serviceList = enabledServices(config);
174
+ const checks = serviceList.map(async (name) => {
175
+ const checker = getChecker(name);
176
+ if (!checker)
177
+ return null;
178
+ return checker();
179
+ });
180
+ const results = (await Promise.all(checks)).filter(Boolean);
181
+ for (const r of results) {
182
+ if (r.status === "down") {
183
+ alerts.push({ level: "critical", source: r.name, message: `Service is DOWN: ${r.detail ?? "unreachable"}` });
184
+ }
185
+ else if (r.status === "degraded") {
186
+ alerts.push({ level: "warning", source: r.name, message: `Service degraded: ${r.detail ?? "slow response"}` });
187
+ }
188
+ else if (r.latency && r.latency > 3000) {
189
+ alerts.push({ level: "warning", source: r.name, message: `High latency: ${r.latency}ms` });
190
+ }
191
+ }
192
+ // 2. Check Vercel for failed deploys
193
+ if (process.env.VERCEL_TOKEN) {
194
+ try {
195
+ const res = await fetch("https://api.vercel.com/v6/deployments?limit=5", {
196
+ headers: { Authorization: `Bearer ${process.env.VERCEL_TOKEN}` },
197
+ });
198
+ const data = await res.json();
199
+ for (const dep of data.deployments ?? []) {
200
+ const state = dep.readyState ?? dep.state;
201
+ if (state === "ERROR" || state === "CANCELED") {
202
+ alerts.push({ level: "critical", source: "Vercel", message: `Deploy ${state}: ${dep.url ?? dep.uid}` });
203
+ }
204
+ }
205
+ }
206
+ catch { /* skip */ }
207
+ }
208
+ // 3. Check Stripe for recent failures
209
+ if (process.env.STRIPE_SECRET_KEY) {
210
+ try {
211
+ const res = await fetch("https://api.stripe.com/v1/charges?limit=20", {
212
+ headers: { Authorization: `Bearer ${process.env.STRIPE_SECRET_KEY}` },
213
+ });
214
+ const data = await res.json();
215
+ const failed = (data.data ?? []).filter((c) => c.status === "failed");
216
+ if (failed.length > 0) {
217
+ alerts.push({ level: "warning", source: "Stripe", message: `${failed.length} failed charge(s) in recent transactions` });
218
+ }
219
+ }
220
+ catch { /* skip */ }
221
+ }
222
+ // 4. Check for error logs in Vercel
223
+ if (process.env.VERCEL_TOKEN) {
224
+ try {
225
+ const depRes = await fetch("https://api.vercel.com/v6/deployments?limit=1", {
226
+ headers: { Authorization: `Bearer ${process.env.VERCEL_TOKEN}` },
227
+ });
228
+ const depData = await depRes.json();
229
+ const dep = depData.deployments?.[0];
230
+ if (dep) {
231
+ const logRes = await fetch(`https://api.vercel.com/v2/deployments/${dep.uid}/events`, {
232
+ headers: { Authorization: `Bearer ${process.env.VERCEL_TOKEN}` },
233
+ });
234
+ const events = await logRes.json();
235
+ if (Array.isArray(events)) {
236
+ const errors = events.filter((e) => e.type === "stderr");
237
+ if (errors.length > 5) {
238
+ alerts.push({ level: "warning", source: "Vercel Logs", message: `${errors.length} stderr entries in latest deploy` });
239
+ }
240
+ }
241
+ }
242
+ }
243
+ catch { /* skip */ }
244
+ }
245
+ if (alerts.length === 0) {
246
+ alerts.push({ level: "info", source: "stk", message: "All clear — no issues detected" });
247
+ }
248
+ return {
249
+ content: [{
250
+ type: "text",
251
+ text: JSON.stringify({
252
+ project: config.name,
253
+ alerts,
254
+ summary: {
255
+ critical: alerts.filter((a) => a.level === "critical").length,
256
+ warnings: alerts.filter((a) => a.level === "warning").length,
257
+ ok: alerts.every((a) => a.level === "info"),
258
+ },
259
+ }, null, 2),
260
+ }],
261
+ };
262
+ });
263
+ } // end registerInfraTools
@@ -0,0 +1,2 @@
1
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ export declare function registerOpsTools(server: McpServer): void;